Published on

RAG 검색 품질 폭락? pgvector HNSW 튜닝 9단계

Authors

RAG에서 검색 품질이 폭락하는 순간은 대개 비슷합니다. 배포 전에는 잘 맞던 답이 갑자기 엉뚱해지고, 상위 문서가 바뀌거나, 같은 질문을 반복해도 결과가 들쭉날쭉해집니다. 이때 원인이 임베딩 모델 변경이나 전처리 버그일 수도 있지만, 의외로 pgvector의 HNSW 인덱스 튜닝 부재가 방아쇠가 되는 경우가 많습니다.

HNSW는 근사 최근접 탐색(ANN)이라 빠른 대신, 정확도(Recall)와 지연시간(Latency)의 균형을 파라미터로 맞춰야 합니다. 데이터가 커지거나 분포가 달라지면 기존 세팅이 더 이상 맞지 않아서 “품질 폭락”처럼 보이는 현상이 발생합니다.

이 글은 pgvector HNSW를 기준으로, RAG 검색 품질을 되살리는 9단계 튜닝 루틴을 제공합니다. (후반부에서 RAG 환각과 리랭크까지 엮고 싶다면 RAG 환각 줄이기 - HyDE+Rerank 실전 튜닝도 함께 보는 것을 권합니다.)

0. 전제: “검색 품질”을 수치로 고정하기

튜닝 전에 반드시 해야 할 일은 측정 기준 고정입니다. RAG에서 검색 품질은 보통 아래로 봅니다.

  • Recall@k: 정답 문서가 상위 k에 포함되는 비율
  • MRR@k: 정답이 위로 올수록 점수가 높아지는 랭킹 지표
  • nDCG@k: 다중 정답, graded relevance가 있을 때 유리
  • 운영 지표: p95 latency, DB CPU, 쿼리 타임아웃 비율

가장 흔한 실패는 “좋아진 것 같은데요?”로 끝나는 것입니다. 최소한 **골든 쿼리 세트(예: 200~1000개)**를 만들고, 튜닝 전후를 동일 조건으로 비교하세요.

1단계: 임베딩 차원, 정규화, 거리함수부터 확인

HNSW를 아무리 만져도, 벡터 자체가 어긋나면 답이 없습니다. 아래를 먼저 점검합니다.

  • 임베딩 모델이 바뀌었는지(차원 dim, 토크나이저, pooling)
  • 벡터 정규화 여부(특히 코사인 유사도)
  • 거리함수 선택이 데이터와 맞는지

pgvector에서 흔한 조합은 아래 중 하나입니다.

  • 코사인 유사도: vector_cosine_ops
  • 내적: vector_ip_ops (정규화된 벡터면 코사인과 유사)
  • L2: vector_l2_ops

정규화를 클라이언트에서 한다면, 파이프라인에 “정규화 누락”이 섞이지 않도록 강제하세요.

-- 예시: 컬럼 차원 확인은 애플리케이션/마이그레이션에서 관리
-- 거리 연산자 예시(코사인):
SELECT id, 1 - (embedding <=> $1) AS cosine_sim
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;

주의: pgvector에서 연산자 의미는 버전에 따라 혼동하기 쉬우니, 사용 중인 연산자와 ops class가 일치하는지 문서/DDL을 다시 확인하세요.

2단계: HNSW가 실제로 타는지 EXPLAIN으로 확인

“품질 폭락”처럼 보이지만 사실은 인덱스가 안 타서 정렬이 느려지고 상위 LIMIT만 겨우 반환되는 케이스가 있습니다. 또는 플래너가 seq scan을 선택하는 경우도 있습니다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM documents
ORDER BY embedding <=> $1
LIMIT 20;

여기서 확인할 것:

  • Index Scan using ... hnsw 형태로 나오는지
  • LIMIT이 작아도 정렬 비용이 커서 Sort가 뜨지 않는지
  • Buffers가 과도하게 증가하지 않는지

인덱스가 안 탄다면, 아래를 우선 점검하세요.

  • 쿼리 형태가 인덱스 사용 가능한 패턴인지(ORDER BY + LIMIT)
  • WHERE 조건이 너무 복잡해서 플래너가 포기하지는 않는지
  • 통계가 낡았는지(ANALYZE)

3단계: HNSW 핵심 파라미터 m, ef_construction 재설계

