시맨틱 검색과 키워드 검색을 결합하는 하이브리드 검색의 원리, BM25+벡터 퓨전 전략, Reciprocal Rank Fusion, 리랭커 통합, 프레임워크별 구현 방법을 다룹니다.
순수 벡터 검색(시맨틱 검색)은 강력하지만 완벽하지 않습니다. 특히 다음과 같은 경우 키워드 기반 검색이 벡터 검색보다 나은 결과를 보입니다.
반대로 키워드 검색만으로는 동의어, 패러프레이즈, 의미적 관계를 잡아내지 못합니다. "자동차 수리"를 검색했을 때 "차량 정비" 관련 문서를 찾지 못하는 것이 대표적인 한계입니다.
하이브리드 검색은 두 접근의 강점을 결합합니다. 실험적으로 순수 벡터 검색 대비 recall이 1-9% 향상되는 것으로 보고되고 있습니다.
**BM25(Best Matching 25)**는 정보 검색 분야에서 수십 년간 사용된 알고리즘입니다. 문서 내 쿼리 용어의 빈도(TF), 전체 문서에서의 희소성(IDF), 문서 길이 정규화를 결합하여 관련도 점수를 계산합니다.
import math
def bm25_score(
query_terms: list[str],
document: list[str],
doc_freq: dict[str, int],
total_docs: int,
avg_doc_length: float,
k1: float = 1.2,
b: float = 0.75
) -> float:
"""BM25 점수 계산 (개념 설명용)"""
score = 0.0
doc_length = len(document)
for term in query_terms:
if term not in document:
continue
# TF: 문서 내 용어 빈도
tf = document.count(term)
# IDF: 역문서 빈도
df = doc_freq.get(term, 0)
idf = math.log((total_docs - df + 0.5) / (df + 0.5) + 1)
# BM25 공식
numerator = tf * (k1 + 1)
denominator = tf + k1 * (1 - b + b * doc_length / avg_doc_length)
score += idf * numerator / denominator
return scoreBM25의 강점은 정확한 용어 매칭과 희소성 반영입니다. 드물게 등장하는 용어일수록 높은 가중치를 받으므로, 전문 용어나 고유 명칭 검색에 효과적입니다.
벡터 검색과 BM25의 결과를 어떻게 합칠 것인가는 하이브리드 검색의 핵심 과제입니다.
RRF는 가장 널리 사용되는 퓨전 방법입니다. 각 검색 결과의 순위(rank)를 기반으로 통합 점수를 계산합니다.
RRF 점수는 각 검색 시스템에서의 순위에 상수 K를 더한 값의 역수를 합산하여 계산합니다. K는 보통 60으로 설정합니다.
def reciprocal_rank_fusion(
rankings: list[list[str]],
k: int = 60
) -> list[tuple[str, float]]:
"""RRF 구현"""
scores: dict[str, float] = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
if doc_id not in scores:
scores[doc_id] = 0.0
scores[doc_id] += 1.0 / (k + rank)
# 점수 내림차순 정렬
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
# 사용 예시
vector_results = ["doc_3", "doc_1", "doc_7", "doc_5", "doc_2"]
bm25_results = ["doc_1", "doc_4", "doc_3", "doc_8", "doc_5"]
fused = reciprocal_rank_fusion([vector_results, bm25_results])
# doc_1과 doc_3이 양쪽에서 높은 순위 -> 상위로 올라감RRF의 장점은 각 검색 시스템의 점수 스케일이 달라도 작동한다는 것입니다. 벡터 유사도(0-1)와 BM25 점수(0-무한대)를 직접 비교할 필요 없이, 순위만으로 통합합니다. 파라미터 K=60은 대부분의 경우 좋은 기본값입니다.
Weaviate에서 사용하는 방식으로, 각 검색 결과의 점수를 0-1 범위로 정규화한 뒤 가중 합산합니다.
def relative_score_fusion(
vector_results: list[tuple[str, float]],
bm25_results: list[tuple[str, float]],
alpha: float = 0.75 # 벡터 비중
) -> list[tuple[str, float]]:
"""Relative Score Fusion"""
def normalize(results):
if not results:
return {}
scores = [s for _, s in results]
min_s, max_s = min(scores), max(scores)
if max_s == min_s:
return {doc_id: 1.0 for doc_id, _ in results}
return {
doc_id: (s - min_s) / (max_s - min_s)
for doc_id, s in results
}
norm_vector = normalize(vector_results)
norm_bm25 = normalize(bm25_results)
all_docs = set(norm_vector.keys()) | set(norm_bm25.keys())
fused = {}
for doc_id in all_docs:
vec_score = norm_vector.get(doc_id, 0.0)
bm25_score = norm_bm25.get(doc_id, 0.0)
fused[doc_id] = alpha * vec_score + (1 - alpha) * bm25_score
return sorted(fused.items(), key=lambda x: x[1], reverse=True)가장 단순한 방식으로, 정규화된 점수에 가중치를 곱해 합산합니다. alpha 파라미터로 벡터/키워드 비중을 조절합니다. Pinecone과 Weaviate의 하이브리드 검색이 이 방식을 지원합니다.
하이브리드 검색의 초기 결과를 **리랭커(Reranker)**로 재순위화하면 정확도를 더욱 높일 수 있습니다. 리랭커는 쿼리와 문서 쌍을 입력받아 관련도를 정밀하게 평가합니다.
Cross-Encoder는 쿼리와 문서를 함께 입력받아 하나의 관련도 점수를 출력합니다. Bi-Encoder(임베딩 모델)보다 정확하지만, 모든 후보에 대해 개별 추론을 수행해야 하므로 느립니다. 그래서 초기 검색(retrieve)으로 후보를 좁힌 뒤 리랭킹 단계에서 사용합니다.
from sentence_transformers import CrossEncoder
# Cross-Encoder 리랭커
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
def search_with_reranking(
query: str,
initial_results: list[dict],
top_k: int = 10
) -> list[dict]:
"""하이브리드 검색 + 리랭킹 파이프라인"""
# 리랭킹: 쿼리-문서 쌍의 관련도 평가
pairs = [(query, doc["content"]) for doc in initial_results]
rerank_scores = reranker.predict(pairs)
# 리랭킹 점수로 재정렬
for doc, score in zip(initial_results, rerank_scores):
doc["rerank_score"] = float(score)
reranked = sorted(
initial_results,
key=lambda x: x["rerank_score"],
reverse=True
)
return reranked[:top_k]Cohere Rerank, Jina Reranker 등 상용 API를 사용하면 모델 호스팅 없이 리랭킹을 적용할 수 있습니다.
import cohere
co = cohere.Client("YOUR_COHERE_KEY")
results = co.rerank(
model="rerank-multilingual-v3.0",
query="벡터 데이터베이스의 HNSW 파라미터",
documents=[doc["content"] for doc in initial_results],
top_n=10
)
for result in results.results:
print(f"Index: {result.index}, Score: {result.relevance_score:.4f}")리랭커는 하이브리드 검색의 정확도를 크게 향상시키지만, 추가 지연시간과 비용이 발생합니다. 20-50개의 초기 후보를 리랭킹하는 것이 일반적이며, 100개 이상은 지연시간 대비 효과가 미미합니다.
articles = client.collections.get("Article")
response = articles.query.hybrid(
query="HNSW 인덱스 최적화",
alpha=0.75,
fusion_type="relative_score",
limit=20
)Weaviate는 BlockMax WAND 알고리즘을 사용한 효율적인 BM25 구현과 벡터 검색을 하나의 쿼리로 결합합니다. alpha 파라미터로 비중을 쉽게 조절할 수 있습니다.
Qdrant v1.9+에서는 명명된 벡터를 활용한 하이브리드 검색이 가능합니다.
from qdrant_client.models import Prefetch, FusionQuery, Fusion
# RRF 기반 하이브리드 검색
results = client.query_points(
collection_name="articles",
prefetch=[
Prefetch(
query=[0.1, 0.2, ...], # 밀집 벡터 검색
using="dense",
limit=20
),
Prefetch(
query=sparse_vector, # 희소 벡터 (BM25/SPLADE)
using="sparse",
limit=20
),
],
query=FusionQuery(fusion=Fusion.RRF), # RRF로 결합
limit=10
)# 밀집 + 희소 벡터 하이브리드 검색
results = index.query(
vector=dense_embedding, # 밀집 벡터
sparse_vector={
"indices": sparse_indices, # SPLADE 등으로 생성
"values": sparse_values
},
top_k=10
)pgvector는 네이티브 하이브리드 검색을 지원하지 않으므로, Full Text Search와 벡터 검색을 별도로 수행한 뒤 애플리케이션 레벨에서 결합해야 합니다.
-- 벡터 검색 결과
WITH vector_results AS (
SELECT id, title,
ROW_NUMBER() OVER (ORDER BY embedding <=> query_vec) AS vec_rank
FROM articles
ORDER BY embedding <=> query_vec
LIMIT 50
),
-- BM25 (tsvector) 검색 결과
text_results AS (
SELECT id, title,
ROW_NUMBER() OVER (ORDER BY ts_rank(tsv, query) DESC) AS text_rank
FROM articles
WHERE tsv @@ plainto_tsquery('korean', 'HNSW 인덱스')
LIMIT 50
)
-- RRF 결합
SELECT
COALESCE(v.id, t.id) AS id,
COALESCE(v.title, t.title) AS title,
COALESCE(1.0 / (60 + v.vec_rank), 0) +
COALESCE(1.0 / (60 + t.text_rank), 0) AS rrf_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT 10;pgvector에서 하이브리드 검색을 구현할 때, PostgreSQL의 Full Text Search는 한국어 형태소 분석이 기본 제공되지 않습니다. mecab 기반 한국어 사전을 설치하거나, 애플리케이션 레벨에서 형태소 분석 후 tsvector를 생성해야 합니다.
| 솔루션 | 하이브리드 검색 | BM25 구현 | 퓨전 방식 |
|---|---|---|---|
| Weaviate | 네이티브 | BlockMax WAND | RSF, Ranked |
| Qdrant | 네이티브 (v1.9+) | 희소 벡터 | RRF |
| Pinecone | 네이티브 | 희소 벡터 | 가중 합산 |
| Milvus | 네이티브 (v2.5+) | Sparse-BM25 | RRF, 가중 합산 |
| pgvector | 수동 구현 | 미지원 | 애플리케이션 레벨 |
| Chroma | 미지원 | - | - |
이번 장에서는 하이브리드 검색의 필요성과 구현 방법을 심층적으로 다루었습니다. 벡터 검색의 시맨틱 이해와 BM25의 키워드 매칭을 결합하면 단일 방식보다 높은 recall을 달성할 수 있으며, RRF를 비롯한 퓨전 전략과 리랭커를 통해 검색 품질을 더욱 끌어올릴 수 있습니다. 각 벡터 DB의 하이브리드 검색 구현 방식은 다르지만, 핵심 원리는 동일합니다.
다음 장에서는 메타데이터 필터링과 고급 쿼리 전략을 다룹니다. 사전/사후 필터링의 차이, 복합 필터 설계, 멀티테넌시 패턴, 성능 최적화 방법을 살펴보겠습니다.
이 글이 도움이 되셨나요?
사전 필터링과 사후 필터링의 차이, 필터 인덱스 설계, 복합 필터 조건, 지오 필터, 멀티테넌시 필터 패턴, 성능 최적화 전략을 다룹니다.
Rust 기반 고성능 벡터 엔진 Qdrant의 페이로드 필터링, 명명된 벡터, 하이브리드 배포를 분석하고, PostgreSQL 확장 pgvector의 트랜잭션 일관성과 pgvectorscale 성능을 비교합니다.
벡터 데이터베이스의 수평/수직 스케일링, 샤딩, 레플리카, 백업 전략, 모니터링 메트릭, 비용 최적화, 솔루션 선택 의사결정 트리, 마이그레이션 가이드를 다룹니다.