- Published on
pgvector HNSW RAG 튜닝 - ef/IVFFlat로 지연↓
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG에서 체감 지연을 만드는 병목은 대개 LLM이 아니라 벡터 검색 단계입니다. 특히 PostgreSQL + pgvector 조합은 운영 편의성이 뛰어난 대신, 기본 설정으로는 검색 지연이 쉽게 튀고(콜드 캐시, VACUUM/통계, 인덱스 파라미터 미스매치), 리콜이 부족하면 답변 품질도 흔들립니다.
이 글은 pgvector의 대표 인덱스인 HNSW와 IVFFlat을 RAG에 맞게 튜닝해 지연을 낮추면서도 리콜을 유지하는 방법을 정리합니다. 핵심은 다음 3가지를 측정 기반으로 맞추는 것입니다.
- HNSW:
ef_search,ef_construction,m - IVFFlat:
lists,probes - RAG 파이프라인: 후보 수
k와 리랭커(또는 cross-encoder) 전략
관련해서 HNSW 튜닝 감을 잡는 데는 Qdrant 사례도 도움이 됩니다. 개념은 유사하고(그래프 탐색 폭과 리콜/지연 트레이드오프), 측정 방법도 비슷합니다: RAG 검색품질 2배 - Qdrant HNSW 튜닝 실전
전제: RAG에서 “빠른 검색”의 정의
RAG 검색은 보통 다음 목표를 동시에 만족해야 합니다.
- P95/P99 지연을 낮춘다 (사용자 체감)
- 리콜을 유지한다 (정답 문서가 후보에 들어와야 함)
- 안정성을 확보한다 (데이터 증가, 업데이트, VACUUM 이후에도 성능이 크게 흔들리지 않음)
여기서 리콜은 보통 오프라인 평가로 잡습니다.
- 골든 쿼리 세트(질문, 정답 문서 id)
- 지표: Recall@k, MRR, nDCG
- 온라인 지표: 클릭/정답률/후속 질문률
그리고 “지연”은 DB만 재는 게 아니라 아래를 분리해서 봐야 합니다.
- 임베딩 생성 시간
- DB 쿼리 시간(네트워크 포함)
- 리랭킹 시간
- LLM 생성 시간
이 글은 DB 검색 구간을 중심으로 다룹니다.
pgvector 인덱스 선택: HNSW vs IVFFlat
pgvector는 크게 두 계열의 근사 최근접 탐색(ANN) 인덱스를 제공합니다.
HNSW가 유리한 경우
- 높은 리콜이 필요하고,
k가 작지 않더라도 안정적으로 가져오고 싶다 - 쿼리당 지연을 낮추되, 인덱스 메모리 사용량을 감수할 수 있다
- 삽입/업데이트가 잦지 않거나(또는 배치), 인덱스 빌드 시간이 길어도 된다
HNSW는 그래프 기반 탐색이라 ef_search를 올리면 리콜이 좋아지지만 CPU와 지연이 증가합니다.
IVFFlat이 유리한 경우
- 데이터가 매우 크고, 메모리 예산이 타이트하다
- 쿼리 패턴이 일정하고,
lists/probes로 성능을 예측 가능하게 만들고 싶다 - 배치 인덱싱(리빌드)이 가능한 환경이다
IVFFlat은 클러스터(lists)를 나누고 일부(probes)만 탐색합니다. probes를 낮추면 빨라지지만 리콜이 떨어질 수 있습니다.
스키마/쿼리 기본기: 튜닝 전에 확인할 것
1) 벡터 차원과 연산자 정합성
임베딩 모델 차원이 768인데 컬럼이 1536이면 삽입이 안 되거나, 캐스팅 비용이 발생할 수 있습니다.
또한 거리 연산자 선택이 인덱스와 맞아야 합니다.
- 코사인 유사도: 보통
vector_cosine_ops - L2 거리:
vector_l2_ops - 내적:
vector_ip_ops
2) 필터가 있으면 “벡터만” 빠르게 해도 느릴 수 있음
RAG는 보통 멀티테넌트, 문서 타입, 권한, 시간 범위 같은 필터가 붙습니다. 이때는 다음을 고려해야 합니다.
- 필터 컬럼에 B-Tree 인덱스
- 파티셔닝(테넌트 단위)
- “필터 후 벡터” vs “벡터 후 필터”의 실행계획 확인
3) 테이블 bloat/통계 문제
삭제/업데이트가 많으면 테이블/인덱스 bloat로 인해 랜덤 IO가 늘고 지연이 튈 수 있습니다. 특히 RAG에서 문서 재수집/재임베딩이 잦으면 자주 발생합니다.
VACUUM이 기대만큼 효과가 없거나 autovacuum이 밀린다면 아래 글이 도움이 됩니다: PostgreSQL VACUUM 안먹을 때 - bloat 원인·해결
HNSW 튜닝: ef_search가 지연을 결정한다
HNSW는 크게 다음 파라미터를 봅니다.
m: 그래프 연결 수(대략). 높을수록 리콜이 좋아지지만 인덱스가 커지고 빌드/삽입 비용이 증가ef_construction: 인덱스 빌드 품질. 높을수록 리콜이 좋아질 수 있으나 빌드 시간이 증가ef_search: 쿼리 시 탐색 폭. 지연과 리콜을 가장 직접적으로 좌우
HNSW 인덱스 생성 예시
아래 예시는 코사인 기준입니다.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE rag_chunks (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
doc_id bigint NOT NULL,
chunk_no int NOT NULL,
content text NOT NULL,
embedding vector(768) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 필터용 인덱스(멀티테넌트/문서 단위)
CREATE INDEX rag_chunks_tenant_doc_idx ON rag_chunks (tenant_id, doc_id);
-- HNSW 인덱스
CREATE INDEX rag_chunks_embedding_hnsw
ON rag_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
쿼리 시 ef_search 조절
pgvector는 SET LOCAL로 세션 단위로 조절할 수 있어, API 요청 단위로 튜닝하기 좋습니다.
BEGIN;
SET LOCAL hnsw.ef_search = 40;
SELECT id, doc_id, chunk_no, content
FROM rag_chunks
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;
COMMIT;
여기서 $2는 쿼리 임베딩 벡터입니다. 본문에 부등호가 들어가면 MDX에서 오인될 수 있어, 위 쿼리는 코드 블록 안에서만 사용해야 합니다.
실전 가이드: ef_search는 “k의 2~10배”에서 시작
경험적으로 다음처럼 시작하면 튜닝 시간이 줄어듭니다.
k = 10이면ef_search = 40부터 시작- 리콜이 부족하면
80,120으로 올려본다 - 지연이 목표를 넘으면
k를 줄이거나(후단 리랭킹으로 보완),ef_search를 낮춘다
중요한 점은 RAG에서 최종적으로 필요한 건 “진짜 top-1”이 아니라 리랭커가 정답을 고를 수 있을 만큼의 후보라는 것입니다. 그래서 HNSW에서 ef_search를 무작정 올리기보다, 아래처럼 파이프라인을 나누는 게 더 효율적입니다.
- 1차 검색: 빠르게 후보 50개
- 2차 리랭킹: 50개 중 상위 5~10개 선택
이 구조에서는 HNSW의 ef_search를 “리랭커가 먹을 후보 수”에 맞춰 최소화할 수 있습니다.
m과 ef_construction은 자주 안 바꾸되, 너무 낮게 시작하지 말 것
m = 16은 무난한 시작점- 리콜이 계속 낮고
ef_search를 올려도 개선이 미미하면m = 24또는32를 고려 ef_construction은 64~256 범위에서 시작
단, m을 올리면 인덱스 메모리 사용량이 증가하고, 빌드 시간이 늘어납니다. 운영 환경에서는 인덱스 빌드/리빌드 시간도 SLO에 포함시키는 게 좋습니다.
IVFFlat 튜닝: lists/probes로 “탐색 범위”를 제어한다
IVFFlat은 다음 파라미터가 핵심입니다.
lists: 클러스터 개수(인덱스 생성 시 고정)probes: 쿼리 시 탐색할 클러스터 수(동적으로 조절 가능)
IVFFlat 인덱스 생성 예시
CREATE INDEX rag_chunks_embedding_ivfflat
ON rag_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 2000);
-- 통계/플래너를 위해 권장
ANALYZE rag_chunks;
lists는 데이터 크기에 따라 달라집니다. 흔히 다음을 старт 포인트로 둡니다.
- 수십만 행:
lists100~1000 - 수백만 행:
lists1000~5000 - 수천만 행:
lists5000 이상
정답은 없고, 결국 probes와 세트로 측정해야 합니다.
쿼리 시 probes 조절
BEGIN;
SET LOCAL ivfflat.probes = 10;
SELECT id, doc_id, chunk_no, content
FROM rag_chunks
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;
COMMIT;
probes를 낮추면 빨라지지만 리콜이 떨어질 수 있음probes를 올리면 리콜이 좋아지지만 지연이 증가
실전에서는 probes = 5 또는 10부터 시작해서, 리콜이 부족하면 20, 50으로 올려봅니다.
IVFFlat의 함정: 데이터 분포가 바뀌면 성능이 흔들린다
IVFFlat은 클러스터링 기반이라, 데이터가 크게 누적되거나 분포가 변하면 초기 클러스터가 최적이 아닐 수 있습니다. 이때는 다음을 고려합니다.
- 주기적 리인덱싱(배치 윈도우 확보)
- 테넌트 단위 파티셔닝으로 분포 변화 완화
- 핫 테넌트는 HNSW로 분리(하이브리드 전략)
“지연↓”의 핵심: 후보 수 설계와 리랭킹
DB 검색만 빠르게 해서는 RAG 전체가 빨라지지 않습니다. 하지만 DB에서 필요 이상으로 높은 리콜을 직접 달성하려고 ef_search 또는 probes를 과하게 올리면, 비용이 급격히 증가합니다.
권장 접근은 아래입니다.
- 1차 ANN에서 후보를 넉넉히 가져온다(예: 30~100)
- 2차 리랭킹으로 상위 5~10을 고른다
- LLM에는 상위 N개만 컨텍스트로 넣는다
이 구조에서 “1차 후보 수”는 다음과 같이 튜닝합니다.
- 리랭커가 없으면:
k를 늘려야 하므로 ANN 파라미터를 더 공격적으로 올려야 함 - 리랭커가 있으면: ANN은 적당한 리콜만 확보하고, 리랭커로 정밀도를 올림
측정 방법: EXPLAIN (ANALYZE, BUFFERS)로 병목을 분해
튜닝은 감이 아니라 숫자로 해야 합니다.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM rag_chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
여기서 확인할 포인트:
- 인덱스를 타는지(Seq Scan이면 거의 실패)
Buffers에서 shared hit/read 비율(디스크 read가 많으면 콜드 캐시 영향)- 필터가 인덱스 탐색을 방해하는지(조건 푸시다운이 되는지)
추가로 운영에서는 다음도 함께 수집하는 게 좋습니다.
- P50/P95/P99 쿼리 시간(테넌트별)
- 쿼리당 반환 후보 수, 리랭킹 소요
- autovacuum 지연, dead tuple 비율(테이블/인덱스 bloat 징후)
추천 튜닝 플로우(체크리스트)
1) 목표를 먼저 정한다
- 예: 검색 쿼리 P95 80ms 이하, Recall@10 0.9 이상
2) HNSW 또는 IVFFlat을 선택한다
- 리콜 우선/안정성 우선이면 HNSW
- 메모리/대용량 우선이면 IVFFlat
3) 초기값으로 벤치마크한다
- HNSW:
m = 16,ef_construction = 128,ef_search = 40 - IVFFlat:
lists = 2000,probes = 10
4) 리콜이 부족하면 “쿼리 파라미터”부터 올린다
- HNSW:
ef_search상향 - IVFFlat:
probes상향
인덱스 재생성이 필요한 파라미터(m, ef_construction, lists)는 마지막에 만지는 게 비용이 적습니다.
5) 지연이 높으면 다음 순서로 줄인다
- 후보
k를 줄이고 리랭킹 도입/강화 - 필터 인덱스/파티셔닝으로 검색 범위 축소
- HNSW는
ef_search하향, IVFFlat은probes하향 - 캐시/커넥션 풀/워크메모리 등 DB 레벨 튜닝
운영 팁: 멀티테넌트 RAG에서 흔한 패턴
테넌트별 데이터 크기 편차가 큰 경우
- 작은 테넌트는 어떤 설정이든 빠르지만, 큰 테넌트가 P99를 망칩니다.
- 해결: 큰 테넌트를 별도 파티션/테이블로 분리하고 인덱스 파라미터를 다르게 운영
업데이트가 잦은 경우
- HNSW는 삽입이 가능하지만, 대량 업데이트/삭제가 반복되면 bloat/성능 변동이 생길 수 있습니다.
- 해결: 배치로 재임베딩 후 교체(새 테이블에 적재 후 스왑), VACUUM/REINDEX 전략 수립
결론: “ef/probes를 올리면 된다”가 아니라, 파이프라인을 설계해야 한다
pgvector RAG 튜닝에서 지연을 낮추는 가장 흔한 실수는, ANN 인덱스 파라미터를 올려서 “DB에서만 정답 top-k를 완벽히 맞추려는 것”입니다. RAG는 애초에 2단계 구조(후보 생성 + 정밀 선택)가 잘 맞습니다.
- HNSW는
ef_search가 지연을 좌우하므로, 리랭커 기준 후보 수에 맞춰 최소화 - IVFFlat은
lists/probes를 세트로 보고, 분포 변화에 대비해 리인덱싱 전략 포함 - 필터/테넌트/업데이트 패턴까지 포함해 측정하고,
EXPLAIN (ANALYZE, BUFFERS)로 병목을 분해
이 과정을 거치면 “검색 지연↓”과 “검색 품질 유지”를 동시에 달성할 확률이 크게 올라갑니다.