byteforce

CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API

RAG

BM25 어휘 검색

BM25 lexical search

시맨틱 검색은 강력하지만 코너케이스가 있습니다. 드물고 정확한 식별자(예: incident 2023)를 물으면 엉뚱한 섹션이 끼어들죠. 해법은 고전적 어휘 검색(lexical search)을 나란히 돌리는 것 — 그 표준 알고리즘이 BM25(Best Match 25)입니다.

전체 내레이션영상 나레이션 한국어 번역 (전체)

Stephen Grider · Anthropic 기술 스태프

RAG 파이프라인의 첫 버전을 다 만들었습니다. 지금은 잘 도는 것 같지만, 곧 검색 결과가 늘 최선은 아니라는 걸 알게 됩니다. 예를 보겠습니다. report.md의 소프트웨어 공학 섹션을 보면 INC(Incident의 약자) 2023 Q4-011 이라는 표현이 그 문단 안에 세 번 나옵니다. 문서를 더 내려가면 Section 10 사이버보안 분석에도 등장합니다 — 헤더에 한 번, 문단 안에 한 번.

이 식별자 "incident 2023 Q4-011"로 검색하면 시맨틱 검색이 무엇을 돌려줄까요? 노트북의 사용자 질의를 "what happened with incident 2023"으로 바꾸고 전체 셀을 다시 돌립니다. 결과가 조금 뜻밖입니다. 첫 결과는 Section 10 — 이 incident를 다루는 섹션이니 좋습니다. 그런데 그다음 결과가 놀랍게도 Section 3 재무분석입니다. Section 3을 열어 보면 그 incident는 어디에도 언급되지 않습니다. 우리가 원한 건 Section 10 다음 Section 2(소프트웨어 공학)였는데, 받은 건 Section 10 다음 엉뚱한 Section 3이었습니다.

시맨틱 검색 기법은 대부분 아주 잘 동작하지만, 이렇게 기대대로 안 되는 코너케이스가 있습니다. 그래서 결과를 개선할 기법을 봅니다. 전략은 이렇습니다. 사용자가 질문하면 그 질문을 시맨틱 검색(임베딩+벡터DB)에 넣는 동시에, 병렬로 별도의 어휘 검색 시스템도 돌립니다. 어휘 검색은 고전적 텍스트 검색에 가깝습니다 — 질문을 개별 단어로 쪼개고, 그 단어들이 든 청크를 찾습니다. 두 시스템의 결과를 모은 뒤 병합하면, 의미적 측면과 평문 검색 측면이 한 결과 집합에 균형 있게 담깁니다.

어휘 검색을 구현하는 방법은 무수히 많지만, 우리가 만드는 것 같은 RAG 파이프라인에서 아주 흔히 보이는 방법이 BM25입니다 — Best Match 25의 약자죠. 알고리즘을 고수준으로 훑어보겠습니다(작은 단계 몇 개는 단순화를 위해 생략).

모든 것은 사용자 질의에서 시작합니다. "a incident 2023 Q4-011" 같은 검색 문자열을 넣었다고 합시다. 1단계 토큰화: 질의를 개별 조각으로 쪼갭니다. 지금은 아주 단순한 방법 — 구두점을 제거하고 공백으로 나눕니다. 그러면 "a", "incident", "2023" 같은 항이 나옵니다. 2단계 빈도: 각 항이 전체 청크들에 얼마나 자주 나오는지 셉니다. 청크가 둘뿐이라 하면, "a"는 다 합쳐 다섯 번, "incident 2023"은 한 번 같은 식으로요.

3단계 상대적 중요도: 사용 빈도로 각 항에 중요도를 줍니다. "a"는 여기저기 다섯 번 쓰여 별로 중요하지 않다고 봅니다. 반면 "incident 2023"은 아주 드물게 쓰여 검색 중요도가 더 큽니다. 4단계: 높은 가중치를 받은 항을 더 많이 쓴 청크를 상위로 올립니다.

