- Published on
pgvector RAG 검색 느림? HNSW 튜닝 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG 파이프라인에서 체감 지연의 대부분은 retrieval 단계에서 터집니다. 특히 Postgres + pgvector 조합은 운영 친화적이지만, HNSW를 “생성만 해두고” 기본값으로 쓰면 데이터가 커질수록 검색이 급격히 느려지거나, 반대로 정확도가 떨어져 rerank 비용이 폭증하는 형태로 나타납니다.
이 글은 pgvector HNSW에서 검색이 느릴 때 바로 확인할 수 있는 튜닝 체크리스트입니다. 단순히 ef_search만 올리는 식의 처방이 아니라, 인덱스 파라미터, 쿼리 작성, 플래너, 메모리/유지보수까지 한 번에 점검하도록 구성했습니다.
관련해서 HNSW 자체 개념과 튜닝 감각을 더 넓게 잡고 싶다면, 벡터 DB 쪽 실전 튜닝 사례도 함께 참고하세요: RAG 성능 2배 - Qdrant HNSW 튜닝 실전
1) 먼저 “정말 벡터 검색이 병목인지” 확인
RAG가 느릴 때는 벡터 검색이 아니라 아래가 원인인 경우도 많습니다.
- 네트워크 왕복: 앱
pool대기, 커넥션 부족 - 필터 조건으로 인덱스 미사용: 결국 테이블 스캔 후 거리 계산
- TopK는 빨리 나오는데, 이후
JOIN/ORDER BY/LIMIT에서 폭발 - rerank 모델 호출이 느려서 “검색이 느린 것처럼” 보임
체크: EXPLAIN (ANALYZE, BUFFERS)로 진짜 비용 분해
아래처럼 실행 계획을 확인해 인덱스를 타는지, 버퍼 hit/read가 어떤지, 정렬이 발생하는지를 봅니다.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content
FROM documents
ORDER BY embedding <-> $1
LIMIT 20;
Index Scan using ...또는Bitmap Index Scan류가 나오면 일단 방향은 맞습니다.Seq Scan+Sort가 보이면 거의 무조건 쿼리/인덱스/필터 문제입니다.
운영에서 느린 쿼리를 체계적으로 파고드는 방법론은 아래 글의 흐름도 참고할 만합니다: MongoDB 느린 쿼리 - explain으로 원인 찾고 인덱스 튜닝
2) HNSW 인덱스가 “맞는 연산자/거리”로 만들어졌는지
pgvector는 거리/유사도에 따라 연산자가 달라집니다. 인덱스도 그에 맞게 만들어야 플래너가 제대로 사용합니다.
- L2 거리:
embedding <-> query - 내적:
embedding <#> query - 코사인 거리:
embedding <=> query
체크: 인덱스 생성 시 vector_*_ops가 쿼리와 일치?
-- 코사인 거리 기반이라면
CREATE INDEX CONCURRENTLY documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops);
쿼리는 코사인 거리 연산자인 <=>를 써야 합니다.
SELECT id
FROM documents
ORDER BY embedding <=> $1
LIMIT 20;
연산자와 ops가 불일치하면, 인덱스가 있어도 플래너가 못 쓰거나 성능이 급락합니다.
3) HNSW 핵심 파라미터: m, ef_construction, ef_search
HNSW는 “검색 속도 vs 정확도”를 트레이드오프로 조절합니다.
m: 그래프에서 노드가 유지하는 이웃 수(대략). 클수록 정확도↑, 메모리/빌드시간↑ef_construction: 인덱스 빌드 품질. 클수록 정확도↑, 빌드시간↑ef_search: 검색 시 탐색 폭. 클수록 recall↑, 검색시간↑
3-1) 인덱스 생성 파라미터 튜닝
CREATE INDEX CONCURRENTLY documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
권장 출발점(경험치):
- 텍스트 임베딩 RAG(수십만~수백만):
m=16또는m=24,ef_construction=100~400 - 데이터가 작고 정확도 최우선:
ef_construction을 더 올리되, 빌드 시간을 감수
빌드가 너무 오래 걸리면 CONCURRENTLY와 워커/IO 상황을 함께 보세요. 인덱스는 “한 번 잘 만들어두면” 검색 비용을 지속적으로 줄여줍니다.
3-2) 세션/쿼리 단위로 ef_search 조절
ef_search는 검색 시점에 조절하는 레버입니다.
SET LOCAL hnsw.ef_search = 80;
SELECT id, content
FROM documents
ORDER BY embedding <=> $1
LIMIT 20;
실전 패턴:
- 1차 후보군 TopK는
ef_search를 중간값으로: 속도 우선 - recall이 부족해서 rerank가 헛돈을 쓰면
ef_search를 올려 후보군 품질 개선
중요: ef_search를 무작정 올리면 “검색이 느린” 문제를 악화시킵니다. 정확도 부족으로 rerank 비용이 폭증하는지까지 같이 측정해야 합니다.
4) TopK, 후보군 크기, rerank 비용의 균형
RAG는 보통 2단 구조입니다.
- 벡터 검색으로 TopK 후보 추출
- rerank(크로스 인코더 등)로 최종 N개 선택
여기서 흔한 함정:
- 벡터 검색 TopK를 너무 크게 잡아 rerank가 병목이 됨
- 반대로 TopK가 너무 작아 recall이 떨어지고, 답변 품질이 나빠짐
체크리스트
LIMIT 20으로 충분한가,LIMIT 50이 필요한가- rerank 입력 토큰이 과도하지 않은가(문서 chunk 크기)
ef_search를 올려 TopK 품질을 개선하면 TopK를 줄일 수 있는가
RAG 품질/환각 측면에서 rerank 최적화는 아래 글과도 연결됩니다: RAG 환각 줄이기 - ColBERTv2+Rerank 최적화
5) 필터링이 있는 RAG: “필터 먼저”가 성능을 죽인다
멀티테넌트, 권한, 기간, 카테고리 같은 필터가 붙는 순간 벡터 검색이 느려지기 쉽습니다.
문제 패턴:
WHERE tenant_id = ...같은 조건이 선택도가 낮거나 통계가 부정확- 플래너가 HNSW를 포기하고 다른 플랜을 선택
- 또는 인덱스는 타지만, 필터로 인해 후보가 부족해 탐색이 비효율
대응 전략 A: 필터 컬럼에 인덱스 추가(기본)
CREATE INDEX CONCURRENTLY documents_tenant_id_idx
ON documents (tenant_id);
이 자체로 해결되기도 하지만, 벡터 검색과 결합된 플랜에서는 여전히 애매할 수 있습니다.
대응 전략 B: 파티셔닝(테넌트/시간)으로 검색 공간 축소
- 테넌트 단위 파티션
- 시간(월별) 파티션
파티션 프루닝이 잘 되면 “애초에 HNSW가 보는 데이터 수”가 줄어 체감 성능이 크게 개선됩니다.
대응 전략 C: 2단 쿼리로 후보군을 먼저 뽑고 조인
예: 메타데이터 테이블과 조인 때문에 폭발하는 경우, 먼저 벡터 TopK를 뽑고 그 결과에만 조인합니다.
WITH candidates AS (
SELECT id
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 50
)
SELECT d.id, d.content, m.source
FROM candidates c
JOIN documents d ON d.id = c.id
JOIN doc_meta m ON m.doc_id = d.id;
6) 테이블/인덱스 블로트와 VACUUM: “업데이트 많은 RAG”의 복병
임베딩 테이블은 보통 INSERT 위주라 괜찮지만, 다음이 섞이면 급격히 느려질 수 있습니다.
- chunk 재생성으로
DELETE/INSERT가 반복 - 메타데이터 업데이트가 잦음
- autovacuum이 밀려 dead tuple이 누적
체크
pg_stat_user_tables에서n_dead_tup증가EXPLAIN (ANALYZE, BUFFERS)에서 disk read 비중 증가
대응
- autovacuum 파라미터를 테이블 단위로 강화
- 대량 삭제 후
VACUUM (ANALYZE)수행 - 통계가 틀어지면
ANALYZE로 플래너 교정
VACUUM (ANALYZE) documents;
7) 메모리/캐시: HNSW는 “따뜻한 캐시”에서 훨씬 빠르다
HNSW는 인덱스 탐색 과정에서 랜덤 접근이 많아, 캐시 적중률이 성능에 직접적입니다.
체크 포인트
shared_buffers가 너무 작아 인덱스 페이지가 자주 밀려나는가- 디스크가 느린 네트워크 스토리지인지
BUFFERS에서read가 과도하게 발생하는지
대응 방향
- 메모리 증설이 가장 단순하고 효과적
- 인덱스/테이블이 RAM에 어느 정도 상주하도록 워킹셋 관리
- RAG 트래픽 패턴이 특정 테넌트/카테고리에 몰리면 파티셔닝이 다시 유효
8) 동시성: 커넥션 풀과 work_mem이 검색을 망친다
벡터 검색 자체는 빠른데, 동시 요청이 늘면 갑자기 느려지는 경우:
- 앱 커넥션 풀이 작아 대기열이 생김
- 반대로 풀이 너무 커서 DB가 컨텍스트 스위칭/IO로 무너짐
work_mem부족으로 정렬/해시가 디스크로 스필
체크
- DB에서 active connection 수, 대기 이벤트
- 쿼리 플랜에
Sort Method: external merge같은 문구
대응
- 풀 크기를 DB 코어/IO에 맞게 제한
- 필요한 쿼리에만
SET LOCAL work_mem = ...적용
BEGIN;
SET LOCAL work_mem = '128MB';
SET LOCAL hnsw.ef_search = 60;
SELECT id
FROM documents
ORDER BY embedding <=> $1
LIMIT 20;
COMMIT;
9) 운영 체크리스트: 빠르게 진단하는 순서
아래 순서대로 보면 “삽질”을 많이 줄일 수 있습니다.
EXPLAIN (ANALYZE, BUFFERS)로Seq Scan여부 확인- 쿼리 연산자(
<->,<=>,<#>)와 인덱스ops일치 확인 - 필터가 있으면 선택도/인덱스/파티셔닝/2단 쿼리 중 무엇이 필요한지 결정
ef_search를 올리기 전에 TopK와 rerank 비용을 같이 측정m,ef_construction이 데이터 규모 대비 너무 보수적인지 점검(필요시 재빌드)- VACUUM/ANALYZE로 통계/블로트 정리
- 캐시 적중률과 스토리지 성능 확인
- 커넥션 풀/동시성/
work_mem로 인한 스필 여부 확인
10) 실전 예시: “느린 검색”을 재현하고 개선하기
10-1) 기본 테이블과 인덱스
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL
);
CREATE INDEX CONCURRENTLY documents_tenant_id_idx
ON documents (tenant_id);
CREATE INDEX CONCURRENTLY documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
10-2) 검색 쿼리(필터 포함) + 세션 튜닝
BEGIN;
SET LOCAL hnsw.ef_search = 60;
WITH candidates AS (
SELECT id
FROM documents
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 50
)
SELECT d.id, d.content
FROM candidates c
JOIN documents d ON d.id = c.id
LIMIT 20;
COMMIT;
- 후보군
50은 rerank 유무에 따라 조정합니다. - rerank가 있다면
50로 뽑고, 최종 답변에는5~10만 사용 같은 방식이 흔합니다.
마무리: ef_search만 만지지 말고 “플랜+필터+유지보수”를 같이 보자
pgvector HNSW 성능 이슈는 대부분 한 가지 원인으로 고정되지 않습니다. 인덱스는 맞게 만들었는데 필터 때문에 플래너가 포기한다거나, 검색은 빠른데 rerank가 느려서 전체가 느려 보인다거나, autovacuum이 밀려서 갑자기 IO가 튄다거나 하는 식입니다.
그래서 가장 효율적인 접근은 다음입니다.
- 실행 계획으로 인덱스 사용 여부를 먼저 확정
- 연산자/
ops불일치 같은 “구조적 오류”를 제거 - 필터/조인 패턴을 2단 쿼리나 파티셔닝으로 정리
- 그 다음에야
m,ef_construction,ef_search를 조절
원하시면 사용 중인
- Postgres 버전과
pgvector버전 - 테이블 스키마(컬럼, 인덱스)
- 대표 쿼리와
EXPLAIN (ANALYZE, BUFFERS)결과
를 기준으로, 어떤 체크 항목부터 손대는 게 가장 효과적인지 “우선순위” 형태로 같이 정리해드릴게요.