6

Hybrid Search, 이렇게 하면 망한다

Day 2: Hybrid Search

학습 목표
  • Hybrid Search 구현 시 자주 발생하는 실수를 파악한다
  • 각 실수에 대한 해결책을 설명할 수 있다
  • Min-Max 정규화의 부작용과 RRF가 그것을 피하는 이유를 설명할 수 있다

Hybrid Search, 이렇게 하면 망한다


실수 1: 점수 정규화를 안 한다

# 잘못된 코드
bm25_score = 12.5        # BM25 스케일: 0 ~ 수십
semantic_score = 0.87     # Cosine 스케일: 0 ~ 1

# 그냥 합산하면?
hybrid = 0.5 * semantic_score + 0.5 * bm25_score
# = 0.5 * 0.87 + 0.5 * 12.5 = 6.685
# BM25가 지배적! Semantic은 영향력 없음

해결: Min-Max 정규화

def normalize(scores):
    min_s, max_s = min(scores), max(scores)
    if max_s == min_s:
        return [1.0] * len(scores)
    return [(s - min_s) / (max_s - min_s) for s in scores]

# 정규화 후: 둘 다 0~1 범위
bm25_norm = 0.95    # 최고점 대비 95%
semantic_norm = 0.87

hybrid = 0.5 * semantic_norm + 0.5 * bm25_norm  # = 0.91 (공정한 합산)

실수 2: alpha를 고정한다

# 잘못된 코드
def search(query, alpha=0.5):  # 항상 0.5
    return hybrid_search(query, alpha)

# "E-4521 에러" -> alpha=0.5? BM25가 더 잘 찾는 쿼리인데!
# "장비 고장 원인" -> alpha=0.5? Semantic이 더 잘 찾는 쿼리인데!

해결: 쿼리 유형별 적응형 alpha

def get_alpha(query):
    if has_code_pattern(query):   # E-4521, ISO 9001
        return 0.3  # BM25 가중
    if has_numeric_query(query):  # 몇 도, 얼마
        return 0.4  # 약간 BM25
    return 0.7  # Semantic 가중 (기본)

실수 3: 토크나이징 불일치

# 잘못된 코드
# 인덱싱 시: "CNC-M500" -> ["CNC", "M500"]
# 검색 시:  "CNC-M500" -> ["CNC-M500"]
# 토큰이 다르니 BM25가 매칭 실패!

해결: 동일한 토크나이저 사용

# 인덱싱과 검색에서 동일 함수 사용
tokenizer = manufacturing_tokenize  # 하나만!
corpus = [tokenizer(doc) for doc in documents]
query_tokens = tokenizer(query)  # 같은 함수!

실수 4: 검색 후보 수가 부족

# 잘못된 코드
bm25_results = search_bm25(query, top_k=5)      # 5개만!
semantic_results = search_semantic(query, top_k=5) # 5개만!

# 합치면 최대 10개, 겹치면 더 적음
# 좋은 문서를 놓칠 확률 높음

해결: initial_k를 final_k의 4~10배로

final_k = 5
initial_k = final_k * 4  # 최소 20개 후보

bm25_results = search_bm25(query, top_k=initial_k)
semantic_results = search_semantic(query, top_k=initial_k)
# 넉넉한 후보에서 Hybrid로 정렬 후 5개 선택

실수 5: 메타데이터 필터를 안 쓴다

# 잘못된 코드
# "CNC-M500 에러 코드"를 검색하면 모든 장비의 에러 코드가 나옴
# CNC-M500 문서만 보고 싶은데!

해결: 사전 필터링 — 단, 색인할 때 그 키를 넣어야 한다

# 1) 색인 시점에 equipment_code 를 메타데이터에 넣는다.
#    이것을 빼먹으면 아래 필터는 언제나 0건을 낸다.
collection.add(
    documents=[doc["content"]],
    metadatas=[{"type": "error", "equipment_code": "CNC-M500"}],
    ids=[doc["id"]],
)

