Published on

Pinecone·Milvus 검색품질 급락? 임베딩 드리프트 탐지

Authors
Binance registration banner

서빙은 멀쩡한데 RAG 검색 품질이 갑자기 무너지는 순간이 있습니다. Pinecone나 Milvus 자체 장애를 의심하기 쉽지만, 실제 현장에서는 임베딩 드리프트(embedding drift) 가 원인인 경우가 훨씬 많습니다. 즉, 인덱스에 들어간 벡터의 분포와, 현재 쿼리에서 생성되는 벡터의 분포가 달라지면서 유사도 점수의 의미가 바뀌고 Top-K가 엉뚱한 문서를 끌어오는 상황입니다.

이 글에서는 Pinecone·Milvus 공통으로 적용 가능한 관점에서

  • 임베딩 드리프트가 왜 검색 품질을 급락시키는지
  • 드리프트를 어떻게 수치로 탐지 하는지
  • 탐지 후 어떤 운영 액션(재색인, 버저닝, 롤백, 가드레일) 을 해야 하는지

를 코드와 함께 정리합니다.

검색 품질 급락의 전형적인 징후

다음 증상이 동시에 보이면 “벡터DB 문제”가 아니라 “임베딩/파이프라인 문제”일 확률이 큽니다.

  • 동일 쿼리의 Top-K가 며칠 전과 완전히 달라짐
  • 평균 유사도 점수(코사인 기준)가 전반적으로 낮아지거나, 반대로 비정상적으로 높아짐
  • 특정 카테고리/언어/길이의 문서만 유난히 검색이 안 됨
  • 재시도해도 결과는 안정적이지만 정답이 아닌 방향으로 안정적

여기서 핵심은 “검색이 랜덤해졌다”가 아니라, “일관되게 이상해졌다”입니다. 이는 보통 분포가 바뀌었기 때문 입니다.

임베딩 드리프트란 무엇인가

임베딩 드리프트는 크게 두 축으로 나뉩니다.

1) 모델 드리프트

  • 임베딩 모델 버전 변경(예: text-embedding-3-small에서 text-embedding-3-large로 변경)
  • 동일 모델이라도 파라미터 변경(정규화 여부, pooling 방식, truncation 길이)
  • ONNX/INT8 양자화 등 최적화 과정에서 수치적 특성이 바뀜

임베딩은 “같은 공간”에서 비교해야 의미가 있습니다. 모델이 바뀌면 공간 자체가 달라져서, 과거에 색인한 벡터와 현재 쿼리 벡터가 서로 다른 좌표계 에 있게 됩니다.

양자화나 변환 파이프라인이 원인인 경우도 많습니다. 임베딩 품질이 미세하게 바뀌어도 검색 품질은 크게 흔들릴 수 있습니다. 관련해서는 PyTorch→ONNX→INT8 양자화 정확도 하락 잡기 같은 최적화 단계에서의 정확도 관리 관점이 그대로 적용됩니다.

2) 데이터/전처리 드리프트

  • 문서 텍스트 전처리 변경(HTML 제거 규칙, 이모지/특수문자 처리, 공백 정규화)
  • chunking 전략 변경(문장 단위에서 토큰 단위로, chunk size 변경)
  • 언어 비율 변화(한글 문서가 늘었는데 영어 중심 임베딩을 그대로 사용)

전처리/청킹은 임베딩 입력을 바꾸기 때문에, 결과적으로 벡터 분포를 바꿉니다. 특히 chunk size가 바뀌면 벡터 노름(norm)이나 유사도 분포가 통째로 달라지는 일이 흔합니다.

Pinecone·Milvus에서 드리프트가 더 치명적인 이유

Pinecone나 Milvus는 인덱싱 구조(HNSW, IVF 등)와 검색 파라미터(efSearch, nprobe)에 따라 근사 탐색의 성질이 달라집니다. 드리프트가 생기면 다음이 겹쳐서 “급락”처럼 보입니다.

  • 분포가 바뀌어 이웃 구조가 달라짐
  • 근사 탐색이 “원래 분포”에 튜닝되어 있었는데, 새 분포에서는 recall이 떨어짐
  • 점수 스케일이 바뀌어 reranker나 threshold 로직이 오작동

