제공해주신 소스 문서를 바탕으로 요청하신 작성 규칙, frontmatter 스키마, 출처 표기 및 템플릿 구조를 엄격히 준수하여 BM25 개념에 대한 위키 노트를 작성 완료했습니다.
위키 노터 파일은 다음 위치에 생성되었습니다:
작성된 위키 노트 요약 및 내용
---
type: tool
status: draft
core: false
tags:
- llm
- rag
- search
- retrieval
- bm25
aliases:
- BM25Okapi
sources:
- 'raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md'
created: 2026-08-27
updated: 2026-08-27
---
# BM25
## 한 줄 정의
BM25는 문서 내 키워드의 출현 빈도(TF)와 역문서 빈도(IDF)를 기반으로 쿼리와의 연관성을 계산하여, 벡터 검색의 의미적 유사도 검색이 놓치는 정확한 키워드 매칭(Exact Matching)을 보완하는 대표적인 전통적 텍스트 검색 랭킹 알고리즘이다.
## 핵심 요지
- **정확한 어구 및 전문 용어 매칭**: 법률, 의료, 사내 약어, 정확한 숫자/조건 식별 등 의미적 유사성만으로는 구별하기 어려운 키워드 일치 검색에 강점을 가진다 (`raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md`).
- **하이브리드 검색(Hybrid Search)의 필수 구성 요소**: 임베딩 기반 시맨틱 검색(Semantic Search)과 결합하여 점수를 정규화한 뒤 알파(\(\alpha\)) 가중치로 합산함으로써, 의미적 연관성과 키워드 정밀도를 동시에 확보하는 3단계 RAG 구축 기법의 핵심으로 활용된다 (`raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md`).
- **알파 조율(Alpha Tuning)**: 도메인 특화 용어나 키워드 중심 질의에서는 BM25의 비중을 높이고(알파값 하향), 자연어 문장 질의에서는 시맨틱 비중을 높여(알파값 상향) 가중치를 유연하게 제어한다 (`raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md`).
## 상세
[[RAG]] 검색 시스템에서 순수 벡터 임베딩 검색(Naive RAG)만을 사용할 경우, "5년 이상 근속한 직원의 PTO 정책"과 같이 특정 키워드("5+ years", "tenure")나 정밀한 숫자가 포함된 쿼리에서 무관한 개념 청크를 검색하는 한계가 발생한다 (`raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md`). BM25는 질의 단어의 토큰화 기반 개별 일치 점수를 산출하여 이러한 시맨틱 검색의 허점을 보완한다.
하이브리드 리트리버 구현 시, BM25의 점수는 0 이상의 임의 범위를 가질 수 있으므로 최대 점수로 나누어 0~1 사이로 정규화(Normalization)한 후 시맨틱 점수와 유클리드/코사인 정규화 결합을 수행한다 (`raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md`).
## 예시
Python의 `rank_bm25` 라이브러리의 `BM25Okapi` 클래스를 활용하여 하이브리드 리트리버를 구현하는 예시 코드이다 (`raw/5단계 난이도로 알아보는 RAG 시스템 구축 및 구현 가이드.md`).
```python
from rank_bm25 import BM25Okapi
import numpy as np
from openai import OpenAI
client = OpenAI()
class HybridRetriever:
def __init__(self, documents: list[str]):
self.documents = documents
self.embeddings = self._embed_all(documents)
# BM25를 위한 토큰화 및 인덱싱
tokenized = [doc.lower().split() for doc in documents]
self.bm25 = BM25Okapi(tokenized)
def _embed_all(self, docs: list[str]) -> list[list[float]]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=docs
)
return [d.embedding for d in response.data]
def search(self, query: str, k: int = 5, alpha: float = 0.5) -> list[str]:
# 1. 시맨틱 점수 계산 및 정규화
q_emb = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
sem_scores = np.dot(self.embeddings, q_emb)
sem_scores = (sem_scores - sem_scores.min()) / (sem_scores.max() - sem_scores.min() + 1e-8)
# 2. BM25 점수 계산 및 정규화
bm25_scores = np.array(self.bm25.get_scores(query.lower().split()))
if bm25_scores.max() > 0:
bm25_scores = bm25_scores / bm25_scores.max()
# 3. 결합: alpha 값으로 시맨틱 vs 키워드 가중치 조절
combined = alpha * sem_scores + (1 - alpha) * bm25_scores
top_k = np.argsort(combined)[::-1][:k]
return [self.documents[i] for i in top_k]충돌
현재 소스 문서상에서 발견되거나 충돌되는 다른 주장은 없음.