Published on

Pinecone·Milvus 하이브리드검색 튜닝 실전

Authors

서치 품질이 어느 날 갑자기 떨어지면, 임베딩 모델만 의심하기 쉽습니다. 하지만 실제로는 하이브리드 검색의 결합 방식, 인덱스 파라미터, 필터링/리랭킹 파이프라인, 데이터 전처리가 동시에 얽혀 결과가 흔들리는 경우가 더 많습니다. 특히 Pinecone과 Milvus는 “벡터 검색”이라는 공통점이 있지만, 하이브리드 구성과 튜닝 포인트가 다르게 드러납니다.

이 글은 다음을 목표로 합니다.

  • Pinecone·Milvus에서 하이브리드 검색을 구성할 때의 핵심 튜닝 레버 이해
  • 점수 결합과 리콜·정확도 트레이드오프를 재현 가능한 방식으로 조정
  • 운영 환경에서 품질을 유지하기 위한 지표/로그/실험 방법 정리

이미 하이브리드 검색의 개념을 알고 있고, 실제 서비스에서 “왜 이 쿼리가 이상하게 나오지”를 해결해야 하는 상황을 전제로 합니다. 하이브리드 튜닝의 큰 그림은 아래 글과도 연결됩니다.

하이브리드 검색이 흔들리는 대표 원인

하이브리드 검색은 보통 vector_score(의미 유사도)와 keyword_score(BM25 등)를 결합합니다. 이때 흔한 실패 패턴은 다음과 같습니다.

1) 스코어 스케일 불일치

벡터 유사도는 코사인 유사도라면 대개 -1부터 1 범위이거나, 내부적으로 정규화된 값일 수 있습니다. BM25는 문서 길이, term frequency에 따라 스케일이 크게 달라집니다. 둘을 단순 가중합하면 특정 쪽이 압도해버립니다.

2) 후보군 리콜 부족

하이브리드는 “결합”보다 먼저 후보군 생성(retrieval) 단계가 중요합니다. 벡터 top k가 너무 작거나, Milvus에서 nprobe가 너무 낮거나, Pinecone에서 필터가 과하게 걸리면 애초에 좋은 문서가 후보군에 없습니다.

3) 전처리/토크나이징 불일치

키워드 검색은 토크나이저, 형태소 분석, 소문자화, 특수문자 처리에 민감합니다. 벡터는 임베딩 모델의 전처리 규칙에 민감합니다. 두 체계가 서로 다른 전처리를 쓰면 결합 시 “키워드로는 잘 맞는데 벡터가 엉뚱한” 또는 그 반대가 됩니다.

4) 필터 조건이 품질을 갉아먹음

권한, 테넌트, 기간, 언어 같은 메타 필터는 필수지만, 필터가 후보군을 너무 줄이면 하이브리드가 의미가 없어집니다. 특히 sparse 쪽은 필터 이후에 남는 문서 수가 급감할 수 있습니다.

튜닝의 출발점: 평가셋과 지표를 먼저 고정

튜닝을 시작하기 전에 “좋다/나쁘다”를 정량화해야 합니다.

  • 평가셋: 쿼리 N개(최소 200~500), 정답 문서(또는 정답 문서군) 라벨
  • 지표: Recall@k, MRR@k, nDCG@k, 그리고 운영 관점의 zero-hit rate, latency p95
  • 세그먼트: 짧은 쿼리, 긴 쿼리, 오타 포함, 전문용어, 다국어 등으로 분리

하이브리드 튜닝은 “전체 평균”보다 세그먼트별 붕괴를 먼저 잡는 게 효과적입니다.

점수 결합 전략: 가중합만으로는 부족하다

가장 단순한 결합은 아래처럼 가중합입니다.

score = alpha * vector + (1 - alpha) * keyword