즉, 드리프트는 단지 “정답이 달라짐”이 아니라, 인덱스와 탐색 파라미터가 기대하던 가정까지 깨뜨립니다.

드리프트 탐지: 무엇을 측정할 것인가

실무에서 유용한 지표는 크게 3가지입니다.

  1. 벡터 노름(norm) 분포 변화
  • 코사인 유사도를 쓰더라도, 임베딩이 정규화되어 있지 않으면 노름 분포가 의미를 가집니다.
  1. 쿼리-문서 유사도 분포 변화
  • 샘플 쿼리 집합을 고정해두고, Top-k의 점수 분포(평균/분산/상위 퍼센타일)를 비교합니다.
  1. 최근접 이웃의 안정성 변화
  • 동일 문서 벡터를 다시 임베딩했을 때, 자기 자신이 Top-1로 나오지 않거나, 이웃 집합이 크게 바뀌면 위험 신호입니다.

여기서 중요한 운영 팁은 “정답 라벨이 없어도” 드리프트는 충분히 탐지 가능하다는 점입니다. 분포 기반 감시만으로도 대부분의 사고를 조기에 잡습니다.

코드: 임베딩 분포 드리프트를 빠르게 감지하기

아래 예시는 다음을 수행합니다.

  • 과거(베이스라인) 임베딩 샘플과 현재 임베딩 샘플을 비교
  • 노름 분포, 코사인 유사도 분포를 계산
  • 간단한 PSI(Population Stability Index)로 분포 변화량을 수치화
import numpy as np


def l2_norms(x: np.ndarray) -> np.ndarray:
    return np.linalg.norm(x, axis=1)


def cosine_sim(a: np.ndarray, b: np.ndarray, eps: float = 1e-12) -> np.ndarray:
    # a, b: (n, d)
    a_n = a / (np.linalg.norm(a, axis=1, keepdims=True) + eps)
    b_n = b / (np.linalg.norm(b, axis=1, keepdims=True) + eps)
    return np.sum(a_n * b_n, axis=1)


def psi(expected: np.ndarray, actual: np.ndarray, bins: int = 20, eps: float = 1e-6) -> float:
    # expected, actual: 1D arrays
    q = np.quantile(expected, np.linspace(0, 1, bins + 1))
    q[0] -= 1e-9
    q[-1] += 1e-9

    e_hist, _ = np.histogram(expected, bins=q)
    a_hist, _ = np.histogram(actual, bins=q)

    e = e_hist / (np.sum(e_hist) + eps)
    a = a_hist / (np.sum(a_hist) + eps)

    e = np.clip(e, eps, 1)
    a = np.clip(a, eps, 1)

    return float(np.sum((a - e) * np.log(a / e)))


# baseline_embeddings: 과거 문서 임베딩 샘플 (n, d)
# current_embeddings: 현재 문서 임베딩 샘플 (m, d)
# baseline_queries, current_queries: 쿼리 임베딩 샘플

baseline_embeddings = np.load("baseline_doc_emb.npy")
current_embeddings = np.load("current_doc_emb.npy")

baseline_queries = np.load("baseline_query_emb.npy")
current_queries = np.load("current_query_emb.npy")

# 1) norm drift
base_norm = l2_norms(baseline_embeddings)
curr_norm = l2_norms(current_embeddings)

print("norm mean", base_norm.mean(), curr_norm.mean())
print("norm psi", psi(base_norm, curr_norm))

# 2) query-doc similarity drift (샘플링해서 1:1 비교)
# 실제로는 동일 쿼리 세트를 고정해두는 게 좋습니다.
n = min(len(baseline_queries), len(current_queries), len(baseline_embeddings), len(current_embeddings))
base_sim = cosine_sim(baseline_queries[:n], baseline_embeddings[:n])
curr_sim = cosine_sim(current_queries[:n], current_embeddings[:n])

print("sim mean", base_sim.mean(), curr_sim.mean())
print("sim psi", psi(base_sim, curr_sim))

PSI 해석 가이드

PSI는 조직마다 기준이 다르지만, 경험적으로 다음처럼 운영하면 무난합니다.

  • 0.0에서 0.1: 정상 범위
  • 0.1에서 0.25: 주의(배포/데이터 변화 확인)
  • 0.25 이상: 경보(검색 품질 하락 가능성 큼)