노트북 004_bm25에서는 BM25Index 클래스가 제공됩니다. 단계는 시맨틱과 같습니다 — 섹션으로 청킹하고, 청크를 스토어에 추가하고, 검색합니다. 같은 질의로 검색하면 이번엔 소프트웨어 공학 → 사이버보안 → 방법론 순서로 나옵니다. 드문 핵심어 "incident 2023"을 가장 많이 쓴 섹션을 제대로 우선합니다. "what happened with" 같은 흔한 항은 원문 곳곳에 쓰이니 그만큼 가중하지 않습니다.

이제 두 검색 시스템이 생겼습니다 — 시맨틱 검색과, 좀 더 고전적인 텍스트 검색. 두 스토어의 구현은 API가 거의 같습니다 — 둘 다 add_documentsearch 함수를 가집니다. 다음 레슨에서 이 둘을 병합합니다. 사용자가 질의를 내면 두 시스템 모두에 전달해 결과를 받고, 그 둘을 합쳐 양쪽의 장점을 모두 취합니다.

이 장에서 배우는 것What you'll learn

약 7분
1

시맨틱 검색의 코너케이스 — 드문 식별자에 엉뚱한 섹션이 끼어듦

2

해법: 어휘 검색을 병렬로 — 질문을 단어로 쪼개 그 단어가 든 청크를 찾음

3

BM25(Best Match 25) — RAG에서 가장 흔한 어휘 검색 알고리즘

4

4단계: 토큰화 → 빈도 → 희소어 가중↑ → 고가중어 많은 청크 우선

5

BM25IndexVectorIndex같은 API(add_document·search)

6

같은 질의 결과: 소프트웨어 공학 → 사이버보안 → 방법론

먼저 짚고 갈 용어
어휘 검색 (lexical search)
질문을 단어로 쪼개 그 단어가 든 청크를 찾는 고전적 텍스트 검색. 의미가 아닌 표면 일치를 본다.
BM25 (Best Match 25)
어휘 검색의 표준 랭킹 함수. 단어 빈도와 희소성으로 청크 점수를 매긴다.
토큰화 (tokenization)
질의·문서를 단어 단위로 쪼개는 일. 여기선 구두점 제거 후 공백으로 분리.
IDF (역문서빈도)
드문 단어일수록 큰 가중치. incident 2023처럼 희소한 항이 검색을 좌우하게 한다.
k1 · b
BM25 파라미터. k1=빈도 포화, b=문서 길이 정규화 정도. 기본 1.5 · 0.75.

시맨틱이 흔들리는 곳

A surprising corner case

소프트웨어 공학 섹션과 사이버보안 섹션 모두 식별자 INC-2023-Q4-011을 언급합니다. 이걸 시맨틱 검색에 물으면 Section 10은 잘 찾지만, 다음 자리에 엉뚱하게 Section 3 재무분석이 끼어듭니다 — 그 incident가 한 번도 안 나오는 섹션인데도요.

코너케이스 · 같은 질의, 두 가지 검색
질의 "what happened with incident 2023 Q4-011" — 원하는 결과는 Section 10 + Section 2

시맨틱은 Section 3 재무분석을 잘못 끼워 넣습니다 — 거기엔 그 incident가 한 번도 안 나오는데도요. BM25는 드문 식별자 incident 2023을 실제로 쓴 섹션을 우선합니다.

해법 · 어휘 검색을 병렬로

Lexical search in parallel

질문을 시맨틱 검색에 넣는 동시에, 별도의 어휘 검색도 돌립니다. 어휘 검색은 질문을 단어로 쪼개 그 단어가 든 청크를 찾습니다. 두 결과를 병합하면 의미적 측면과 평문 검색 측면이 한데 모입니다. 표준 알고리즘이 BM25입니다.

BM25 4단계

① 토큰화 구두점 제거·공백 분리 → ② 빈도 각 항이 전체 청크에 몇 번 나오나 → ③ 희소어 가중↑ 드문 항일수록 중요 → ④ 우선순위 고가중어를 많이 쓴 청크를 상위로.