HNSW 품질은 크게 두 축입니다.

  • m: 노드당 연결 수(그래프 밀도)
  • ef_construction: 인덱스 빌드 시 탐색 폭(정확도/시간)

일반적인 경향:

  • m 증가: 메모리 증가, 빌드 느려짐, 검색 recall 상승 경향
  • ef_construction 증가: 빌드 느려짐, recall 상승 경향

데이터가 커질수록(문서 수 증가), 기존 m이 너무 작으면 그래프가 성기게 형성되어 근사 탐색이 쉽게 빗나갑니다. “어제까지 되던 게 오늘 안 됨”은 사실상 데이터 규모 변화에 대한 임계점 통과인 경우가 많습니다.

인덱스 생성 예시:

CREATE INDEX CONCURRENTLY documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

권장 출발점(경험칙):

  • 소규모(수만~수십만): m=16, ef_construction=100~200
  • 중대규모(수백만): m=24~32, ef_construction=200~400

단, 메모리와 빌드 시간을 같이 보면서 올려야 합니다.

4단계: 런타임 ef_search로 Recall을 즉시 끌어올리기

운영 중 “품질이 급락”했다면, 가장 빠른 응급처치는 런타임 탐색 폭인 ef_search를 올리는 것입니다. 인덱스를 다시 만들지 않아도, recall이 즉시 개선되는 경우가 많습니다.

-- 세션 단위로 적용(예시)
SET hnsw.ef_search = 80;

SELECT id
FROM documents
ORDER BY embedding <=> $1
LIMIT 20;

경향:

  • ef_search 증가: recall 상승, latency 증가

운영에서는 보통 쿼리 타입별로 ef_search를 다르게 가져갑니다.

  • 탐색형 질문(정답 다양): ef_search 높게
  • 네비게이션/정확 질의: ef_search 낮게

애플리케이션에서 커넥션 풀을 쓴다면, 세션 파라미터가 섞이지 않도록 요청 스코프에서 설정하거나 트랜잭션 단위로 강제하세요.

5단계: 후보군 k와 RAG 파이프라인의 “후단 리랭크” 정렬

많은 팀이 topK=5 같은 작은 값으로 시작합니다. 데이터가 커지고 중복 문서가 늘면, ANN 특성상 상위 몇 개가 흔들리기 쉬워서 “정답이 6~20위에 있는데 못 가져오는” 문제가 생깁니다.

권장 패턴:

  • 벡터 검색에서 k_candidate를 충분히 크게(예: 50~200)
  • 그 다음 단계에서 BM25, 크로스 인코더, 규칙 기반 리랭크로 k_final을 줄이기

SQL 예시(후단 리랭크는 애플리케이션에서 수행한다고 가정):

-- 후보군을 넉넉히 가져오기
SELECT id, chunk, embedding <=> $1 AS distance
FROM document_chunks
ORDER BY embedding <=> $1
LIMIT 100;

리랭크/HyDE를 함께 쓰는 전략은 위 내부 링크 글에 상세히 정리되어 있습니다.

6단계: 필터(테넌트, 권한, 날짜)와 HNSW의 상호작용 다루기

RAG 운영에서 가장 흔한 현실은 “벡터 검색 + 필터”입니다.

  • 테넌트 필터: WHERE tenant_id = ...
  • ACL 필터: WHERE doc_id IN (...) 또는 조인
  • 최신성: WHERE created_at >= ...

문제는 HNSW가 기본적으로 “전체 공간”에서 근사 탐색을 하므로, 강한 필터가 붙으면 유효 후보가 급감해 recall이 떨어지거나 latency가 튈 수 있습니다.

대응 전략:

  1. 테넌트 단위로 인덱스를 분리(테이블 파티셔닝 포함)
  2. 필터 카디널리티가 높으면, 먼저 필터로 후보를 줄이고 그 안에서 정확 검색(하지만 비용 큼)
  3. “최근 N일” 같은 조건은 별도 최신 인덱스/테이블로 분리

예시: 테넌트별 파티셔닝(개념 예시)

-- 파티셔닝은 스키마 설계에 따라 달라짐
-- 핵심은 필터가 인덱스 효율을 망가뜨리지 않게 데이터 물리 구조를 맞추는 것

7단계: 업데이트/삭제가 많은 워크로드면 재색인 전략을 세우기