PSI는 단독으로 절대 진실은 아니지만, “배포 직후 PSI가 급증했다” 같은 시그널은 매우 강력합니다.

코드: Pinecone·Milvus 공통 Top-K 점수 분포 모니터링

벡터DB를 무엇을 쓰든, 결국 결과는 Top-K와 점수입니다. 샘플 쿼리 집합을 고정해두고, 점수 분포가 바뀌는지 매일 체크하면 드리프트를 빨리 잡습니다.

아래는 “검색 결과 점수의 상위 퍼센타일”을 로그로 남기는 예시입니다. Pinecone SDK나 Milvus client 호출부는 환경마다 달라서, search_fn만 주입받는 형태로 작성했습니다.

import numpy as np
from typing import Callable, List, Dict, Any


def monitor_score_distribution(
    query_vectors: np.ndarray,
    search_fn: Callable[[np.ndarray, int], List[Dict[str, Any]]],
    top_k: int = 10,
) -> Dict[str, float]:
    scores = []
    for q in query_vectors:
        results = search_fn(q, top_k)
        # results: [{"id": "...", "score": 0.123}, ...]
        for r in results:
            scores.append(float(r["score"]))

    arr = np.array(scores, dtype=np.float64)
    if len(arr) == 0:
        return {"count": 0}

    return {
        "count": float(len(arr)),
        "mean": float(arr.mean()),
        "p50": float(np.quantile(arr, 0.50)),
        "p90": float(np.quantile(arr, 0.90)),
        "p99": float(np.quantile(arr, 0.99)),
        "min": float(arr.min()),
        "max": float(arr.max()),
    }


# 사용 예시
# search_fn은 Pinecone든 Milvus든 결과를 score 포함 형태로 표준화해서 반환

이 지표는 단순하지만 효과가 큽니다. 예를 들어 코사인 유사도 기반 시스템에서 p90이 갑자기 떨어지면 “쿼리-문서가 전반적으로 멀어졌다”는 뜻이고, 이는 임베딩 공간이 바뀌었거나 전처리가 달라졌을 가능성이 큽니다.

원인 추적 체크리스트: 드리프트는 어디서 생겼나

드리프트가 감지되면, 다음 순서로 원인을 좁히는 것이 빠릅니다.

1) 임베딩 모델 버전과 설정 확인

  • 모델 이름/버전이 바뀌었는지
  • normalize_embeddings 같은 후처리 여부가 바뀌었는지
  • 입력 최대 길이(truncation)가 바뀌었는지

여기서 가장 흔한 사고는 “쿼리는 새 모델, 문서는 옛 모델”처럼 부분 교체 가 일어난 경우입니다.

2) 차원(dimension)과 거리 메트릭 확인

  • 임베딩 차원이 바뀌었는데 인덱스는 그대로인 경우
  • 코사인과 내적(dot), L2를 혼용한 경우

특히 Milvus는 컬렉션 스키마와 인덱스 파라미터가 고정되기 때문에, 차원 변경은 거의 항상 재색인이 필요합니다.

3) 청킹/전처리 변경 확인

  • chunk size, overlap 변경
  • 문서 텍스트 추출 로직 변경(PDF 파서, HTML stripper)

이 변화는 코드 diff만 보면 작아 보여도, 임베딩 입력이 크게 바뀌어 검색 품질이 급락할 수 있습니다.

4) 근사 탐색 파라미터 재튜닝 필요 여부

분포가 바뀌면 nprobeefSearch 같은 파라미터의 최적점도 바뀝니다. “드리프트가 약간”이어도, 근사 탐색이 그 약간을 증폭시켜 recall이 떨어질 수 있습니다.

대응 전략: 재색인만이 답인가

드리프트의 종류에 따라 대응이 달라집니다.

A) 모델이 바뀌었다면: 버저닝과 이중 인덱스가 정석

  • embedding_version을 메타데이터로 저장
  • 새 버전 인덱스를 별도로 구축(이중 인덱스)
  • 트래픽을 점진적으로 전환(canary)

이 방식은 비용이 들지만, “검색 품질 급락” 같은 대형 사고를 가장 확실히 막습니다.

B) 전처리/청킹이 바뀌었다면: 재색인 범위를 줄여라