하지만 앞서 말한 스케일 문제 때문에, 실전에서는 다음 중 하나를 권합니다.

  1. 각 스코어를 min-max 또는 z-score로 정규화 후 결합
  2. 랭크 기반 결합(Reciprocal Rank Fusion)
  3. 2단계 검색: 후보군 합집합 후 리랭커로 최종 정렬

RRF(Reciprocal Rank Fusion) 예시

RRF는 스코어 스케일 문제를 피하기 위해 “순위”만 사용합니다.

from collections import defaultdict

def rrf_fusion(vector_ids, keyword_ids, k=60, weight_v=1.0, weight_s=1.0):
    # vector_ids, keyword_ids: 정렬된 문서 id 리스트 (rank 1이 가장 상위)
    scores = defaultdict(float)

    for rank, doc_id in enumerate(vector_ids, start=1):
        scores[doc_id] += weight_v * (1.0 / (k + rank))

    for rank, doc_id in enumerate(keyword_ids, start=1):
        scores[doc_id] += weight_s * (1.0 / (k + rank))

    return sorted(scores.items(), key=lambda x: x[1], reverse=True)
  • k가 클수록 상위 랭크의 영향이 완만해집니다.
  • weight_v, weight_s로 벡터/키워드 비중을 조절합니다.

실전에서는 “짧은 쿼리”는 키워드 가중치를 높이고, “긴 자연어 질문”은 벡터 가중치를 높이는 식의 쿼리 라우팅이 효과적입니다.

Pinecone 하이브리드 튜닝 포인트

Pinecone은 관리형 서비스로 운영 부담이 낮지만, 대신 내부 인덱스 파라미터를 Milvus만큼 세밀하게 만지기 어렵습니다. 그만큼 쿼리 레벨 튜닝데이터 설계가 중요합니다.

1) 후보군 크기 조절: top_k와 필터

  • top_k를 너무 작게 잡으면 결합 이전에 리콜이 깨집니다.
  • 필터가 강할수록 top_k를 키워야 합니다.

권장 접근:

  • 벡터 검색 top_k = 200 정도로 넉넉히 뽑고
  • sparse/키워드도 유사한 규모로 후보를 확보한 뒤
  • 결합 또는 리랭킹으로 최종 k = 10~20

2) 메타데이터 필터의 “선택도”를 측정

필터가 검색을 얼마나 줄이는지(선택도)를 로그로 남기세요.

  • 필터 적용 전 후보 수
  • 필터 적용 후 후보 수

필터 후 후보가 평균적으로 30개 미만으로 떨어지면, 하이브리드 결합이 아니라 “가난한 후보군에서 억지로 정렬”하는 상태가 됩니다.

3) 하이브리드에서 자주 터지는 운영 문제: 레이턴시

하이브리드는 호출이 늘거나(벡터+키워드), 후보군이 커져 후처리가 늘어 레이턴시가 튈 수 있습니다. 서버리스/컨테이너 환경이라면 콜드스타트가 더해져 p95가 망가집니다.

Pinecone 자체 레이턴시뿐 아니라, 애플리케이션의 후처리(결합/리랭킹) 비용까지 함께 봐야 합니다.

Milvus 하이브리드 튜닝 포인트

Milvus는 인덱스와 검색 파라미터를 비교적 직접 튜닝할 수 있어, 리콜·레이턴시 트레이드오프를 정교하게 맞출 수 있습니다.

1) IVF 계열에서 핵심: nlistnprobe

IVF(예: IVF_FLAT, IVF_PQ)는 벡터를 클러스터로 나눕니다.

  • nlist: 클러스터 개수(인덱스 빌드 시)
  • nprobe: 검색 시 탐색할 클러스터 수

일반적으로:

  • nlist가 너무 작으면 근사 오차가 커지고
  • nprobe가 너무 작으면 리콜이 급락합니다.

