사용자가 "what happens if someone lies to get money(누가 돈을 얻으려고 거짓말을 하면 어떻게 될까)"라고 입력하고, 실제로는 이 사건을 다루는 법조문인 "Section 420 IPC(인도 형법 제420조)"를 찾고자 합니다. 키워드 검색(keyword search)은 두 문장 사이에 공유 어휘(shared vocabulary)가 없으므로 이 문서를 완전히 놓칩니다. 의미 기반 검색(semantic search)도 임베딩(embedding)이 법률 텍스트(legal text)로 학습되지 않았다면 마찬가지로 놓치게 됩니다. 실제 검색 시스템은 이 두 가지 실패 양상을 모두 다뤄야 합니다.
정보 검색(Information Retrieval; IR)은 모든 검색 증강 생성(Retrieval-Augmented Generation; RAG) 시스템과 모든 검색창, 그리고 문서 사이트(docs site)의 퍼지 조회(fuzzy lookup) 아래에 깔려 있는 파이프라인입니다. 2026년의 프로덕션 환경(production)에서 잘 작동하는 아키텍처는 단일 방법이 아니라, 서로의 실패를 잡아 주는 상호 보완적인 방법들의 연쇄(complementary method chain)입니다.
이 강의에서는 각 구성 요소를 직접 만들어 보고, 각 단계가 어떤 실패를 잡아 주는지 이름을 붙여 봅니다.
사전 테스트
2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요
1.사용자가 'what happens if someone lies to get money'로 검색했는데, 관련 문서에는 'Section 420 IPC — fraud and cheating'이라고 쓰여 있습니다. BM25가 이 문서를 찾지 못하는 이유는 무엇인가요?
2.현대 검색 시스템이 희소 검색(sparse retrieval)과 밀집 검색(dense retrieval)을 하나만 쓰지 않고 결합하는 이유는 무엇인가요?
0/2 답변 완료
개념
검색 파이프라인은 네 개의 계층(layer)으로 나뉩니다. 필요한 계층만 골라서 조합하면 됩니다.
희소 검색(Sparse retrieval, BM25). 빠르고 정확한 매칭에 강하지만, 의미적 유사성에는 약합니다. 역색인(inverted index) 위에서 동작하며, 수백만 건의 문서에서도 질의당 10ms 미만으로 처리됩니다. 법조문 참조, 제품 코드, 에러 메시지, 고유명사(named entity)를 정확히 잡아 줍니다.
밀집 검색(Dense retrieval). 질의(query)와 문서(document)를 벡터(vector)로 인코딩하고, 최근접 이웃 탐색(nearest neighbor search)을 수행합니다. 표현이 다른 패러프레이즈(paraphrase)와 의미적 유사성을 잘 잡지만, 한 글자 정도만 달라지는 정확한 키워드 매칭은 놓칠 수 있습니다. FAISS나 벡터 데이터베이스(vector DB)를 쓰면 질의당 50-200ms 정도가 듭니다.
융합(Fusion). 희소 검색과 밀집 검색이 만든 순위 목록(ranked list)을 합칩니다. 상호 순위 융합(Reciprocal Rank Fusion; RRF)은 서로 척도가 다른 원시 점수(raw score)를 무시하고 순위 위치만 사용하기 때문에, 쉬운 기본값으로 가장 많이 쓰입니다. 한 신호가 특정 도메인에서 압도적으로 좋다는 사실을 알고 있다면 가중 융합(weighted fusion)도 선택지입니다.
교차 인코더 재순위화(Cross-encoder rerank). 융합 결과의 상위 30개(top-30)를 가져와, 교차 인코더(cross-encoder; 질의와 문서를 함께 넣어 한 쌍씩 점수를 매기는 모델)로 다시 점수를 매깁니다. 그리고 상위 5개(top-5)만 남깁니다. 교차 인코더는 이중 인코더(bi-encoder)보다 한 쌍을 처리하는 속도는 느리지만 훨씬 정확하며, 상위 30개에만 적용해 비용을 분산시킵니다.
3-way 검색(BM25 + 밀집 검색 + SPLADE 같은 학습형 희소 검색(learned-sparse))은 2026년 벤치마크에서 2-way 검색을 능가하지만, 학습형 희소 색인(learned-sparse index)을 운영할 인프라가 필요합니다. 대부분의 팀에게는 2-way + 교차 인코더 재순위화가 가장 좋은 균형점(sweet spot)입니다.
직접 만들기
Step 1: BM25를 처음부터 구현하기
import math
import re
from collections import Counter
TOKEN_RE = re.compile(r"[a-z0-9]+")
deftokenize(text):
return TOKEN_RE.findall(text.lower())
classBM25:
def__init__(self, corpus, k1=1.5, b=0.75):
ifnot corpus:
raise ValueError("corpus는 비어 있으면 안 됩니다")
self.corpus = [tokenize(d) for d in corpus]
self.k1 = k1
self.b = b
self.n_docs = len(self.corpus)
self.avg_dl = sum(len(d) for d inself.corpus) / self.n_docs
self.df = Counter()
for doc inself.corpus:
for term inset(doc):
self.df[term] += 1defidf(self, term):
n = self.df.get(term, 0)
return math.log(1 + (self.n_docs - n + 0.5) / (n + 0.5))
defscore(self, query, doc_idx):
q_tokens = tokenize(query)
doc = self.corpus[doc_idx]
dl = len(doc)
freq = Counter(doc)
score = 0.0for term in q_tokens:
f = freq.get(term, 0)
if f == 0:
continue
numerator = f * (self.k1 + 1)
denominator = f + self.k1 * (1 - self.b + self.b * dl / self.avg_dl)
score += self.idf(term) * numerator / denominator
return score
defrank(self, query, top_k=10):
scored = [(self.score(query, i), i) for i inrange(self.n_docs)]
scored.sort(reverse=True)
return scored[:top_k]
알아 둘 만한 파라미터(parameter)는 두 개입니다. k1=1.5는 단어 빈도 포화(term-frequency saturation)를 조절하며, 값이 클수록 같은 단어가 반복해서 등장하는 것에 더 큰 가중치를 줍니다. b=0.75는 문서 길이 정규화(length normalization)를 조절합니다. 0이면 문서 길이를 무시하고, 1이면 완전히 정규화합니다. 기본값은 원 논문의 Robertson 권장값이며, 거의 튜닝할 필요가 없습니다.
Step 2: 이중 인코더(bi-encoder)를 사용한 밀집 검색
from sentence_transformers import SentenceTransformer
import numpy as np
defbuild_dense_index(corpus, model_id="sentence-transformers/all-MiniLM-L6-v2"):
encoder = SentenceTransformer(model_id)
embeddings = encoder.encode(corpus, normalize_embeddings=True)
return encoder, embeddings
defdense_search(encoder, embeddings, query, top_k=10):
q_emb = encoder.encode([query], normalize_embeddings=True)
sims = (embeddings @ q_emb.T).flatten()
order = np.argsort(-sims)[:top_k]
return [(float(sims[i]), int(i)) for i in order]
임베딩을 L2 정규화(L2-normalize)하면 내적(dot product)이 곧 코사인 유사도(cosine similarity)와 같아집니다. all-MiniLM-L6-v2는 384차원으로 빠르고, 영어 검색 대부분에 충분히 강합니다. 다국어(multilingual) 작업에는 paraphrase-multilingual-MiniLM-L12-v2를 쓰고, 최상의 정확도가 필요하다면 bge-large-en-v1.5나 e5-large-v2를 고려합니다.
Step 3: 상호 순위 융합(Reciprocal Rank Fusion)
defreciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, (_, doc_idx) inenumerate(ranking):
scores[doc_idx] = scores.get(doc_idx, 0.0) + 1.0 / (k + rank + 1)
fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [(score, doc_idx) for doc_idx, score in fused]
상수 k=60은 RRF 원 논문에서 제시한 값입니다. k가 크면 순위 차이로 인한 기여도가 완만해지고, 작으면 상위 순위가 더 지배적인 영향을 줍니다. 60은 논문에서 공표된 기본값이며 거의 튜닝할 필요가 없습니다.
세 단계를 조합한 구조입니다. BM25는 어휘적 매칭(lexical match)을 찾고, 밀집 검색은 의미적 매칭(semantic match)을 찾으며, RRF는 점수 보정(score calibration) 없이 두 순위를 병합합니다. 교차 인코더는 상위 30개의 질의-문서 쌍을 함께 보면서 이중 인코더가 놓친 세밀한 관련성(fine-grained relevance)을 잡아 줍니다. 그리고 최종적으로 상위 5개만 남깁니다.
Step 5: 평가(Evaluation)
지표
의미
Recall@k
정답 문서가 존재하는 질의들 중에서, 정답이 상위 k개 안에 들어오는 비율입니다.
MRR(Mean Reciprocal Rank)
첫 번째 관련 문서의 순위(rank)의 역수를 평균한 값입니다.
nDCG@k
단순 이진 관련성이 아니라 관련성의 등급(relevance gradation)까지 반영합니다.
특히 RAG에서는 검색기의 Recall@k가 가장 중요한 수치입니다. 검색된 집합(retrieved set) 안에 올바른 구절(passage)이 없다면, 답변기(reader)는 그것을 답할 수 없기 때문입니다.
디버깅 팁: 실패한 질의에 대해서는 희소 순위와 밀집 순위를 비교(diff)해 봅니다. 한쪽만 정답 문서를 찾는다면, 어휘 불일치(vocabulary mismatch)거나 의미적 모호성(semantic ambiguity)입니다. 전자는 빠진 쪽의 신호를 더해 주는 것으로 고치고, 후자는 더 나은 임베딩이나 재순위화 모델로 고칩니다.
하이브리드를 지원하는 Qdrant / Weaviate / Vespa / Milvus. 상위 30개에 교차 인코더 재순위화를 적용합니다.
최고 품질
3-way (BM25 + 밀집 검색 + SPLADE) + ColBERT 후기 상호작용(late-interaction) 재순위화
무엇을 고르든 평가에 예산을 할당해야 합니다. End-to-end RAG 정확도를 측정하기 전에 검색 재현율(retrieval recall)을 먼저 벤치마크합니다. 답변기는 검색기가 놓친 것을 고쳐 줄 수 없습니다.
2026년 프로덕션 RAG에서 얻은 교훈
RAG 실패의 80%는 모델이 아니라 인제스트(ingestion)와 청크 분할(chunking)에서 비롯됩니다. 팀은 LLM을 바꾸고 프롬프트(prompt)를 튜닝하느라 몇 주를 보내지만, 그 사이 검색 단계는 세 질의에 한 번꼴로 조용히 잘못된 컨텍스트를 반환하고 있습니다. 청크 분할부터 먼저 고쳐야 합니다.
청크 크기보다 청크 분할 전략이 더 중요합니다. 고정 크기 분할(fixed-size split)은 표, 코드, 중첩된 헤더를 망가뜨립니다. 문장 단위 분할(sentence-aware)이 기본값이고, 기술 문서나 제품 매뉴얼에는 의미 기반 분할(semantic chunking)이나 LLM 기반 분할(LLM-based chunking)이 효과를 발휘합니다.
부모-문서 패턴(Parent-doc pattern). 정밀도를 위해 작은 "자식" 청크를 검색합니다. 같은 부모 섹션에서 여러 자식 청크가 함께 잡히면, 컨텍스트 보존을 위해 부모 블록(parent block)으로 교체해 넣어 줍니다. 이 패턴은 재학습 없이도 답변 품질을 꾸준히 끌어올려 줍니다.
k_rerank=3이 보통 최적입니다. 그 이상은 토큰 비용과 생성 지연만 늘리고 답변 품질을 끌어올리지는 못합니다. 만약 k=8이 여전히 k=3보다 낫다면, 그것은 재순위화 모델이 부족하다는 신호입니다.
HyDE / 질의 확장(query expansion). 질의로부터 가상의 답변(hypothetical answer)을 만들어 그것을 임베드하고 검색합니다. 짧은 질문과 긴 문서 사이의 표현 격차(phrasing gap)를 메워 주며, 추가 학습 없이 정밀도를 끌어올릴 수 있는 방법입니다.
컨텍스트 예산은 8K 토큰 이하로. 이 한계에 자꾸 부딪힌다면, 재순위화의 임계값(threshold)이 너무 느슨하다는 뜻입니다.
모든 것을 버전 관리합니다. 프롬프트, 청크 분할 규칙, 임베딩 모델, 재순위화 모델 모두입니다. 어느 한쪽이라도 드리프트(drift)가 발생하면 답변 품질이 조용히 무너집니다. 충실성(faithfulness), 컨텍스트 정밀도(context precision), 답변 불가율(unanswered-question rate)을 CI 게이트(gate)로 두어, 사용자에게 회귀(regression)가 노출되기 전에 차단합니다.
3-way 검색(BM25 + 밀집 + SPLADE 같은 학습형 희소)이 2-way를 능가합니다. 2026년 벤치마크 기준이며, 특히 고유명사와 의미가 섞인 질의에서 더욱 두드러집니다. 인프라가 SPLADE 색인을 지원할 때 도입하면 좋습니다.
좋은 검색 설계는 2026년 산업계 측정 기준으로 환각(hallucination)을 70-90% 줄여 줍니다. 대부분의 RAG 성능 향상은 모델 파인튜닝이 아니라 더 나은 검색에서 나옵니다.
산출물 만들기
outputs/skill-retrieval-picker.md로 저장합니다.
---
name: retrieval-picker
description: Pick a retrieval stack for a given corpus and query pattern.
version: 1.0.0
phase: 5
lesson: 14
tags: [nlp, retrieval, rag, search]
---
Given requirements (corpus size, query pattern, latency budget, quality bar, infra constraints), output:
Guide the student in Korean.
1. Stack. BM25 only, dense only, hybrid (BM25 + dense + RRF), hybrid + cross-encoder rerank, or three-way (BM25 + dense + learned-sparse).
2. Dense encoder. Name the specific model. Match to language(s), domain, and context length.
3. Reranker. Name the specific cross-encoder model if used. Flag that rerank adds 30-100ms latency on top-30.
4. Evaluation plan. Recall@10 is the primary retriever metric. MRR for multi-answer. Baseline first, incremental improvements measured against it.
Refuse to recommend dense-only for corpora with named entities, error codes, or product SKUs unless the user has evidence dense handles exact matches. Refuse to skip reranking for high-stakes retrieval (legal, medical) where the final top-5 decides the user's answer.
연습문제
쉬움. 500개 문서로 이루어진 코퍼스(corpus)에서 위의 hybrid_search를 구현합니다. 질의 20개로 BM25만 사용한 경우, 밀집 검색만 사용한 경우, 그리고 하이브리드의 Recall@5를 비교합니다.
중간. MRR 계산을 추가합니다. 정답 문서를 알고 있는 각 테스트 질의에 대해, BM25, 밀집 검색, 하이브리드 순위에서 정답 문서의 순위(rank)를 찾아내고 각각의 MRR을 보고합니다.
어려움. Sentence Transformers의 MultipleNegativesRankingLoss를 사용해 자신의 도메인에 맞게 밀집 인코더를 파인튜닝(fine-tune)합니다. 500개의 질의-문서 쌍으로 학습 데이터를 만들고, 파인튜닝 전후의 재현율을 비교합니다.
핵심 용어
용어
흔한 설명
실제 의미
BM25
키워드 검색
Okapi BM25. 단어 빈도(term frequency), IDF, 문서 길이로 문서에 점수를 매깁니다.