모든 문서를 재색인하기 전에, 영향 범위를 먼저 계산할 수 있습니다.

  • 변경된 파서/규칙이 적용되는 문서만 재처리
  • chunking 규칙이 바뀐 경우, 문서 단위가 아니라 “chunk 단위”로 증분 재생성

C) 점수 스케일이 바뀌었다면: threshold 로직부터 점검

많은 RAG 시스템은

  • score가 특정 값 이하이면 “검색 실패” 처리
  • 또는 Top-K 평균 점수가 낮으면 “웹 검색 fallback”

같은 정책을 둡니다. 드리프트로 점수 스케일이 바뀌면 이 정책이 오작동해 체감 품질이 더 급락합니다. 먼저 threshold를 로그 기반으로 재보정하세요.

운영 가드레일: 재발 방지를 위한 설계

1) 임베딩 파이프라인에 “서명(signature)” 남기기

문서/쿼리 임베딩을 만들 때 아래를 함께 저장하면, 사고 원인 파악이 빨라집니다.

  • 모델 이름과 버전
  • 전처리 버전
  • chunking 파라미터
  • 차원 수

이 값들을 합쳐 해시를 만들고, embedding_signature로 메타데이터에 넣는 방식이 실전에서 유용합니다.

2) 배포 파이프라인에서 드리프트 테스트를 게이트로

CI에서 샘플 문서/쿼리를 임베딩해 PSI 같은 지표를 계산하고, 기준을 넘으면 배포를 막는 방식입니다. 이때 캐시나 빌드 환경 차이로 테스트가 흔들리면 신뢰도가 떨어지니, CI 캐시/환경을 안정화하는 것도 중요합니다. 관련해서는 Docker BuildKit 캐시 깨짐? GitLab CI 속도 3배 같은 캐시 안정화 글이 참고가 됩니다.

3) 온라인 모니터링: “고정 쿼리 세트”로 매일 리그레션

  • 실제 사용자 쿼리에서 대표 샘플을 뽑아 고정
  • 매일 Top-K 결과의 변화율, 점수 분포, 중복률 등을 기록

이 방식은 라벨 없이도 품질 변화를 빠르게 감지합니다.

4) 앱 레벨 캐시/ISR가 품질 이슈를 가리는 경우

검색 품질 문제를 디버깅할 때, 프론트/엣지 캐시가 과거 결과를 보여줘서 “문제가 없는 것처럼 보였다가” 캐시 만료 후 갑자기 폭발하는 경우가 있습니다. Next.js를 쓴다면 ISR 캐시 동작도 함께 점검하세요. Next.js ISR 캐시 꼬임으로 404·구버전 뜰 때 해결 관점이 그대로 적용됩니다.

Pinecone와 Milvus에서 특히 자주 하는 실수

Pinecone: 네임스페이스/인덱스 전략 없이 모델만 교체

  • 같은 인덱스에 새 임베딩을 섞어 넣으면, 결과가 “부분적으로만” 망가져서 더 찾기 어렵습니다.
  • 모델 버전별로 namespace를 분리하거나, 인덱스를 분리하는 편이 안전합니다.

Milvus: 인덱스 파라미터를 고정값으로 박아두기

  • 데이터 분포가 바뀌면 nprobe 최적값도 바뀝니다.
  • 드리프트 감지 후에는 재색인뿐 아니라 탐색 파라미터 재튜닝 도 체크리스트에 넣으세요.

결론: “벡터DB가 느려졌다”보다 먼저 드리프트를 의심하라

Pinecone·Milvus에서 검색 품질이 급락할 때, 가장 먼저 확인할 것은 CPU나 인덱스 상태가 아니라 임베딩 공간이 동일한가 입니다.

  • 모델/전처리/청킹이 바뀌면 임베딩 드리프트가 생긴다
  • 드리프트는 PSI, 노름/점수 분포, 이웃 안정성으로 라벨 없이도 탐지 가능하다
  • 대응은 재색인만이 아니라 버저닝, 이중 인덱스, threshold 재보정, 파라미터 재튜닝까지 포함된다

운영 관점에서 가장 강력한 해법은 “임베딩 버전 관리”와 “고정 쿼리 리그레션 모니터링”입니다. 이 두 가지만 갖춰도 검색 품질 급락의 상당수를 배포 전에 막을 수 있습니다.