- Published on
RAG 응답 느림? pgvector IVFFlat 튜닝 6단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG에서 “응답이 느리다”는 말은 대개 LLM 추론이 아니라 벡터 검색 단계에서 시작됩니다. 특히 Postgres + pgvector 조합은 운영 편의성이 뛰어나지만, 기본 설정 그대로 쓰면 데이터가 커질수록 지연이 급격히 늘어납니다. 이 글은 pgvector의 대표적인 근사 최근접 검색(ANN) 인덱스인 IVFFlat을 기준으로, 측정부터 재현 가능한 튜닝 순서를 6단계로 정리합니다.
아래 단계는 단순히 파라미터를 “올려보는” 접근이 아니라, 지연(latency)·정확도(recall)·비용(CPU/IO) 트레이드오프를 통제하는 방식으로 구성했습니다.
0. 전제: 느린 원인이 정말 벡터 검색인가?
RAG 파이프라인 전체를 쪼개면 보통 다음 4구간입니다.
- 임베딩 생성(encode)
- 벡터 검색(retrieve)
- 재랭킹(optional)
- LLM 생성(generate)
체감상 “RAG가 느리다”의 상당수는 2번 retrieve에서 발생합니다. 하지만 애플리케이션 레벨에서 타임아웃/동시성 설정이 원인일 수도 있습니다. 예를 들어 서버리스나 프록시에서 503/504가 섞여 보이면 검색이 아니라 요청 처리 한계일 수 있습니다. 운영 환경에서 HTTP 타임아웃과 동시성도 같이 점검하세요.
이제 “retrieve가 병목”이라는 가정 하에 본격적으로 들어갑니다.
1단계: EXPLAIN (ANALYZE, BUFFERS)로 병목을 숫자로 고정
튜닝의 시작은 감이 아니라 플랜과 버퍼 통계입니다. pgvector 쿼리는 보통 아래 형태입니다.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, doc_id, embedding <-> $1 AS distance
FROM chunks
WHERE tenant_id = $2
ORDER BY embedding <-> $1
LIMIT 10;
여기서 확인할 포인트는 다음입니다.
- 플랜에
Index Scan using ... ivfflat이 뜨는지 BUFFERS에서 shared hit/read 비율(디스크 read가 많으면 느림)Sort가 발생하는지(인덱스가 제대로 쓰이면 불필요한 정렬이 줄어듦)- 실행 시간의 대부분이 어디에 있는지
만약 Seq Scan이 보이면, 인덱스가 없거나(혹은 사용 불가) 조건 때문에 인덱스를 못 타는 상태입니다. 이 글의 다음 단계들이 바로 그 원인을 제거하는 과정입니다.
팁: 벡터 검색은 워크로드 특성상 “가끔만 느린” 꼬리 지연이 문제를 크게 만듭니다. p50뿐 아니라 p95, p99도 같이 보세요.
2단계: 거리 함수와 연산자 클래스(opclass) 일치시키기
pgvector는 거리 측정 방식에 따라 인덱스 연산자 클래스가 다릅니다. 여기서 불일치가 나면 인덱스를 만들었어도 쿼리가 인덱스를 못 탑니다.
대표적으로 많이 쓰는 조합은 아래 3가지입니다.
- L2 거리:
vector_l2_ops와 연산자<-> - 내적:
vector_ip_ops와 연산자<#> - 코사인 거리:
vector_cosine_ops와 연산자<=>
예: 코사인 거리 기반으로 검색할 거면 인덱스도 코사인 opclass로 만들어야 합니다.
CREATE INDEX CONCURRENTLY chunks_embedding_ivfflat_cos
ON chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 200);
그리고 쿼리도 코사인 연산자를 써야 합니다.
SELECT id, doc_id, embedding <=> $1 AS distance
FROM chunks
ORDER BY embedding <=> $1
LIMIT 10;
실무에서 흔한 실수는 “모델은 cosine로 학습/평가했는데, DB는 <->로 돌리고 있었다” 같은 케이스입니다. 이 경우 성능과 품질 둘 다 흔들립니다.
3단계: lists 산정하기(인덱스의 코어 파라미터)
IVFFlat은 전체 벡터를 lists개의 버킷으로 나누고, 쿼리 시 일부 버킷만 탐색합니다. lists가 작으면 빠르지만 recall이 떨어지고, 크면 recall이 좋아지지만 인덱스 크기와 빌드 비용이 증가합니다.
경험칙(절대 법칙 아님):
- 데이터가
N개일 때lists를sqrt(N)근처에서 시작 - 또는
N / 1000수준에서 시작(데이터 분포에 따라 조정)
예시:
N = 1,000,000이면sqrt(N) = 1000이므로lists = 1000부터 시도
DROP INDEX CONCURRENTLY IF EXISTS chunks_embedding_ivfflat_cos;
CREATE INDEX CONCURRENTLY chunks_embedding_ivfflat_cos
ON chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 1000);
중요한 포인트:
lists는 인덱스 생성 시 결정되며, 바꾸려면 재생성이 필요합니다.lists를 너무 크게 잡으면 각 list가 너무 작아져 오히려 효율이 떨어질 수 있습니다(오버헤드 증가).
운영 팁:
- 대규모 테이블에서 인덱스 재생성은 I/O를 크게 씁니다.
CONCURRENTLY를 사용하고, 유지보수 윈도우를 확보하세요.
4단계: ivfflat.probes로 지연-정확도 트레이드오프 제어
probes는 쿼리 시 몇 개의 list를 탐색할지 결정합니다. probes가 커질수록 recall은 좋아지지만 느려집니다.
세션 단위로 빠르게 실험할 수 있다는 점이 핵심입니다.
SET LOCAL ivfflat.probes = 5;
SELECT id, doc_id
FROM chunks
ORDER BY embedding <=> $1
LIMIT 10;
튜닝 순서 권장:
- 먼저
lists를 “합리적으로” 잡는다(3단계) - 그 다음
probes를 올리며 목표 recall을 맞춘다
실무에서 자주 쓰는 접근:
- 기본값을 낮게(예: 5~10) 두고
- “정확도가 중요한 요청”(예: 최종 답변 생성)에서만
probes를 높이는 전략
예를 들어, 1차 후보군은 빠르게 뽑고(낮은 probes), 상위 요청에서만 정확도를 끌어올리는 방식입니다.
주의:
probes를lists와 같게 만들면 사실상 대부분을 훑게 되어 ANN 이점이 줄어듭니다.
5단계: 필터 조건(테넌트/권한/문서 타입)이 느리게 만드는 구조 개선
RAG는 거의 항상 필터가 있습니다.
tenant_iddocument_id in (...)visibilitylang
문제는 IVFFlat이 “벡터 기준으로 후보를 찾은 뒤” 필터를 적용하는 형태가 되기 쉬워, 필터 선택도가 높으면(즉, 조건이 빡세면) 후보가 많이 탈락하면서 더 많은 탐색이 필요해집니다.
이때 선택지는 크게 3가지입니다.
5-1. 파티셔닝으로 검색 공간 자체를 줄이기
테넌트 단위가 명확하면 테이블 파티셔닝이 강력합니다.
CREATE TABLE chunks (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
doc_id bigint NOT NULL,
embedding vector(1536) NOT NULL,
content text NOT NULL
) PARTITION BY LIST (tenant_id);
각 파티션에 IVFFlat 인덱스를 만들면, 특정 테넌트 쿼리는 해당 파티션에서만 검색합니다.
5-2. 부분 인덱스(partial index)로 “자주 조회되는 subset” 최적화
예: visibility = 'public'만 자주 검색한다면 부분 인덱스를 고려합니다.
CREATE INDEX CONCURRENTLY chunks_public_embedding_ivfflat
ON chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 500)
WHERE visibility = 'public';
5-3. 메타데이터 인덱스와의 조합을 점검
벡터 검색 전에 필터 후보를 줄이려면 tenant_id, doc_id 같은 컬럼에도 적절한 B-tree 인덱스가 필요합니다.
CREATE INDEX CONCURRENTLY chunks_tenant_doc_idx
ON chunks (tenant_id, doc_id);
다만 Postgres 플래너가 어떤 순서로 실행할지는 실제 통계/카디널리티에 따라 달라지므로, 1단계의 EXPLAIN으로 확인하면서 조정해야 합니다.
6단계: 운영 체크리스트(리빌드, 통계, 메모리, 커넥션)
튜닝이 “한 번 하고 끝”이 아닌 이유는, 데이터가 늘고 분포가 바뀌면 최적점이 이동하기 때문입니다. 아래는 운영에서 자주 놓치는 항목들입니다.
6-1. 통계 갱신: ANALYZE 없으면 플랜이 틀어진다
대량 적재 후에는 통계를 갱신하세요.
VACUUM (ANALYZE) chunks;
6-2. 인덱스 재생성 타이밍
IVFFlat은 데이터가 크게 변하면 품질/성능이 흔들릴 수 있습니다. 다음 상황이면 재생성을 고려합니다.
- 데이터가 초기 대비 2배 이상 증가
- 테넌트/도메인 분포가 크게 변함
- recall을 맞추려
probes를 계속 올리고 있음(= 인덱스 구조가 현재 분포에 비효율)
6-3. 메모리/캐시 관점: 디스크 read가 많으면 체감이 급락
BUFFERS에서 read가 많으면, 단순히 lists/probes 문제가 아니라 캐시 미스가 큽니다.
shared_buffers가 너무 작지 않은지- 워킹셋이 메모리에 올라갈 수 있는지
- 스토리지가 느리지 않은지
특히 RAG는 피크 트래픽에서 같은 테넌트/같은 문서군이 반복 조회되는 경우가 많아 캐시 효율이 중요합니다.
6-4. 커넥션/풀링과 꼬리 지연
DB 커넥션 풀이 고갈되면 검색 쿼리 자체가 느린 게 아니라 “대기”가 느려집니다. 애플리케이션 APM에서 DB 대기 시간을 분리해 보세요.
6-5. 벤치마크는 “정확도”까지 같이 본다
속도만 보고 probes를 낮추면 답변 품질이 무너질 수 있습니다. 최소한 다음을 같이 측정하세요.
- latency:
p50/p95/p99 - recall@k 또는 hit rate(정답 문서가 top-k에 들어오는 비율)
- 비용: CPU 사용률, I/O read, 인덱스 크기
추가로, 벡터 DB/인덱스는 시간이 지나며 임베딩 분포가 변하는 “드리프트”가 생길 수 있습니다. 품질이 서서히 떨어진다면 드리프트 관점 점검도 병행하세요.
실전 예시: 튜닝 실험을 재현 가능하게 만드는 SQL 스니펫
아래는 같은 쿼리를 probes만 바꿔가며 측정할 때 유용한 패턴입니다.
-- 1) 코사인 기반 IVFFlat 인덱스가 이미 있다고 가정
-- 2) probes를 바꿔가며 플랜/버퍼/시간을 비교
DO $$
DECLARE
p int;
BEGIN
FOREACH p IN ARRAY ARRAY[1, 5, 10, 20, 50] LOOP
RAISE NOTICE 'probes=%', p;
EXECUTE format('SET LOCAL ivfflat.probes = %s', p);
-- 실제로는 애플리케이션에서 바인딩 파라미터를 사용하세요.
-- 여기서는 예시를 위해 $1, $2 형태로 표기합니다.
EXECUTE $q$
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, doc_id
FROM chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10
$q$;
END LOOP;
END $$;
이렇게 하면 “어느 probes에서 p95가 목표치에 들어오는지”를 팀 내에서 동일한 방식으로 합의할 수 있습니다.
마무리: 6단계 요약
IVFFlat 튜닝은 결국 다음 2가지를 맞추는 작업입니다.
- 인덱스 구조(
lists)를 데이터 규모/분포에 맞게 잡고 - 탐색량(
probes)을 SLO와 품질 목표에 맞게 조절
정리하면:
EXPLAIN (ANALYZE, BUFFERS)로 병목을 수치화- 거리 함수와 opclass를 일치(L2/IP/Cosine)
lists를 데이터 규모에 맞게 산정하고 재생성ivfflat.probes로 지연-정확도 트레이드오프 제어- 필터 구조(파티셔닝/부분 인덱스/메타 인덱스)로 검색 공간 축소
- 운영 체크(ANALYZE, 캐시, 리빌드 타이밍, 커넥션)로 꼬리 지연 방지
이 6단계를 순서대로 적용하면 “RAG가 느리다”를 막연한 감상이 아니라, 재현 가능한 튜닝 과제로 바꿀 수 있습니다.
- 내부 참고: RAG 응답이 느릴 때 애플리케이션 레벨 타임아웃/동시성도 함께 점검하려면 GCP Cloud Run 503/504 원인별 해결 - 타임아웃·동시성도 같이 보세요.