- Published on
RAG 검색 품질 급락? Qdrant HNSW 튜닝 7단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG(검색 증강 생성)에서 답변 품질이 갑자기 나빠졌다면, LLM 프롬프트보다 먼저 검색 단계의 리콜(recall)과 노이즈를 의심해야 합니다. 특히 Qdrant 같은 벡터 DB에서 기본 인덱스인 HNSW는 파라미터 조합과 운영 환경 변화(데이터 증가, 필터 추가, 샤딩, 메모리 압박)에 따라 탐색 품질이 급락할 수 있습니다.
이 글은 “왜 갑자기 검색이 안 맞지?”라는 상황에서, 감으로 ef 값을 올려보는 수준을 넘어 측정 가능한 지표 기반으로 Qdrant HNSW를 튜닝하는 7단계를 제시합니다.
중간중간 인프라 이슈(타임아웃, 5xx)로 검색 결과가 누락되어 품질이 떨어지는 경우도 있으니, 운영 중이라면 KServe+Istio에서 모델 503 해결 7단계나 EKS ALB Ingress 504(5xx) 간헐 발생 원인·해결 같은 네트워크 레이어 점검도 병행하는 것을 권장합니다.
0. 전제: “품질 급락”을 수치로 정의하기
HNSW 튜닝은 정답이 하나인 최적화가 아니라, 비용(지연/CPU/RAM)과 품질(리콜/정확도)의 트레이드오프입니다. 그래서 먼저 “급락”을 다음처럼 수치로 잡아야 합니다.
- Recall@k: 정답 문서(또는 정답 chunk)가 Top-k에 포함되는 비율
- MRR 또는 nDCG: 순위 품질(정답이 위로 올라오는지)
- Latency p95/p99: 검색 지연
- Timeout / error rate: 오류로 인한 결과 누락(품질처럼 보임)
RAG라면 아래 두 가지를 분리 측정하는 게 핵심입니다.
- 검색 품질: Top-k에 “정답 근처”가 들어오나?
- 생성 품질: 들어온 컨텍스트를 LLM이 잘 활용하나?
이번 글은 1)을 올리는 데 집중합니다.
1단계: 데이터/임베딩 변화부터 의심하기(인덱스보다 먼저)
검색 품질 급락의 원인은 의외로 HNSW가 아니라 아래 변화인 경우가 많습니다.
- 임베딩 모델 교체(차원 변경, 도메인 성능 차이)
- chunking 정책 변경(문장 단위
->문단 단위, overlap 변경 등) - 정규화 방식 변경(코사인 유사도에서 벡터 정규화 누락 등)
- 메타데이터 필터 조건 추가(필터가 강해지면 후보군 급감)
체크리스트:
- 컬렉션
distance가 현재 임베딩과 맞는지(예: Cosine 사용 시 벡터 정규화) - 차원 수가 동일한지(차원 불일치가 있으면 보통 삽입 단계에서 터지지만, 혼재하면 더 큰 문제)
- 동일 질의에 대해 “예전 임베딩”과 “현재 임베딩”의 최근접 이웃이 얼마나 달라졌는지
Qdrant 컬렉션 설정 예시(개념용):
{
"vectors": {
"size": 768,
"distance": "Cosine"
}
}
Cosine을 쓰면서 벡터 정규화를 애플리케이션에서 하지 않았다면, 분포가 흔들리며 유사도 순위가 망가질 수 있습니다.
2단계: “정확한 기준” 만들기(브루트포스/정답셋)
HNSW는 근사 탐색이라, 튜닝하려면 기준이 필요합니다. 가장 좋은 방법은:
- (가능하면) 소규모 샘플에 대해 브루트포스(정확 탐색) 결과를 만들고
- HNSW 결과와 비교해 Recall@k를 계산하는 것입니다.
Qdrant는 설정에 따라 정확 탐색(인덱스 사용 안 함)을 강제할 수 있습니다. 버전/기능에 따라 옵션이 다를 수 있으니, 핵심은 “HNSW를 끄고” 기준을 만들거나, 별도 파이프라인에서 코사인/내적을 직접 계산해 기준을 만드는 것입니다.
파이썬으로 간단히 Recall@k를 계산하는 예시(개념 코드):
import numpy as np
def recall_at_k(gt_ids, pred_ids, k=10):
gt = set(gt_ids)
pred = pred_ids[:k]
hit = any(x in gt for x in pred)
return 1.0 if hit else 0.0
# 여러 쿼리 평균
scores = [recall_at_k(q.gt, q.pred, k=10) for q in queries]
print("Recall@10:", float(np.mean(scores)))
이 기준이 있어야, 뒤 단계에서 ef_search를 올렸을 때 “품질이 올라갔다”를 확신할 수 있습니다.
3단계: HNSW 핵심 파라미터 이해하기(m, ef_construct, ef_search)
Qdrant의 HNSW 튜닝은 결국 아래 3개를 다루는 일입니다.
m: 노드당 연결(edge) 수. 클수록 그래프가 촘촘해져 리콜이 오르기 쉽지만, RAM과 빌드 비용이 증가합니다.ef_construct: 인덱스 빌드 시 탐색 폭. 클수록 **인덱스 품질(리콜)**이 올라가지만, 빌드 시간이 증가합니다.ef_search: 검색 시 탐색 폭. 클수록 리콜이 올라가지만, 지연과 CPU가 증가합니다.
실무에서 자주 보는 패턴:
- 데이터가 커졌는데
ef_search를 그대로 두면, 동일 지연 내에서 탐색이 상대적으로 얕아져 리콜이 떨어집니다. m이 너무 작으면ef_search를 올려도 한계가 빨리 옵니다.ef_construct가 너무 낮으면, 아무리ef_search를 올려도 “좋은 길” 자체가 부족합니다.
4단계: 먼저 ef_search로 “응급 처치”하고 상한을 찾기
품질 급락 상황에서는 인덱스를 다시 빌드하기 전에, 운영 중에도 조정 가능한 ef_search부터 만져 즉시 리콜을 회복할 수 있는지 확인합니다.
권장 접근:
- 대표 쿼리 50~200개 선정
ef_search를 단계적으로 올리며 Recall@k와 p95 지연을 같이 기록- 리콜이 더 이상 오르지 않는 지점(포화)을 찾기
예시(개념):
ef_search: 64, 128, 256, 512- 관찰: Recall@10이 0.72
->0.84->0.88->0.885 - 지연: p95가 40ms
->65ms->120ms->220ms
이 경우 ef_search=256이 타협점일 수 있습니다.
주의할 점:
- 필터가 강하게 걸리는 쿼리는
ef_search를 올려도 효과가 제한적일 수 있습니다. - 타임아웃이 걸리면 결과가 “부분적으로”만 반환되어 품질이 더 떨어져 보일 수 있습니다. ALB나 Ingress 타임아웃 이슈라면 EKS ALB Ingress 504(60초) idle_timeout 해결도 함께 확인하세요.
5단계: 포화가 빨리 오면 m을 올려 “그래프 자체”를 개선
ef_search를 충분히 올렸는데도 Recall@k가 목표치에 못 미치고 포화된다면, 그래프 연결성이 부족할 가능성이 큽니다. 이때는 m을 올리는 방향을 검토합니다.
m을 올리면 인덱스 메모리 사용량이 증가합니다.- 데이터가 수백만 포인트 이상이면,
m의 작은 차이가 RAM에 큰 영향을 줍니다.
실전 팁:
m을 무작정 크게 하기보다, 현재m에서+4또는+8수준으로 올려 A/B 측정- RAM 여유가 없다면,
m을 올리는 대신 샤딩/리소스 확장/압축(양자화 등)을 먼저 고려
Qdrant 컬렉션 HNSW 설정 예시(개념 JSON):
{
"hnsw_config": {
"m": 32,
"ef_construct": 256
}
}
중요: m과 ef_construct 변경은 보통 리인덱싱 또는 컬렉션 재구성이 필요합니다. 운영 중에는 새 컬렉션을 만들고 듀얼 라이트 후 스위칭하는 방식이 안전합니다.
6단계: ef_construct로 “인덱스 빌드 품질”을 끌어올리기
데이터가 커질수록 인덱스 빌드 품질(ef_construct)이 중요해집니다. 특히 아래 증상이 있으면 ef_construct가 낮을 가능성이 있습니다.
ef_search를 높여도 리콜이 잘 안 오름- 특정 구간(최근 적재된 데이터)만 유독 검색이 약함
- 인덱스 빌드/머지 이후 품질이 출렁임
권장 접근:
ef_construct를 올린 새 컬렉션을 만들고- 동일한
m에서ef_construct만 바꿔 리콜 변화를 확인
예시(개념):
m=24고정ef_construct: 128->256->512
빌드 시간은 늘지만, “검색 시 비용”인 ef_search를 덜 올려도 목표 리콜에 도달하는 경우가 많아, 장기적으로는 더 싸질 수 있습니다.
7단계: 운영 변수(필터, 샤딩, 리소스, 스냅샷/컴팩션)를 함께 튜닝
HNSW 파라미터만 맞춰도 해결되지 않는 품질 급락은 보통 운영 변수에서 터집니다.
7-1. 필터가 리콜을 깎는 방식 이해하기
RAG에서는 tenant_id, doc_type, lang, published_at 같은 필터가 흔합니다. 필터가 강해지면 후보군이 줄어들어, 근사 탐색의 “좋은 이웃”을 찾기 어려워집니다.
대응 전략:
- 필터를 1차로 강하게 걸기보다, 가능한 경우 “약한 필터 + 재랭킹”으로 전환
k를 늘리고(예: Top-10->Top-50), 애플리케이션에서 교차 인코더 재랭킹 적용- 필터 조합이 너무 다양하면, 페이로드 인덱싱 전략을 점검(필터 성능이 느리면 타임아웃으로 품질이 떨어져 보임)
7-2. 샤딩/레플리카가 검색 품질처럼 보이는 장애를 만든다
샤드가 많아질수록, 각 샤드에서 Top-k를 뽑고 합치는 과정에서 전역 Top-k 근사 오차가 커질 수 있습니다(구현/설정에 따라 다름). 또한 레플리카 불일치나 일시적 오류가 있으면 결과가 누락됩니다.
- 샤드 수가 과도하지 않은지
- 리밸런싱 중 지연/에러가 증가하지 않는지
- p99 지연이 튀는 구간에 검색 결과 수가 줄지 않는지
7-3. 메모리 압박은 HNSW 품질을 “간접적으로” 깎는다
메모리가 부족하면 OS 캐시 미스가 늘고, 디스크 I/O가 증가해 타임아웃/부분 실패가 늘 수 있습니다. 이 경우 사용자는 “검색 품질이 떨어졌다”고 체감합니다.
- 노드의 RAM 여유
- 디스크 사용률과 I/O wait
- 컨테이너 환경이라면
shm등 런타임 제약
디스크가 꽉 차거나 삭제 파일이 열린 채로 남아 I/O가 급격히 나빠지는 케이스도 있으니, 리눅스 운영 이슈라면 logrotate 했는데 디스크 100%? 열린 삭제파일 찾기 같은 점검도 도움이 됩니다.
튜닝 로드맵 요약(실전 순서)
- 임베딩/청킹/정규화/필터 변경 이력 확인
- 정답셋 + Recall@k로 품질을 수치화
ef_search를 올려 즉시 회복 가능한지 확인(지연과 함께)ef_search가 포화되면m상향 검토(메모리 예산 포함)ef_construct상향으로 인덱스 품질 자체를 개선- 필터 전략(후보군 크기)과 샤딩/레플리카 운영 변수를 점검
- 타임아웃/에러/리소스 압박이 “품질”로 보이는 현상 제거
Qdrant 튜닝 실전 예시: A/B로 안전하게 바꾸는 절차
운영에서 한 번에 바꾸지 말고, 새 컬렉션을 만들어 A/B로 비교하는 방식이 안전합니다.
collection_v1운영 중collection_v2생성(새m,ef_construct적용)- 듀얼 라이트(동일 포인트를 v1/v2에 동시 upsert)
- 동일 쿼리를 v1/v2에 날려 Recall@k, 지연, 에러율 비교
- 목표치 도달 시 read 트래픽 스위치
API 호출은 언어별 SDK를 쓰되, 핵심은 “동일 쿼리, 동일 필터, 동일 k”로 비교하는 것입니다.
마무리: RAG 품질은 결국 “리콜과 안정성”의 합
RAG에서 사용자가 느끼는 품질은 단순 유사도 알고리즘이 아니라,
- HNSW의 근사 탐색 품질(리콜)
- 필터/샤딩/리소스/타임아웃 같은 운영 안정성
이 함께 결정합니다. 위 7단계를 순서대로 밟으면, ef_search만 올려서 CPU를 태우는 식의 임시 처방이 아니라, 측정 기반으로 비용 대비 품질을 최적화할 수 있습니다.
원하시면 현재 사용 중인 벡터 차원, 포인트 수, 평균 필터 선택도, 목표 p95 지연, 목표 Recall@k를 기준으로 m, ef_construct, ef_search의 권장 탐색 범위를 구체적으로 잡아드릴게요.