HNSW는 동적 삽입을 지원하지만, 업데이트/삭제가 누적되면 그래프 품질이 서서히 나빠질 수 있습니다. “서서히 품질이 내려가다가 어느 순간 확 떨어짐” 패턴이 여기에 해당합니다.

체크리스트:

  • 벡터 업데이트가 잦은가(문서 재임베딩, chunk 재생성)
  • 삭제가 많은가(만료 문서)
  • 인덱스를 만든 지 오래됐는가

대응:

  • 주기적 REINDEX 또는 신규 인덱스 CONCURRENTLY 생성 후 스왑
  • 대량 배치 삽입 후 인덱스 생성(가능하면)
-- 운영 영향 줄이기: 신규 인덱스를 concurrent로 만들고 교체
CREATE INDEX CONCURRENTLY document_chunks_embedding_hnsw_v2
ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 24, ef_construction = 300);

-- 검증 후 기존 인덱스 드롭
DROP INDEX CONCURRENTLY document_chunks_embedding_hnsw;
ALTER INDEX document_chunks_embedding_hnsw_v2 RENAME TO document_chunks_embedding_hnsw;

8단계: 메모리/병렬성/진공(VACUUM)로 “성능 흔들림” 제거

품질이 떨어져 보이지만, 실제로는 latency가 튀면서 애플리케이션이 타임아웃/조기 종료를 일으켜 결과가 빈약해지는 경우가 있습니다. 특히 p95가 튀면 RAG는 “검색이 실패한 것처럼” 보입니다.

점검 포인트:

  • work_mem이 너무 작아 정렬/연산이 디스크로 떨어지는지
  • autovacuum이 밀려서 bloat가 커졌는지
  • CPU 스로틀링, IO 병목이 있는지

운영에서 자주 보는 증상은 쿠버네티스 환경에서의 리소스 제한으로 인한 급격한 성능 저하입니다. DB 또는 벡터 검색 서비스가 OOMKilled 또는 재시작을 반복하면 품질이 아니라 가용성 문제입니다. 이 경우는 Kubernetes CrashLoopBackOff 12가지 원인·해결 체크리스트처럼 인프라 레벨부터 안정화하세요.

9단계: 회귀 방지용 “튜닝 실험 템플릿” 만들기

한 번 튜닝에 성공해도, 데이터가 바뀌면 다시 흔들립니다. 그래서 다음을 자동화하는 게 최종 단계입니다.

  • 골든 쿼리 세트 버전관리
  • 튜닝 후보 조합(m, ef_construction, ef_search, k_candidate) 그리드 탐색
  • 결과 리포트(Recall@k, MRR@k, p95 latency)
  • 변경 감지 알림(임베딩 모델/전처리/인덱스 설정)

간단한 실험 테이블 예시:

CREATE TABLE rag_hnsw_experiments (
  id bigserial PRIMARY KEY,
  created_at timestamptz DEFAULT now(),
  m int NOT NULL,
  ef_construction int NOT NULL,
  ef_search int NOT NULL,
  k_candidate int NOT NULL,
  recall_at_20 double precision,
  mrr_at_20 double precision,
  p95_latency_ms double precision,
  notes text
);

애플리케이션 측에서는 실험 실행 시 세션에 SET hnsw.ef_search = ...를 주고 동일한 쿼리 세트를 돌린 뒤 결과를 적재하면 됩니다.

자주 겪는 “품질 폭락” 원인 요약

  • 데이터가 커졌는데 m이 작아 그래프가 성기다
  • ef_search가 너무 낮아 근사 탐색이 빗나간다
  • 필터가 강해져 유효 후보가 줄었다(테넌트/ACL/최신성)
  • 업데이트/삭제 누적으로 인덱스 품질이 저하됐다
  • 인덱스가 안 타거나, latency 튐으로 검색이 사실상 실패한다

결론: HNSW는 “한 번 만들고 끝”이 아니다

pgvector HNSW는 RAG의 성능과 품질을 동시에 잡을 수 있는 강력한 선택이지만, 데이터가 성장하면 반드시 재튜닝이 필요합니다. 실무적으로는

  • 응급 처치: ef_search 상향
  • 근본 처방: m, ef_construction 재설계 및 재색인
  • 장기 처방: 필터/파티셔닝 설계 + 리랭크 파이프라인 + 회귀 테스트 자동화

이 3단계로 가져가면 “갑자기 RAG가 멍청해짐” 같은 사건을 대부분 예방할 수 있습니다.