Published on

Pinecone·Milvus 검색 품질 급락? 임베딩·HNSW 튜닝

Authors

운영 중이던 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 인덱스보다 더 자주 범인인 게 임베딩입니다. 특히 아래 변화가 있으면 거의 확정입니다.

체크리스트

  1. 임베딩 모델 버전 변경(예: text-embedding-3-large로 교체)
  2. 차원 변경(예: 768에서 1536)
  3. 토크나이저/정규화 변경(소문자화, 특수문자 제거, 공백 정리)
  4. 문서 청킹 정책 변경(청크 크기, 오버랩)
  5. 벡터 정규화 방식 변경(L2 normalize 여부)
  6. 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 원인 패턴

  1. 트래픽 증가로 지연 SLA를 맞추려고 efSearch를 낮춤
  2. 샤딩/리소스 변경으로 자동 튜닝이 바뀜
  3. 데이터가 커졌는데 efSearch를 그대로 둠(상대적으로 탐색이 얕아짐)
  4. 필터가 추가되어 후보 풀이 줄었는데 efSearch가 낮아 더 줄어듦

경험칙(초기값 가이드)

  • M: 16 또는 32에서 시작(텍스트 임베딩은 16도 자주 충분)
  • efConstruction: 100~400
  • efSearch: 50~200 (품질 문제면 먼저 올려서 상한을 확인)

중요한 건 “정답률이 목표치에 도달하는 최소 efSearch”를 찾는 것입니다. 즉, 품질-지연 트레이드오프 커브를 그려야 합니다.

4) 튜닝 절차: 브루트포스 기준선부터 만들기

HNSW는 근사 검색이라, “얼마나 근사해도 되는지”를 모르면 튜닝이 감각싸움이 됩니다. 그래서 표본 데이터에서 브루트포스(정확 탐색) 기준선을 만들고, HNSW 결과와 비교합니다.

오프라인 평가 파이프라인(개념)

  1. 쿼리-정답 문서 쌍(또는 관련 문서 리스트) 준비
  2. 브루트포스로 top_k 계산(정답에 가까운 진짜 상위 결과)
  3. HNSW로 top_k 계산
  4. 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만”으로 해결 안 되면: 하이브리드 + 재랭킹

임베딩이 애매한 도메인(짧은 질의, 숫자/코드/약어, 고유명사)에서는 벡터 검색만으로 안정적인 품질을 얻기 어렵습니다.

권장 아키텍처:

  1. BM25(키워드) + 벡터(시맨틱) 하이브리드로 후보를 넓히고
  2. 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) 부족 입니다.

진단은 항상

  1. 오프라인 기준선(브루트포스)으로 회귀를 고정하고
  2. efSearch를 올려 ANN 근사 문제인지 확인한 뒤
  3. 임베딩/필터/데이터 커버리지/재랭킹 순으로 확장 하는 흐름이 가장 빠릅니다.

이 루틴을 넣어두면 “갑자기 품질이 떨어졌다”가 아니라, “어떤 변화가 Recall을 몇 퍼센트 떨어뜨렸는지”까지 추적 가능한 시스템이 됩니다.