한 줄 정리
BM25와 PGroonga는 “층위가 다른 개념”임. BM25는 “점수 매기는 공식”, PGroonga는 “검색 엔진 전체 패키지”. 비교 대상 자체가 다름.
키워드 검색 엔진의 3단계 구조
1단계: 토큰화(Tokenization) → 텍스트를 쪼갬
2단계: 인덱싱(Indexing) → 역인덱스(Inverted Index, 토큰→문서 매핑표) 구축
3단계: 스코어링(Scoring) → 검색 결과에 점수 매기기
각 단계는 서로 독립적인 역할임. PGroonga는 1단계부터 3단계까지 전부 포함된 “풀 패키지”, BM25는 3단계에만 꽂히는 “알고리즘 한 조각”.
PGroonga 내부 구성
| 단계 | PGroonga의 선택 |
|---|---|
| 토큰화 | N-gram (기본 Bigram, 한국어도 무난) |
| 인덱싱 | Groonga 엔진의 역인덱스 |
| 스코어링 | TF (Term Frequency) 중심, BM25는 아님 |
다른 검색 시스템 비교
| 검색 시스템 (식당 비유) | 토큰화 (재료 손질) | 인덱싱 (냉장고 정리) | 스코어링 (레시피) |
|---|---|---|---|
| PGroonga | N-gram | Groonga 역인덱스 | TF |
| Elasticsearch | 형태소 분석 (한국어는 nori) | Lucene 역인덱스 | BM25 |
| pg_search (ParadeDB) | 설정 가능 | Tantivy 역인덱스 | BM25 |
| LanceDB FTS | 설정 가능 | Tantivy 역인덱스 | BM25 |
한마디 요약: PGroonga는 “엔진 한 덩어리”, BM25는 “엔진 안에 꽂히는 부품”.
역인덱스(Inverted Index)의 동작
키워드 검색 엔진은 텍스트를 쌓아두기만 하는 게 아니라, 책 뒷면 “찾아보기” 같은 역인덱스를 만들어 둠. 질문 들어오면 거기서 역으로 끌어냄.
생성 단계
- 토큰화(Tokenization): 문장을 토큰(단어 단위) 으로 쪼갬
- 정규화(Normalization): 대소문자 통합, 어간 추출(Stemming, 단어의 뿌리만 남기기), 불용어(Stopword, “은/는/이/가” 같은 검색 가치 없는 단어) 제거
- 역매핑 기록: 각 토큰이 어느 문서의 어느 위치에 있는지 저장
코드 구조로 감 잡기
docs = {
1: "AI is powerful",
2: "AI agents are future"
}
# 역인덱스 (Inverted Index) 형태
inverted_index = {
"AI": [1, 2], # 두 문서 모두에 등장
"powerful": [1], # 문서 1 전용
"future": [2], # 문서 2 전용
}N-gram 토큰화 예시 (“주택담보대출”)
Bigram(2글자씩 자르기)으로 쪼개면:
"주택담보대출" → "주택", "택담", "담보", "보대", "대출"
역인덱스에 위치(position) 까지 같이 저장됨:
"주택" → [문서1(pos:0), 문서3(pos:42), 문서7(pos:15)]
"택담" → [문서1(pos:1), 문서3(pos:43)]
"담보" → [문서1(pos:2), 문서3(pos:44), 문서7(pos:16)]
"보대" → [문서1(pos:3), 문서3(pos:45)]
"대출" → [문서1(pos:4), 문서3(pos:46), 문서5(pos:8)]
위치까지 같이 저장된 덕분에 “주택담보대출”이 연속으로 붙어 있는 문서만 골라낼 수 있음. 문서1과 문서3은 위치가 0→1→2→3→4로 연속이라 후보, 문서7은 “주택”과 “담보”는 있지만 “택담”, “보대”가 없어 탈락.
스코어링: TF vs TF-IDF vs BM25
같은 역인덱스에서 후보를 찾았어도, 누구를 1등으로 올릴지는 스코어링 공식이 결정함.
TF (Term Frequency)
가장 단순한 방식. “질문 단어가 문서에 몇 번 등장하는가”.
TF(term, doc) = term이 doc에 등장한 횟수
- 문제: 긴 문서가 무조건 유리해짐 (단어 많으면 카운트 올라가니)
- “이”, “그”, “것” 같은 흔한 단어가 점수를 지배
TF-IDF
TF에 IDF(Inverse Document Frequency, 역문서빈도) 를 곱해서 흔한 단어 패널티를 줌.
IDF(term) = log(전체 문서 수 / term이 등장한 문서 수)
TF-IDF = TF × IDF
- “는”, “이” 처럼 모든 문서에 깔린 단어는 IDF가 작아 점수 기여도 낮음
- 문서 길이 보정이 약함
BM25 (Best Matching 25)
TF-IDF의 “문서 길이 보정”과 “TF 포화(Saturation, 일정 이상 등장하면 점수 상승 둔화)“를 정교하게 다듬은 확률론 기반 공식. 2025년 기준 상용 검색 엔진(Elasticsearch, Lucene, Tantivy) 사실상 표준.
공식 (하나의 질의어 $q_i$ 기여분):
$$ \text{BM25}(D, Q) = \sum_{q_i \in Q} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)} $$
| 기호 | 의미 |
|---|---|
| $f(q_i, D)$ | 문서 D에서 질의어 $q_i$가 등장한 횟수 |
| $ | D |
| $\text{avgdl}$ | 전체 문서의 평균 길이 |
| $k_1$ | TF 포화 파라미터 (보통 1.2 ~ 2.0). 높을수록 “더 자주 나올수록 점수 많이 올라감” |
| $b$ | 길이 정규화 파라미터 (보통 0.75). 1에 가까울수록 긴 문서 패널티 강함 |
TF/TF-IDF/BM25 비교표
| 구분 | TF | TF-IDF | BM25 |
|---|---|---|---|
| 등장 횟수 반영 | ○ | ○ | ○ (포화 곡선) |
| 흔한 단어 패널티 | ✗ | ○ | ○ |
| 문서 길이 보정 | ✗ | 약함 | 강함 (b 파라미터) |
| 튜닝 여지 | 없음 | 거의 없음 | k1, b 조절 가능 |
| 대표 엔진 | PGroonga | 구형 Solr | Elasticsearch, Lucene, Tantivy |
한마디 요약: BM25는 “단어 많이 나오면 점수 올리되 과하게는 안 올리고, 긴 문서라고 날로 먹지는 못하게 막는” 개량판 TF-IDF.
PGroonga vs pg_bigm: PostgreSQL 위의 한국어 검색 두 라이벌
PostgreSQL 환경에서 한국어 전문 검색을 걸려면 거의 둘 중 하나. 둘 다 “N-gram 기반”이라 헷갈림.
| 구분 | PGroonga | pg_bigm |
|---|---|---|
| 내부 엔진 | Groonga (독립된 검색엔진) | PostgreSQL GIN 인덱스 위에 얇게 얹음 |
| 인덱스 크기 | 상대적으로 큼 | 상대적으로 작음 |
| 검색 속도 | 빠름 (자체 엔진 최적화) | 중간 |
| 복잡 질의 | 자체 쿼리 언어 지원 | LIKE 연산자 위주 |
| 설치 난이도 | 외부 패키지 의존성 있음 | 비교적 가벼움 |
| 스코어링 | TF 중심 (score 함수 제공) | 점수 없음 (필터 용도) |
선택 기준: “랭킹이 필요한가”가 갈림길. 필요하면 PGroonga, LIKE 필터 대체 용도면 pg_bigm.
하이브리드 검색에서 BM25가 끼는 자리
pgvector 혼자서는 “정확한 키워드 매칭”이 약함. “RTX 4090” 같은 고유 명사를 벡터 유사도로 찾으면 엉뚱한 “그래픽카드” 일반 문서를 끌고 올 수 있음. 그래서 BM25(또는 PGroonga 같은 키워드 엔진)로 후보를 한 번 더 걸러내거나 점수를 섞음.
[하이브리드 검색 흐름]
사용자 질문
├─→ 벡터 검색 (pgvector <=>) → 후보 Top-50
└─→ 키워드 검색 (BM25 or PGroonga) → 후보 Top-50
│
└─→ RRF(Reciprocal Rank Fusion) 등으로 점수 합산
→ 최종 Top-10을 LLM에 전달
관련: 벡터유사도에 관하여, retriever_추천, RAG_rangchain 이외에 다른 방식
결론
- 비교 대상이 다름: PGroonga(엔진 전체) vs BM25(스코어링 알고리즘)
- BM25는 TF-IDF 개량판: 포화와 길이 정규화 두 개가 핵심
- 한국어 전문 검색: PostgreSQL에선 PGroonga가 사실상 1티어
- RAG에 쓰려면: 키워드 엔진 + 벡터 검색 하이브리드가 거의 표준