한 줄 정리

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는 아님

다른 검색 시스템 비교

검색 시스템 (식당 비유)토큰화 (재료 손질)인덱싱 (냉장고 정리)스코어링 (레시피)
PGroongaN-gramGroonga 역인덱스TF
Elasticsearch형태소 분석 (한국어는 nori)Lucene 역인덱스BM25
pg_search (ParadeDB)설정 가능Tantivy 역인덱스BM25
LanceDB FTS설정 가능Tantivy 역인덱스BM25

한마디 요약: PGroonga는 “엔진 한 덩어리”, BM25는 “엔진 안에 꽂히는 부품”.


역인덱스(Inverted Index)의 동작

키워드 검색 엔진은 텍스트를 쌓아두기만 하는 게 아니라, 책 뒷면 “찾아보기” 같은 역인덱스를 만들어 둠. 질문 들어오면 거기서 역으로 끌어냄.

생성 단계

  1. 토큰화(Tokenization): 문장을 토큰(단어 단위) 으로 쪼갬
  2. 정규화(Normalization): 대소문자 통합, 어간 추출(Stemming, 단어의 뿌리만 남기기), 불용어(Stopword, “은/는/이/가” 같은 검색 가치 없는 단어) 제거
  3. 역매핑 기록: 각 토큰이 어느 문서의 어느 위치에 있는지 저장

코드 구조로 감 잡기

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 비교표

구분TFTF-IDFBM25
등장 횟수 반영○ (포화 곡선)
흔한 단어 패널티
문서 길이 보정약함강함 (b 파라미터)
튜닝 여지없음거의 없음k1, b 조절 가능
대표 엔진PGroonga구형 SolrElasticsearch, Lucene, Tantivy

한마디 요약: BM25는 “단어 많이 나오면 점수 올리되 과하게는 안 올리고, 긴 문서라고 날로 먹지는 못하게 막는” 개량판 TF-IDF.


PGroonga vs pg_bigm: PostgreSQL 위의 한국어 검색 두 라이벌

PostgreSQL 환경에서 한국어 전문 검색을 걸려면 거의 둘 중 하나. 둘 다 “N-gram 기반”이라 헷갈림.

구분PGroongapg_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에 쓰려면: 키워드 엔진 + 벡터 검색 하이브리드가 거의 표준