- Published on
Milvus RAG 리콜 급락? HNSW 파라미터 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
RAG 파이프라인을 운영하다 보면 어느 날부터 답변 품질이 눈에 띄게 나빠지고, 로그를 보면 검색 단계에서 상위 문서가 엉뚱하게 뽑히는 경우가 있습니다. 특히 Milvus에서 HNSW 기반 ANN 검색을 쓰는 환경이라면, 리콜(recall) 급락의 원인이 인덱스/검색 파라미터 미스매치인 경우가 매우 흔합니다.
이 글은 다음 상황을 전제로 합니다.
- 동일한 쿼리인데도 TopK 결과가 자주 바뀌거나, 정답 문서가 TopK에서 사라짐
- 트래픽 증가나 배포 이후부터 리콜이 급격히 하락
- 지연시간은 줄었는데(또는 비슷한데) 품질이 악화
핵심은 간단합니다. HNSW는 근사 검색이므로 ef(검색 폭)와 인덱스 품질(M, efConstruction)을 충분히 확보하지 않으면 리콜이 쉽게 무너집니다. 반대로 무작정 키우면 지연시간과 메모리가 폭증합니다. 운영에 맞는 균형점을 찾는 튜닝이 필요합니다.
또한 RAG 운영 이슈는 “원인-증상”이 얽히기 쉬워서, 관측/재시도/백오프 같은 안정화 패턴도 함께 챙겨야 합니다. API 호출이 섞여 있다면 OpenAI 429 rate_limit_exceeded 재시도·백오프 설계 같은 글의 패턴도 검색 품질 방어에 간접적으로 도움이 됩니다(재시도로 인한 타임아웃이 검색 단계 품질 지표를 왜곡하는 경우가 많습니다).
리콜 급락을 먼저 “정의”하기
튜닝을 시작하기 전에, 리콜 급락을 정량적으로 정의해야 합니다. 추천하는 최소 지표는 아래 3가지입니다.
- Hit@K: 정답(또는 강한 정답 후보) 문서가 TopK에 포함되는 비율
- MRR: 정답 문서의 평균 순위 품질
- p95 지연시간: 검색 단계(벡터 검색)만 분리해서 측정
정답 레이블이 없다면, 임시로 다음을 사용합니다.
- 이전 버전(또는 brute-force) 결과를 “준정답”으로 삼아 근사 리콜을 측정
- 사내 FAQ/문서 링크 클릭 로그를 약한 정답으로 삼아 Hit@K를 측정
근사 리콜 측정의 기준선 만들기
운영 환경에서 가장 빠른 기준선은 “동일 컬렉션에서 작은 샘플만” brute-force로 돌려 보는 것입니다.
- 샘플 N: 5,000~50,000
- 쿼리 Q: 200~2,000
- metric: cosine 또는 inner product 등 실제 설정과 동일
이 기준선이 있어야 ef를 올렸을 때 리콜이 얼마나 회복되는지, 혹은 인덱스 자체가 망가졌는지 판단할 수 있습니다.
HNSW에서 리콜을 좌우하는 3대 파라미터
Milvus에서 HNSW를 쓸 때, 리콜과 지연시간을 크게 좌우하는 건 아래 3가지입니다.
M: 그래프의 연결도(노드당 링크 수). 메모리와 인덱스 품질에 큰 영향efConstruction: 인덱스 빌드 품질. 빌드 시간과 메모리, 품질에 영향ef(또는efSearch): 검색 시 탐색 폭. 리콜을 가장 즉각적으로 올리는 레버
정리하면:
- 리콜이 갑자기 떨어졌다면 1차로
ef를 의심 - 배포/재인덱싱 이후부터 계속 낮다면
M,efConstruction, metric/정규화, 데이터 분포 변화를 의심
“리콜 급락”의 흔한 원인 체크리스트
1) ef가 너무 작아짐
가장 흔한 케이스입니다. 예를 들어 지연시간을 줄이려고 ef를 낮췄는데, 특정 도메인/긴 문장 쿼리에서 리콜이 크게 무너집니다.
- 증상: 평균 지연시간은 줄었는데 Hit@K가 하락
- 해결:
ef를 올려서 리콜-지연시간 커브를 다시 측정
2) topK만 올리고 ef는 그대로
topK를 5에서 20으로 올렸는데 ef가 32 같은 값이면, 탐색 자체가 빈약해서 TopK가 늘어도 “좋은 후보”가 늘지 않습니다.
권장 감각:
ef는 최소topK보다 충분히 커야 함- 실무에선
ef를topK의 5배~20배 범위에서 탐색
3) metric 불일치 또는 벡터 정규화 누락
임베딩이 cosine 기반인데 inner product로 검색하거나, cosine을 쓰면서 벡터 정규화를 빼먹으면 분포가 틀어져 리콜이 급락할 수 있습니다.
- cosine을 쓰면 보통 L2 정규화를 전제
- inner product를 cosine처럼 쓰고 싶다면 입력 벡터를 정규화하고 IP를 쓰는 방식도 가능
4) 인덱스 재빌드 시 M/efConstruction이 낮아짐
재인덱싱 파이프라인에서 “성능 최적화”를 하다가 빌드 파라미터를 낮춰 인덱스 품질이 구조적으로 떨어지는 경우가 있습니다.
- 증상:
ef를 올려도 리콜이 일정 수준 이상 회복되지 않음 - 해결:
M/efConstruction을 올려 재빌드 후 비교
5) 데이터 분포 변화(신규 문서 대량 유입, 도메인 확장)
HNSW는 데이터가 더 복잡해질수록(클러스터가 늘고 경계가 많아질수록) 더 많은 탐색이 필요해집니다.
- 기존에
ef=64로 충분했는데, 데이터가 2배 늘면서ef=128이상이 필요해질 수 있음
Milvus에서 HNSW 인덱스 생성/검색 예시
아래 예시는 Python SDK(pymilvus) 기준의 전형적인 설정입니다. 실제 버전별로 필드명이 조금씩 다를 수 있으니, 본인 환경의 Milvus 문서를 함께 확인하세요.
from pymilvus import (
connections, FieldSchema, CollectionSchema, DataType,
Collection
)
connections.connect(alias="default", host="localhost", port="19530")
dim = 1024
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=dim),
]
schema = CollectionSchema(fields, description="RAG embeddings")
col = Collection(name="rag_chunks", schema=schema)
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 32,
"efConstruction": 200
}
}
col.create_index(field_name="embedding", index_params=index_params)
col.load()
# 검색 파라미터: ef(탐색 폭)
search_params = {
"metric_type": "COSINE",
"params": {"ef": 128}
}
query_vec = [[0.0] * dim] # 예시
res = col.search(
data=query_vec,
anns_field="embedding",
param=search_params,
limit=10,
output_fields=[]
)
print(res[0][0].distance)
여기서 리콜 급락을 다룰 때 가장 먼저 바꿔볼 값은 params의 ef입니다. 인덱스를 재빌드하기 전에 검색 파라미터만으로도 리콜이 크게 회복되는지 확인하세요.
튜닝 전략: ef 먼저, 그 다음 M/efConstruction
1단계: ef 스윕으로 리콜-지연시간 커브를 만든다
추천 절차:
topK를 고정(예: 10)ef를32, 64, 96, 128, 192, 256, 384처럼 단계적으로 증가- 각 단계에서 Hit@10, MRR, p95 지연시간을 측정
판단 기준(실무 감각):
ef를 올렸는데 Hit@K가 거의 안 오르면 인덱스 품질(M,efConstruction) 또는 metric/정규화 문제 가능성이 큼ef를 올릴수록 Hit@K가 꾸준히 오르면, 인덱스는 정상이고 검색 탐색 폭이 부족했던 것
2단계: M을 조정해 “기본 인덱스 품질”을 올린다
M은 메모리 사용량을 크게 좌우합니다. 하지만 데이터가 커지고 다양해질수록 M이 낮으면 구조적으로 리콜이 불리해집니다.
- 일반적인 출발점:
M=16또는M=32 - 리콜이 민감한 RAG(정답 문서가 희소한 케이스)라면
M=32이상도 자주 씁니다.
경향:
M증가: 리콜 상승(특히 낮은ef에서도), 메모리 증가- 너무 큰
M: 메모리/캐시 미스 증가로 지연시간이 오히려 나빠질 수 있음
3단계: efConstruction으로 빌드 품질을 올린다
efConstruction은 빌드 시 더 넓게 탐색하며 그래프를 더 “좋게” 만드는 값입니다.
- 출발점:
efConstruction=100~200 - 리콜이 안 나오면 300~500도 고려
주의:
efConstruction을 올리면 빌드 시간이 늘고, 빌드 중 자원 사용량이 커집니다.- 운영에선 야간 배치, 별도 빌드 클러스터, 롤링 교체 전략이 필요합니다.
리콜 급락을 “운영” 관점에서 막는 방법
검색 파라미터를 코드에 하드코딩하지 말고, 실시간으로 조절 가능하게
ef는 가장 강력한 즉시 대응 수단입니다. 장애/품질 이슈가 생겼을 때 빠르게 올려서 품질을 방어하고, 이후 근본 원인을 분석하는 식으로 운영하는 게 좋습니다.
- Feature flag 또는 config로
ef를 런타임 조정 - 트래픽/지연시간에 따라
ef를 동적으로 조절(Adaptive ef)
캐시/재시도/타임아웃이 리콜 지표를 오염시키는지 확인
RAG는 보통 다음 단계들이 이어집니다.
- 임베딩 생성
- 벡터 검색
- rerank(선택)
- LLM 생성
이 중 1, 4는 외부 API 또는 GPU 자원에 영향을 크게 받고, 재시도/백오프가 들어가면 전체 지연시간이 출렁입니다. 이때 “느린 요청”을 타임아웃으로 잘라버리면, 품질 측정 데이터셋에서 특정 쿼리가 누락되어 리콜이 왜곡될 수 있습니다.
재시도 설계가 필요하다면 Claude 3 API 529/503 과부하 재시도·백오프 설계에서 다룬 것처럼, 지수 백오프 + 지터 + 상한을 두고, 검색 단계와 생성 단계의 타임아웃을 분리하세요.
실전 예시: 리콜이 떨어졌을 때의 빠른 진단 플로우
아래는 제가 운영에서 자주 쓰는 순서입니다.
1) 동일 쿼리로 ef만 올려서 재현
- 현재
ef=64에서 리콜이 낮다면ef=256까지 올려서 결과가 회복되는지 확인 - 회복된다면: 인덱스/metric보다는 탐색 폭 문제일 가능성이 큼
2) metric/정규화 확인
- cosine인데 정규화를 안 했다면, 임베딩 저장 시점 또는 검색 시점에 정규화 적용
간단한 정규화 예시:
import numpy as np
def l2_normalize(v: np.ndarray, eps: float = 1e-12) -> np.ndarray:
n = np.linalg.norm(v)
return v / (n + eps)
vec = np.random.randn(1024).astype(np.float32)
vec = l2_normalize(vec)
3) 인덱스 재빌드 파라미터 비교
- 과거 인덱스의
M,efConstruction과 현재 값을 비교 - 재빌드 파이프라인 변경 이력 확인
4) 데이터 변화량 확인
- 최근 1주/1달 문서 수 증가
- chunking 정책 변경(청크 길이, overlap)
- 임베딩 모델 변경(차원, 도메인 적합성)
여기서 chunking이 바뀌면 “정답 문서”의 형태가 달라져 Hit@K가 흔들릴 수 있습니다. 이건 HNSW 튜닝만으로 해결되지 않으니, 평가셋 정의도 함께 업데이트해야 합니다.
권장 시작값(출발점)과 튜닝 가이드
데이터 규모와 도메인에 따라 다르지만, RAG 청크 수가 수십만~수천만으로 커질 때 자주 쓰는 출발점은 아래입니다.
- 인덱스
M=16또는M=32efConstruction=200
- 검색
topK=10ef=128부터 시작해서 리콜이 부족하면 192, 256로 증가
경험칙:
- 리콜이 최우선이면
ef를 올리는 것이 가장 빠른 해결책 ef를 너무 올려도 리콜이 안 오르면 인덱스를 의심(M,efConstruction)- 지연시간이 빡빡한 서비스라면 rerank를 붙이기 전에, 먼저
ef와topK의 균형을 잡아야 함
튜닝 결과를 재현 가능하게 남기는 방법
HNSW 튜닝은 “감”으로 하면 반드시 재발합니다. 아래를 로그/메타데이터로 남겨두면, 다음 리그레션 때 원인 추적이 쉬워집니다.
- 컬렉션 버전(데이터 스냅샷 ID)
- 임베딩 모델 버전 및 차원
- metric 타입
- HNSW:
M,efConstruction - Search:
ef,topK - 평가셋 버전과 Hit@K/MRR/p95
이건 데이터 파이프라인에서 조인/중복으로 평가셋이 깨질 때도 유용합니다. 평가 로그를 합치다가 행이 폭증하면 지표가 왜곡되는데, 이런 문제는 Pandas merge에서 행 폭증? 중복키 진단법 같은 패턴으로 빠르게 잡을 수 있습니다.
마무리: 리콜 급락은 “대개 ef”, 하지만 “항상 ef”는 아니다
Milvus HNSW에서 리콜이 갑자기 떨어지면, 우선 ef를 올려서 회복되는지 확인하는 게 가장 비용 대비 효과가 큽니다. 그 다음으로 metric/정규화, topK와의 밸런스, 인덱스 빌드 파라미터(M, efConstruction), 데이터 분포 변화 순으로 점검하면 대부분의 케이스를 커버할 수 있습니다.
운영 관점에서의 결론은 이렇습니다.
ef는 런타임에서 조절 가능한 “품질 레버”로 설계- 인덱스 파라미터는 재빌드 비용이 크니, 평가셋 기반으로 신중히 변경
- 리콜 지표는 타임아웃/재시도/평가셋 조인 오류에 쉽게 오염되므로, 관측과 데이터 품질을 함께 관리
이 3가지만 체계화해도 “RAG가 갑자기 멍청해졌다”는 이슈의 절반 이상은 빠르게 진단하고 복구할 수 있습니다.