제공해주신 소스 문서를 바탕으로 요청하신 작성 규칙, 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]

충돌

현재 소스 문서상에서 발견되거나 충돌되는 다른 주장은 없음.

관련 노트