# 2) 질의 시점에 같은 키로 거른다
import re
equipment = re.search(r'[A-Z]+-[A-Z0-9]+', query)
if equipment:
    results = collection.query(
        query_texts=[query],
        where={"equipment_code": equipment.group()},
        n_results=top_k
    )

색인에 없는 키로 필터를 걸면 오류가 나지 않고 결과가 0건으로 조용히 비어 나온다. 필터를 도입할 때는 반드시 색인 스키마부터 확인하라.


실수 6: Min-Max 정규화의 부작용을 모른다

실수 1의 해결책이 그대로 새 함정이 된다.

# 정규화는 각 검색기의 최고점을 항상 1.0, 최저점을 항상 0.0 으로 만든다
def normalize(scores):
    min_s, max_s = min(scores), max(scores)
    return [(s - min_s) / (max_s - min_s) for s in scores]

문제: BM25 가 아무것도 못 찾은 질의를 생각해 보자. 원점수가 0.4, 0.3, 0.2 처럼 전부 낮아도 정규화를 거치면 그중 1등이 1.0 을 받는다. 그 1.0 이 alpha 가중합에 그대로 들어가 semantic 결과를 밀어낸다. 정규화는 상대 순위를 보존하지만 「이 검색기가 애초에 쓸 만한 것을 찾았는가」라는 정보를 지운다.

해결 두 가지

# (1) 최고 원점수가 바닥이면 그 검색기를 아예 빼거나 가중치를 줄인다
if max(bm25_raw_scores) < MIN_USEFUL_BM25:
    alpha = 1.0   # 이번 질의는 semantic 만 쓴다

# (2) 순위 기반 결합(RRF)으로 간다 — 점수 스케일 문제 자체가 없어진다

Score Fusion vs RRF: 언제 뭘 쓸까?

기준Score FusionRRF
장점alpha로 세밀 조절 가능점수 스케일 문제가 없다, 파라미터 튜닝 불필요
단점정규화 필수, 위 실수 6에 취약점수 차이(격차)를 반영하지 않음
적합쿼리 유형이 예측 가능하고 튜닝할 데이터가 있을 때쿼리가 다양하거나 튜닝할 평가셋이 아직 없을 때
제조 추천평가셋을 갖춘 뒤 적응형 alpha와 함께기본값. 순위 기반이라 스케일에 강하다

RRF 가 검색 시스템의 실무 기본값이 된 이유가 바로 정규화 문제에 강하다는 점이다. 「프로토타이핑용」이 아니다. Score Fusion 은 alpha 를 재서 정할 수 있게 된 뒤에 쓴다.


체크리스트

  • 점수 정규화 (Min-Max) 적용
  • 쿼리 유형별 적응형 alpha 설정
  • 인덱싱과 검색에 동일 토크나이저 사용
  • initial_k를 final_k의 4배 이상으로 설정
  • 메타데이터 필터 활용 (색인 스키마에 그 키가 있는지 먼저 확인)
  • 정규화 후 1.0 이 "잘 찾았다"를 뜻하지 않는다는 것을 코드에서 다루었는가
AI로 학습하기 — 꿀팁
Hybrid Search 구현 실수 점검AI 학습 팁

Hybrid Search에서 BM25와 벡터 점수 스케일 차이, alpha 설정 오류, RRF 파라미터 오용 등이 없는지 점검해 보세요.

Hybrid Search 구현 시 흔한 실수에 대한 아래 주장이 맞는지 검증해줘. (1) 'BM25 점수와 코사인 유사도 점수는 같은 스케일이므로 가중 평균을 바로 적용해도 된다.' (2) 'alpha=0.5는 BM25와 벡터 검색을 동등하게 사용하는 완벽히 균형 잡힌 설정이다.' (3) 'RRF 공식에서 k=60은 임의값이며 도메인에 따라 1~1000 범위에서 자유롭게 조정한다.' (4) 'Hybrid Search는 항상 벡터 단독 검색보다 Recall이 높다.' 제조 RAG 구현 맥락으로 교정해줘.
이 팁이 도움이 됐나요?
용어