실전 팁:

  • nprobe는 “레이턴시가 허용하는 범위에서” 단계적으로 올리며 Recall@k 곡선을 확인
  • 하이브리드에서는 벡터 후보군 리콜이 매우 중요하므로, 단일 벡터 검색보다 nprobe를 더 공격적으로 잡는 편이 안전

2) HNSW 계열에서 핵심: M, efConstruction, ef

HNSW는 그래프 기반 근사 검색입니다.

  • M: 노드 연결 수(메모리/정확도 영향)
  • efConstruction: 인덱스 빌드 품질(시간/정확도)
  • ef: 검색 시 탐색 폭(레이턴시/리콜)

하이브리드에서는 ef를 올려 벡터 후보군을 넉넉히 확보한 뒤, sparse와 결합하는 전략이 자주 이깁니다.

3) Milvus에서 “필터 + ANN” 조합 주의

메타 필터가 강하면 ANN 탐색이 비효율적이거나 결과가 빈약해질 수 있습니다. 이때는 아래 중 하나를 고려합니다.

  • 필터를 먼저 좁히지 말고, 후보를 먼저 뽑은 뒤 후필터링
  • 테넌트/권한처럼 강제 필터는 파티셔닝 전략으로 해결
  • 필터 선택도가 높은 필드는 별도 컬렉션/파티션으로 분리

실전 아키텍처: 2단계 검색 + 리랭킹이 가장 안정적

하이브리드 결합을 “한 번의 쿼리”로 끝내려 하면, 스코어 정규화/튜닝이 끝이 없습니다. 실전에서는 아래 구조가 안정적입니다.

  1. 1차 후보군: 벡터 top k1 + 키워드 top k2합집합
  2. 2차 리랭킹: Cross-Encoder 또는 LLM 기반 리랭커로 top k 선정

간단한 리랭킹 파이프라인 예시

def hybrid_retrieve(query, vector_db, keyword_engine, k1=200, k2=200, k=20):
    v_hits = vector_db.search(query, top_k=k1)      # [(id, score_v, text, meta), ...]
    s_hits = keyword_engine.search(query, top_k=k2) # [(id, score_s, text, meta), ...]

    # 후보 합집합
    cand = {}
    for h in v_hits:
        cand[h[0]] = {"id": h[0], "text": h[2], "meta": h[3], "sv": h[1], "ss": 0.0}
    for h in s_hits:
        if h[0] not in cand:
            cand[h[0]] = {"id": h[0], "text": h[2], "meta": h[3], "sv": 0.0, "ss": h[1]}
        else:
            cand[h[0]]["ss"] = h[1]

    # 1차 결합(예: 정규화 전 임시로 RRF)
    fused = rrf_fusion(
        [x[0] for x in v_hits],
        [x[0] for x in s_hits],
        k=60,
        weight_v=1.0,
        weight_s=1.0,
    )

    # fused 상위 N개만 리랭커에 투입
    top_ids = [doc_id for doc_id, _ in fused[:100]]
    rerank_input = [cand[i] for i in top_ids]

    # 2차 리랭킹(여기서는 더미)
    reranked = sorted(rerank_input, key=lambda x: (x["sv"] + x["ss"]), reverse=True)
    return reranked[:k]

리랭커를 붙이면 하이브리드 결합의 부담이 크게 줄어듭니다. 대신 비용/레이턴시가 늘 수 있으니, 상위 50~200개만 리랭킹하는 식으로 절충합니다.

쿼리 라우팅: 모든 쿼리에 하이브리드를 쓰지 말기

하이브리드는 만능이지만, 항상 이기지는 않습니다. 다음 규칙 기반 라우팅이 실전에서 효과적입니다.

  • 쿼리 길이 <= 2 토큰이고, 제품명/에러코드/약어가 많으면 키워드 비중을 높임
  • 자연어 질문(의문문, 조사/어미 포함)이면 벡터 비중을 높임
  • 숫자/버전/식별자가 포함되면 키워드를 반드시 포함

예시:

import re