BM25 4단계 · 항별 빈도와 가중치
토큰화 결과: aincident2023q4011
검색 항전체 청크 내 가중치점수
흔한 항 a · what · with → 가중치 낮음희소 항 incident · 2023 · q4 · 011 → 가중치 높음
청크 A · 재무분석
Quarterly revenue rose as a result of a stronger asset mix ...
고가중어 적중 0 · 0.4
청크 B · 소프트웨어 공학
Resolved incident INC-2023-Q4-011 after three cycles ...
고가중어 적중 4 · 8.1

한 항을 클릭하면 그 항이 어느 청크를 끌어올리는지 보입니다. 고가중어를 더 많이 쓴 청크 B가 상위로 올라갑니다.

BM25Index 구현

The BM25Index class

노트북에서 BM25Index 클래스가 제공됩니다. 핵심은 토큰화기와 _calculate_idf — 희소어일수록 큰 가중치를 줍니다. 공개 API는 시맨틱 스토어와 똑같이 add_document·search입니다.

004_bm25.ipynb · BM25Index (k1=1.5, b=0.75)
import re, math

class BM25Index:
    def __init__(self, k1=1.5, b=0.75, tokenizer=None):
        self.k1 = k1                       # 단어 빈도 포화 정도
        self.b = b                         # 문서 길이 정규화 정도
        self.tokenizer = tokenizer or self._default_tokenizer
        self.documents = []

    def _default_tokenizer(self, text):
        text = text.lower()                # 소문자화
        tokens = re.split(r"\W+", text)   # 구두점 제거 · 공백 분리
        return [t for t in tokens if t]

    def _calculate_idf(self, term, N, df):
        # 희소어일수록 idf↑ — 드문 단어가 검색을 좌우
        return math.log(((N - df + 0.5) / (df + 0.5)) + 1)

    def add_document(self, document):   # 시맨틱 스토어와 동일 API
        ...
    def search(self, query_text, k=1):  # BM25 점수로 상위 k
        ...

사용 단계도 시맨틱과 같습니다 — 섹션으로 청킹하고, 청크를 스토어에 추가하고, 시맨틱이 틀렸던 바로 그 질의로 검색합니다.

004_bm25.ipynb · 청킹 → 추가 → 검색
with open("./report.md", "r") as f:
    text = f.read()

# 1. 섹션으로 청킹
chunks = chunk_by_section(text)

# 2. BM25 스토어 생성 + 청크 추가
store = BM25Index()
for chunk in chunks:
    store.add_document({"content": chunk})

# 3. 검색 — 시맨틱이 틀렸던 바로 그 질의
results = store.search("what happened with incident 2023 Q4-011", 3)

for doc, distance in results:
    print(distance)
    print(doc["content"][:200])
    print("\n----\n")
출력 · 상위 3개 (copy 없음)
## Section 2: Software Engineering — Project Phoenix
The engineering team resolved incident INC-2023-Q4-011 after
three regression cycles, restoring the payment service ...

----
## Section 10: Cybersecurity Analysis
Incident INC-2023-Q4-011 originated from an exposed staging
endpoint; containment completed within 4 hours ...

----
## Methodology
Each domain team followed a shared incident-review protocol ...

정리 & 점검

Recap & check
핵심 정리
  • 시맨틱 검색은 강력하지만 드문 식별자에서 코너케이스가 생긴다.
  • 해법은 어휘 검색(BM25)을 병렬로 돌려 두 결과를 병합하는 것.
  • BM25: 토큰화 → 빈도 → 희소어 가중↑(IDF) → 고가중어 많은 청크 우선.
  • BM25IndexVectorIndex같은 API(add_document·search) — 다음 레슨에서 병합한다.

Q1시맨틱 검색의 코너케이스란 어떤 상황인가요?

Q2BM25에서 드문 단어(예: incident 2023)는 어떻게 다뤄지나요?

Q3BM25IndexVectorIndex와 같은 API로 만든 이유는?

MEMBER SESSION REQUIRED · REGISTRATION IS FREE

여기부터는 등록한 분에게 열립니다.

전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.

등록하고 이어서 읽기

이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.