- Published on
pgvector RAG 느림? IVFFlat 튜닝 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG를 PostgreSQL pgvector로 운영하다 보면 “처음엔 괜찮았는데 점점 검색이 느려진다”, “인덱스를 만들었는데도 시퀀셜 스캔이 난다”, “필터를 붙이면 지연이 폭증한다” 같은 증상을 자주 겪습니다. 특히 IVFFlat은 설정을 대충 잡으면 정확도와 지연이 동시에 망가지기 쉬운 인덱스라, 데이터 분포와 쿼리 패턴에 맞춘 튜닝이 필수입니다.
이 글은 IVFFlat을 기준으로, 느린 RAG 검색을 원인별로 분해하고 lists, probes, 쿼리 작성 방식, 유지보수까지 한 번에 점검하는 체크리스트를 제공합니다. (HNSW가 더 적합한 케이스도 있으니 비교가 필요하면 pgvector HNSW 튜닝으로 RAG 검색지연 50% 줄이기도 함께 참고하세요.)
1) IVFFlat이 느려지는 대표 원인 6가지
1-1. lists가 너무 작거나 너무 큼
IVFFlat은 벡터를 여러 클러스터(lists)로 나누고, 검색 시 일부 클러스터만 탐색합니다.
lists가 너무 작음: 각 list에 데이터가 너무 많이 들어가서 스캔량 증가lists가 너무 큼: 각 list가 너무 잘게 쪼개져서 탐색 오버헤드 증가 및 학습(빌드) 비용 증가
1-2. probes 기본값(또는 과도한 값) 문제
probes는 검색 시 몇 개 list를 열어볼지 결정합니다.
- 너무 낮음: 빠르지만 리콜 저하
- 너무 높음: 리콜은 좋아지나 지연 급증
1-3. WHERE 필터가 인덱스 활용을 깨뜨림
RAG는 보통 tenant_id, doc_type, collection_id, lang 같은 메타 필터가 붙습니다. 이때 필터 선택도가 낮거나(너무 많은 행이 통과) 쿼리 작성이 비효율적이면, IVFFlat로 후보를 좁히지 못하고 많은 후보를 recheck 하면서 느려집니다.
1-4. 정렬/거리 연산자 선택 실수
pgvector는 거리 연산자(예: 코사인, L2 등)에 따라 인덱스 연산자 클래스가 달라집니다. 거리 연산과 인덱스 정의가 불일치하면 인덱스가 무용지물이 됩니다.
1-5. 테이블/인덱스 bloat 및 통계 부정확
업데이트/삭제가 누적되면 bloat가 커지고, 플래너가 잘못된 계획을 선택할 수 있습니다. 특히 대량 적재 후 ANALYZE가 부족하면 인덱스가 있어도 계획이 어긋납니다. (운영 중 VACUUM 이슈가 의심되면 PostgreSQL VACUUM 안 돼 bloat 폭증? 즉시 복구도 같이 확인하세요.)
1-6. 임베딩 차원/정규화 정책이 일관되지 않음
코사인 유사도를 쓰는데 벡터가 정규화되어 있지 않거나, 서로 다른 모델/차원이 섞이면 검색 품질과 성능이 동시에 흔들립니다.
2) 전제: 현재 쿼리가 “인덱스를 타는지”부터 확인
튜닝은 감으로 하면 끝이 없습니다. 먼저 EXPLAIN (ANALYZE, BUFFERS)로 실제 실행 계획을 확인합니다.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, doc_id, chunk_id,
1 - (embedding <=> $1) AS score
FROM rag_chunks
WHERE tenant_id = $2
AND collection_id = $3
ORDER BY embedding <=> $1
LIMIT 10;
여기서 확인할 포인트:
Index Scan using ... ivfflat ...또는 유사한 인덱스 스캔이 뜨는지Rows Removed by Filter가 과도하게 큰지Buffers: shared hit/read가 급증하는지(디스크 읽기 증가)Sort Method가 발생하는지(불필요한 정렬)
만약 Seq Scan이 뜬다면 아래를 우선 점검하세요.
- 벡터 컬럼 타입이
vector(n)으로 올바른지 - 연산자(
<=>,<->,<#>등)와 인덱스 연산자 클래스가 맞는지 enable_seqscan을 잠깐 꺼서(테스트 용도) 인덱스가 가능한 구조인지 확인
SET enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM rag_chunks
ORDER BY embedding <=> $1
LIMIT 10;
테스트에서만 사용하세요. 운영에서 강제하면 다른 쿼리가 더 망가질 수 있습니다.
3) IVFFlat 인덱스 만들기: 거리 함수와 옵션 일치
3-1. 코사인 거리 기준(가장 흔한 RAG)
코사인 거리(또는 코사인 유사도)는 보통 <=>를 사용합니다. 인덱스는 vector_cosine_ops로 맞춥니다.
CREATE INDEX CONCURRENTLY rag_chunks_embedding_ivfflat
ON rag_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 200);
3-2. L2 거리 기준
L2를 쓸 때는 <->와 vector_l2_ops 조합을 사용합니다.
CREATE INDEX CONCURRENTLY rag_chunks_embedding_ivfflat_l2
ON rag_chunks
USING ivfflat (embedding vector_l2_ops)
WITH (lists = 200);
거리/연산자/옵스클래스가 어긋나면 인덱스가 있어도 플래너가 사용하지 않거나, 사용하더라도 효율이 떨어집니다.
4) 핵심 튜닝 1: lists를 데이터 규모에 맞추기
lists는 “클러스터 개수”입니다. 정답 공식은 없지만, 운영에서 안전하게 시작하는 경험칙이 있습니다.
- 수만 건:
lists를 50~200 - 수십만 건:
lists를 200~1000 - 수백만 건 이상:
lists를 1000~5000 이상도 고려
중요한 점:
lists를 바꾸면 인덱스를 다시 만들어야 합니다.- 데이터가 크게 늘면, 과거에 적절했던
lists가 이제는 부족할 수 있습니다.
튜닝 절차 예시(데이터가 100만 행이라고 가정):
- 500, 1000, 2000으로 3개 후보를 잡고
- 동일한 쿼리셋으로 p50/p95 지연과 리콜(정답셋 대비)을 비교
리콜 측정이 어렵다면, 임시로 probes를 크게 올린 결과를 “근사 정답”으로 두고 비교하는 방식도 실무에서 자주 씁니다.
5) 핵심 튜닝 2: probes로 지연-정확도 트레이드오프 제어
probes는 쿼리 시점 파라미터라, 인덱스를 다시 만들지 않고도 실험이 가능합니다.
SET ivfflat.probes = 10;
SELECT id, doc_id
FROM rag_chunks
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 10;
실무 팁:
- 기본값(버전에 따라 다를 수 있음)으로 만족하지 말고, 반드시 튜닝하세요.
probes는 보통 1부터 시작해서 5, 10, 20… 식으로 올리며 측정합니다.- RAG에서 “답이 좀 틀려도 빠른 검색”이 우선이면 낮게, “정확도가 우선”이면 높게.
운영 전략으로는 다음이 현실적입니다.
- 온라인 질의:
probes낮게(예: 5~10) - 백그라운드 재랭킹/오프라인 평가:
probes높게(예: 20~50)
6) 필터가 있는 RAG 쿼리: 느려지는 패턴과 해결법
6-1. 필터 선택도가 낮으면 IVFFlat이 손해
예를 들어 tenant_id 하나가 전체 데이터의 80%를 차지하면, 사실상 필터가 후보를 거의 줄이지 못합니다. 이 경우 IVFFlat이 list를 열어보고도 결국 많은 후보를 만지게 됩니다.
해결 방향은 보통 3가지입니다.
- 파티셔닝: 테넌트/컬렉션 단위로 물리 분리
CREATE TABLE rag_chunks (
tenant_id bigint not null,
id bigserial primary key,
embedding vector(1536) not null,
content text not null
) PARTITION BY LIST (tenant_id);
- 부분 인덱스: 특정 컬렉션에만 집중되는 경우
CREATE INDEX CONCURRENTLY rag_chunks_c1_ivfflat
ON rag_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 200)
WHERE collection_id = 1;
- 사전 후보 축소: 메타 조건으로 먼저 줄일 수 있으면 줄이고, 그 안에서 벡터 검색
단, PostgreSQL 플래너가 항상 의도대로 해주진 않으니 EXPLAIN으로 확인이 필요합니다.
6-2. Top-K 이후 재랭킹(2단계 검색)으로 비용 절감
대부분의 RAG는 “대충 가까운 후보 K개”만 빠르게 뽑고, 그 다음에 더 비싼 계산(크로스 인코더, LLM rerank)을 합니다. PostgreSQL 내부에서도 2단계로 나눌 수 있습니다.
SET ivfflat.probes = 10;
WITH candidates AS (
SELECT id, doc_id, chunk_id,
(embedding <=> $1) AS dist
FROM rag_chunks
WHERE tenant_id = $2
AND collection_id = $3
ORDER BY embedding <=> $1
LIMIT 50
)
SELECT id, doc_id, chunk_id, dist
FROM candidates
ORDER BY dist
LIMIT 10;
이 패턴은 “후보 50개까지는 ANN으로”라는 의도를 명확히 해, 불필요한 추가 작업을 줄이는 데 도움이 됩니다.
7) 인덱스 빌드/재빌드 운영: ANALYZE와 bloat 관리
7-1. 대량 적재 후에는 ANALYZE가 필수
대량으로 임베딩을 넣은 직후 통계가 부정확하면, 플래너가 엉뚱한 계획을 선택합니다.
ANALYZE rag_chunks;
7-2. 업데이트/삭제가 많으면 bloat로 느려질 수 있음
RAG 청크는 보통 append-only에 가깝지만, “재임베딩”, “문서 삭제”, “TTL 정리”가 들어가면 bloat가 쌓입니다. bloat가 의심되면 VACUUM (ANALYZE)와 필요 시 REINDEX를 검토하세요.
VACUUM (ANALYZE) rag_chunks;
REINDEX INDEX CONCURRENTLY rag_chunks_embedding_ivfflat;
TTL로 오래된 벡터를 정리하는 운영 패턴은 메모리/저장소/성능을 동시에 안정화하는 데 유효합니다. 벡터 저장이 계속 누적되는 서비스라면 AutoGPT 메모리 누수? 벡터DB TTL로 해결하기도 운영 아이디어 측면에서 참고할 만합니다.
8) 성능 측정: 지연만 보지 말고 “리콜”도 같이 보자
IVFFlat 튜닝은 결국 “지연 vs 리콜” 최적화입니다. 지연만 줄이면 검색 품질이 무너질 수 있습니다.
실무에서 많이 쓰는 간이 리콜 측정 방법:
- 동일한 질의 집합
Q를 준비 probes를 매우 크게(예: 50~100) 두고 나온 Top-K를 기준 정답으로 간주- 운영 설정(예:
probes=10)의 Top-K와 겹치는 비율을 리콜 근사치로 계산
정확한 평가는 별도 파이프라인이 좋지만, 이 정도만 해도 “너무 빠른 대신 엉뚱한 문서만 가져오는” 상황을 초기에 잡을 수 있습니다.
9) 추천 튜닝 레시피(현실적인 시작점)
아래는 코사인 기준, 다중 테넌트 RAG에서 흔히 먹히는 시작점입니다.
- 인덱스
lists: 200부터 시작(수십만 행 기준)
- 쿼리
SET ivfflat.probes = 10;WHERE tenant_id = ... AND collection_id = ...같이 필터를 명확히- 2단계 후보 추출
LIMIT 50후 최종LIMIT 10
- 유지보수
- 대량 적재 후
ANALYZE - 삭제/재임베딩이 잦으면 주기적
VACUUM (ANALYZE)및 필요 시REINDEX
- 대량 적재 후
- 분기
- 필터 선택도가 낮아 계속 느리면 파티셔닝 또는 컬렉션 단위 분리 검토
- 지연 목표가 빡세면 HNSW로 전환 고려
10) 체크리스트: “IVFFlat인데 왜 이렇게 느리지?”
-
EXPLAIN (ANALYZE, BUFFERS)에서 인덱스 스캔이 실제로 발생하는가 - 거리 연산자(
<=>등)와 인덱스 옵스클래스(vector_cosine_ops등)가 일치하는가 -
lists가 데이터 규모 대비 지나치게 작거나 크지 않은가 -
ivfflat.probes를 측정 기반으로 조정했는가 - 필터 선택도가 낮아 후보가 과도하게 커지지 않는가(파티셔닝/부분 인덱스 검토)
- 대량 적재/변경 후
ANALYZE를 했는가 - 삭제/업데이트 누적으로 bloat가 커지지 않았는가(
VACUUM,REINDEX)
마무리
IVFFlat은 “한 번 만들면 끝”인 인덱스가 아니라, 데이터가 커지고 쿼리 패턴이 바뀔수록 재튜닝이 필요한 ANN 인덱스입니다. 가장 효율적인 접근은 EXPLAIN으로 병목을 확인하고, lists는 인덱스 레벨에서 크게 잡은 뒤 probes로 온라인 트래픽의 지연-정확도 균형을 맞추는 것입니다.
다음 단계로는, 현재 데이터 건수(청크 수), 평균 필터 선택도(테넌트/컬렉션당 청크 수), 목표 p95 지연, 목표 리콜(K=10 기준)을 기준으로 lists 후보군과 probes 범위를 정해 벤치마크를 돌려보는 것을 권합니다.