def route(query: str) -> str:
    has_id_like = bool(re.search(r"\b\d+(\.\d+)+\b|\b[A-Z]{2,}\d+\b", query))
    tokens = query.strip().split()

    if has_id_like or len(tokens) <= 2:
        return "keyword_heavy"
    return "vector_heavy"

라우팅을 넣으면 평균 레이턴시를 낮추면서도, “짧은 쿼리에서 하이브리드가 오히려 망가지는” 케이스를 줄일 수 있습니다.

데이터 측면 튜닝: 청크, 중복, 메타 설계

하이브리드 품질은 인덱스 파라미터만으로 해결되지 않습니다.

1) 청크 크기와 오버랩

  • 너무 작은 청크: 키워드에는 걸리지만 의미가 빈약해 벡터가 흔들림
  • 너무 큰 청크: 벡터는 평균화되어 둔해지고, 키워드는 잡음이 늘어남

권장: 문서 성격에 따라 다르지만, 기술 문서는 대체로 300~800 토큰 범위에서 시작해 실험합니다.

2) 중복 제거

동일/유사 청크가 많으면 하이브리드에서 상위 결과가 “비슷한 문장만 반복”되는 현상이 생깁니다.

  • 해시 기반(정규화 텍스트 SHA)
  • 미니해시/SimHash
  • 임베딩 코사인 유사도로 near-duplicate 제거

3) 메타데이터는 필터뿐 아니라 랭킹 피처로

예: 최신 문서 가산점, 공식 문서 가산점, 클릭 로그 기반 가산점.

단, 가산점도 스코어 스케일 문제를 만들 수 있으니, 정규화 후 작은 범위로 적용합니다.

운영 체크리스트: 문제를 빨리 찾는 로그 설계

하이브리드 검색은 디버깅이 어렵습니다. 최소한 아래를 구조화 로그로 남기면 원인 추적이 빨라집니다.

  • 쿼리 원문, 라우팅 결과
  • 벡터 top k1의 상위 5개 id와 점수
  • 키워드 top k2의 상위 5개 id와 점수
  • 결합 후 상위 10개 id
  • 필터 조건과 필터 후 후보 수
  • 레이턴시 분해: embedding, vector search, keyword search, fuse, rerank

RAG 파이프라인이라면, 리트리벌 품질 저하가 곧 LLM 호출 재시도/타임아웃 증가로 이어질 수 있습니다. 외부 API 재시도 정책까지 같이 점검해야 운영이 안정됩니다.

Pinecone vs Milvus: 튜닝 전략 요약

  • Pinecone

    • 강점: 운영 단순, 스케일링 편함
    • 튜닝 포인트: 후보군 크기, 필터 선택도, 결합/리랭킹, 데이터 설계
    • 권장: 2단계(후보 합집합 + 리랭킹)로 안정화
  • Milvus

    • 강점: 인덱스/검색 파라미터를 세밀하게 제어
    • 튜닝 포인트: IVF nprobe, HNSW ef, 파티셔닝/필터 전략
    • 권장: 리콜을 먼저 확보하고(파라미터 상향), 결합/리랭킹으로 정밀도 보정

마무리: “결합”보다 “후보군”과 “평가 루프”가 먼저

하이브리드 검색 튜닝에서 가장 큰 착각은 alpha만 만지면 해결된다는 믿음입니다. 실전에서는 다음 순서가 재현성과 효율이 좋습니다.

  1. 평가셋·지표 고정
  2. 벡터/키워드 각각의 후보군 리콜 확보(특히 필터 조건 하에서)
  3. RRF 또는 정규화 결합으로 1차 안정화
  4. 필요할 때만 리랭커로 마무리
  5. 운영 로그로 세그먼트 붕괴를 조기 탐지

이 루프를 만들면 Pinecone이든 Milvus든, 하이브리드 품질을 “감”이 아니라 “실험”으로 올릴 수 있습니다.