- Published on
Pinecone·Milvus 검색 품질 급락? 임베딩·HNSW 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중이던 RAG/시맨틱 검색이 어느 날부터 “엉뚱한 문서만” 가져오거나, 특정 쿼리에서만 정확도가 급락하는 경우가 있습니다. Pinecone든 Milvus든 내부 엔진 차이는 있어도, 품질이 무너지는 패턴은 꽤 비슷합니다. 대부분은 임베딩(모델/전처리/정규화) 변화 또는 근사 최근접 탐색(ANN) 인덱스 튜닝 불일치, 그리고 필터링/스코어링 설계에서 발생합니다.
이 글은 “왜 품질이 떨어졌는지”를 감으로 추측하지 않고, 재현 가능한 측정 → 원인 분리 → 튜닝 순서로 접근합니다. 하이브리드 검색과 재랭킹까지 포함한 품질 방어선은 RAG 환각 줄이기 - 하이브리드 검색+재랭킹 튜닝도 함께 참고하면 좋습니다.
1) 먼저: 품질 급락을 “수치”로 고정하기
운영 장애처럼 보이는 품질 문제는 먼저 측정 지표를 고정해야 합니다.
추천 오프라인 지표
Recall@k: 정답 문서가 상위k에 포함되는 비율MRR@k: 정답이 상위에 올수록 높은 점수nDCG@k: 관련도 레이블이 다단계인 경우
온라인 지표(실서비스)
- 클릭/채택률, 답변 후속 질문률, “찾는 내용이 없어요” 비율
- RAG라면: 근거 문서 사용률, 인용 문서의 중복률, 컨텍스트 토큰 낭비율
핵심은 “어제까지 Recall@10이 0.82였는데 오늘 0.63으로 떨어졌다”처럼 회귀(regression) 를 명확히 만드는 것입니다.
2) 품질 급락의 1순위: 임베딩 드리프트(모델/전처리/정규화)
Pinecone/Milvus에서 검색 품질이 갑자기 떨어질 때, ANN 인덱스보다 더 자주 범인인 게 임베딩입니다. 특히 아래 변화가 있으면 거의 확정입니다.
체크리스트
- 임베딩 모델 버전 변경(예:
text-embedding-3-large로 교체) - 차원 변경(예: 768에서 1536)
- 토크나이저/정규화 변경(소문자화, 특수문자 제거, 공백 정리)
- 문서 청킹 정책 변경(청크 크기, 오버랩)
- 벡터 정규화 방식 변경(L2 normalize 여부)
- metric 변경(코사인 vs 내적 vs L2)
“같은 쿼리인데 갑자기 엉뚱해짐”의 전형
- 인덱스에는 예전 임베딩이 섞여 있고
- 쿼리는 새 임베딩으로 만들고
- metric/정규화도 다르면
결과적으로 “서로 다른 좌표계”에서 검색하는 꼴이 됩니다.
빠른 검증: 임베딩 호환성 스모크 테스트
아래는 쿼리/문서 임베딩의 정규화 여부와 코사인 유사도를 점검하는 간단한 예시입니다.
import numpy as np
def l2_normalize(v: np.ndarray) -> np.ndarray:
n = np.linalg.norm(v)
return v if n == 0 else v / n
def cosine(a: np.ndarray, b: np.ndarray) -> float:
a = l2_normalize(a)
b = l2_normalize(b)
return float(np.dot(a, b))
# 같은 문장을 서로 다른 파이프라인으로 임베딩했다고 가정
q_old = np.random.randn(1536)
q_new = np.random.randn(1536)
print("cos(old, new)=", cosine(q_old, q_new))
실제로는 “동일 문장”을 구버전/신버전 임베딩으로 각각 만들고, 그 유사도가 과하게 낮아졌다면(특히 분포가 달라졌다면) 인덱스 재생성이 필요합니다.
운영 팁: 임베딩 버전 필드로 혼합 인덱스 방지
메타데이터에 embedding_version을 넣고, 쿼리 시 동일 버전만 검색하거나(필터) 아예 컬렉션/인덱스를 버전별로 분리하세요.
{
"id": "doc_123#chunk_04",
"embedding_version": "v3_2026_02",
"source": "kb",
"lang": "ko"
}
필터가 성능에 미치는 영향은 DB 인덱스와 유사한 함정이 있습니다. 조건이 복잡할수록 “검색은 했는데 결과가 빈약”해질 수 있으니, 필터 설계 감각은 PostgreSQL JSONB 인덱스가 안타는 이유 7가지에서 다루는 사고방식도 도움이 됩니다.
3) ANN(HNSW) 튜닝: 품질 급락은 보통 ef 부족에서 시작
Pinecone과 Milvus 모두 내부적으로 HNSW 또는 유사한 ANN 구조를 사용/지원합니다. HNSW에서 품질(Recall)을 좌우하는 대표 파라미터는 다음입니다.
M: 그래프에서 노드가 갖는 최대 이웃 수(메모리와 빌드 시간 증가)efConstruction: 인덱스 구축 시 탐색 폭(클수록 품질↑, 빌드 비용↑)efSearch또는ef: 검색 시 탐색 폭(클수록 Recall↑, 지연↑)
품질이 “갑자기” 떨어지는 HNSW 원인 패턴
- 트래픽 증가로 지연 SLA를 맞추려고
efSearch를 낮춤 - 샤딩/리소스 변경으로 자동 튜닝이 바뀜
- 데이터가 커졌는데
efSearch를 그대로 둠(상대적으로 탐색이 얕아짐) - 필터가 추가되어 후보 풀이 줄었는데
efSearch가 낮아 더 줄어듦
경험칙(초기값 가이드)
M: 16 또는 32에서 시작(텍스트 임베딩은 16도 자주 충분)efConstruction: 100~400efSearch: 50~200 (품질 문제면 먼저 올려서 상한을 확인)
중요한 건 “정답률이 목표치에 도달하는 최소 efSearch”를 찾는 것입니다. 즉, 품질-지연 트레이드오프 커브를 그려야 합니다.
4) 튜닝 절차: 브루트포스 기준선부터 만들기
HNSW는 근사 검색이라, “얼마나 근사해도 되는지”를 모르면 튜닝이 감각싸움이 됩니다. 그래서 표본 데이터에서 브루트포스(정확 탐색) 기준선을 만들고, HNSW 결과와 비교합니다.
오프라인 평가 파이프라인(개념)
- 쿼리-정답 문서 쌍(또는 관련 문서 리스트) 준비
- 브루트포스로
top_k계산(정답에 가까운 진짜 상위 결과) - HNSW로
top_k계산 Recall@k비교
import numpy as np
def topk_bruteforce(query, docs, k=10):
# cosine similarity with L2 normalization
q = query / (np.linalg.norm(query) + 1e-12)
d = docs / (np.linalg.norm(docs, axis=1, keepdims=True) + 1e-12)
sims = d @ q
idx = np.argsort(-sims)[:k]
return idx, sims[idx]
# query: (dim,), docs: (n, dim)
이 기준선을 만든 뒤에야 efSearch를 올렸을 때 Recall이 어디까지 회복되는지 “숫자”로 확인할 수 있습니다.
5) Pinecone에서 자주 겪는 품질 이슈 포인트
서비스형 벡터DB는 내부 구현이 감춰져 있어 “내가 바꾼 게 없는데?”라는 상황이 생깁니다. 그래서 아래를 특히 점검합니다.
(1) Metric과 정규화의 불일치
- 코사인 유사도는 보통 벡터 정규화에 민감합니다.
- 내적(dot product)은 벡터 크기(scale)에 영향을 받습니다.
운영 중 임베딩 파이프라인에서 정규화가 빠지면, 유사도 분포가 깨지고 상위 결과가 뒤틀립니다.
(2) 필터 추가로 후보 풀이 급감
lang, tenant_id, doc_type 같은 필터는 필수지만, 조합이 늘면 “검색 공간이 너무 좁아져서” 품질이 떨어진 것처럼 보일 수 있습니다. 이 경우는 ANN 문제가 아니라 데이터 커버리지 문제입니다.
대응:
- 필터 조합별 최소 문서 수 모니터링
- 필터가 강할수록
top_k를 늘리고 재랭킹으로 정밀도를 회복
(3) 업데이트/삭제가 많은 워크로드
대량 upsert와 delete가 반복되면 세그먼트/컴팩션 특성에 따라 품질/지연이 흔들릴 수 있습니다. 이때는
- 배치 단위 upsert
- 삭제 대신 tombstone 전략 후 주기적 리빌드 같은 운영 전략이 필요합니다.
6) Milvus에서 자주 겪는 품질 이슈 포인트
Milvus는 설정 자유도가 큰 만큼, “내가 설정을 잘못했는데도 돌아는 가는” 케이스가 많습니다.
(1) 인덱스 타입/파라미터가 컬렉션에 부적합
HNSW 외에도 IVF 계열을 쓰는 경우가 있는데, 데이터 규모/분포에 따라 파라미터가 크게 달라집니다. 품질 급락이 보이면 먼저
- 인덱스 파라미터를 코드/인프라 설정에서 추적 가능하게 만들고
- 변경 이력을 남기세요.
(2) 검색 파라미터(ef)가 너무 낮음
Milvus는 검색 시 파라미터를 호출마다 줄 수 있습니다. 운영에서 지연을 줄이려고 낮춰두면, 가장 먼저 Recall이 무너집니다.
(3) scalar filter와 ANN의 결합
필터가 강하면 후보가 줄어 ANN이 더 근사적으로 동작합니다. 이때는
- 먼저 필터로 후보를 줄인 뒤 ANN을 하는 구조인지
- ANN 후 필터링인지 엔진 전략에 따라 결과가 달라집니다.
7) “HNSW만”으로 해결 안 되면: 하이브리드 + 재랭킹
임베딩이 애매한 도메인(짧은 질의, 숫자/코드/약어, 고유명사)에서는 벡터 검색만으로 안정적인 품질을 얻기 어렵습니다.
권장 아키텍처:
- BM25(키워드) + 벡터(시맨틱) 하이브리드로 후보를 넓히고
- Cross-encoder 또는 LLM 기반 재랭킹으로 최종 정렬
이 접근은 “ANN 파라미터를 아무리 올려도 정답이 안 나오는” 케이스에서 특히 효과적입니다. 구체 패턴과 튜닝은 RAG 환각 줄이기 - 하이브리드 검색+재랭킹 튜닝에서 더 깊게 다뤘습니다.
8) 실전 튜닝 플레이북(우선순위)
운영에서 가장 빠르게 원인을 좁히는 순서입니다.
1단계: 임베딩 파이프라인 회귀 확인
- 모델/차원/정규화/전처리/청킹 변경 여부
- 인덱스 내 임베딩 버전 혼재 여부
- metric 설정과 임베딩 특성 일치 여부
2단계: efSearch를 올려 “상한” 확인
efSearch를 크게 올렸을 때 Recall이 회복되면 ANN 근사 문제- 올려도 회복이 없으면 임베딩/데이터/필터 문제 가능성 큼
3단계: 필터 조합별 데이터 커버리지 점검
- 특정
tenant_id나 특정lang에서만 급락한다면 필터가 원인일 확률이 높음
4단계: 인덱스 리빌드/파라미터 재설계
- 데이터가 성장했으면
M,efConstruction재검토 - 업데이트가 많으면 리빌드 주기/전략 수립
5단계: 하이브리드 + 재랭킹으로 방어선 구축
- 벡터 검색은 “후보 생성”으로 두고
- 재랭킹으로 최종 품질을 고정
HNSW 자체의 지연 최적화 관점은 벡터DB가 달라도 공통점이 많습니다. ef/M/efConstruction 트레이드오프를 더 체계적으로 보고 싶다면 pgvector HNSW로 RAG 검색 지연 50% 줄이기도 같이 읽으면 좋습니다.
9) 운영에서 꼭 넣어야 하는 모니터링
품질 급락은 “사후 분석”보다 “사전 탐지”가 훨씬 쌉니다.
- 임베딩 분포 모니터링: 벡터 L2 norm 분포, 평균/분산, 이상치 비율
- 쿼리-결과 유사도 분포: 상위 1개 점수의 이동
- 필터 조합별 결과 수:
top_k대비 실제 반환 수 - 인덱스/검색 파라미터 변경 이력: GitOps 또는 설정 스냅샷
임베딩 norm이 갑자기 커지거나 작아지는 건 정규화 누락/스케일 변경의 강력한 신호입니다.
import numpy as np
# vectors: (n, dim)
norms = np.linalg.norm(vectors, axis=1)
print("mean=", norms.mean(), "p95=", np.percentile(norms, 95))
마무리: 품질 급락은 ‘임베딩’과 ‘탐색폭’에서 갈린다
Pinecone/Milvus에서 검색 품질이 무너질 때, 가장 먼저 의심할 것은
- 인덱스가 아니라 임베딩 파이프라인의 일관성
- 그리고 HNSW의 검색 탐색폭(
efSearch) 부족 입니다.
진단은 항상
- 오프라인 기준선(브루트포스)으로 회귀를 고정하고
efSearch를 올려 ANN 근사 문제인지 확인한 뒤- 임베딩/필터/데이터 커버리지/재랭킹 순으로 확장 하는 흐름이 가장 빠릅니다.
이 루틴을 넣어두면 “갑자기 품질이 떨어졌다”가 아니라, “어떤 변화가 Recall을 몇 퍼센트 떨어뜨렸는지”까지 추적 가능한 시스템이 됩니다.