Dense / Sparse Retrieval
문서 검색의 두 가지 기본 접근법인 Dense Retrieval(벡터 검색)과 Sparse Retrieval(키워드 검색)을 비교합니다.
Dense Retrieval (벡터 검색)
텍스트를 밀집 벡터(dense vector)로 변환하여 의미적 유사도로 검색합니다.
코드 예제
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(
collection_name="documents",
embedding_function=embeddings,
)
# 문서 추가
vectorstore.add_documents(chunks)
# Dense Retrieval
results = vectorstore.similarity_search(
query="LLM의 환각 문제를 해결하는 방법",
k=4,
)
# 유사도 점수와 함께 검색
results_with_scores = vectorstore.similarity_search_with_score(
query="LLM의 환각 문제를 해결하는 방법",
k=4,
)
for doc, score in results_with_scores:
print(f"유사도: {score:.4f} | {doc.page_content[:80]}")
임베딩 모델 비교
| 모델 | 차원 | 다국어 | 비용 | 특징 |
|---|
text-embedding-3-small | 1536 | O | $0.02/1M tokens | OpenAI, 범용 |
text-embedding-3-large | 3072 | O | $0.13/1M tokens | OpenAI, 고성능 |
BAAI/bge-m3 | 1024 | O | 무료 (로컬) | 다국어 최강, 한국어 우수 |
jhgan/ko-sroberta-multitask | 768 | 한국어 | 무료 (로컬) | 한국어 특화 |
유사도 메트릭
| 메트릭 | 설명 | 값 범위 | 벡터 DB 기본값 |
|---|
| Cosine Similarity | 벡터 방향 유사도 | -1 ~ 1 | Chroma, Qdrant |
| Dot Product (내적) | 벡터 크기 + 방향 | -∞ ~ ∞ | Milvus |
| Euclidean Distance | 벡터 간 거리 | 0 ~ ∞ | - |
장단점
| 장점 | 단점 |
|---|
| 의미적 유사도 포착 | 키워드 정확 매칭에 약함 |
| 동의어/패러프레이즈 처리 | 임베딩 모델 품질에 의존 |
| 다국어 검색 가능 | 고유명사, 코드 검색에 부적합 |
| 짧은 쿼리에서도 효과적 | 임베딩 비용 발생 |
Sparse Retrieval (키워드 검색)
단어의 출현 빈도와 희귀성을 기반으로 검색합니다.
BM25
from langchain_community.retrievers import BM25Retriever
# BM25 Retriever 생성
bm25_retriever = BM25Retriever.from_documents(
documents=chunks,
k=4,
)
# 키워드 기반 검색
results = bm25_retriever.invoke("RAPTOR 트리 구조")
BM25 파라미터
| 파라미터 | 설명 | 기본값 | 효과 |
|---|
k1 | 용어 빈도 포화도 | 1.2~2.0 | 높을수록 빈도 가중치 증가 |
b | 문서 길이 정규화 | 0.75 | 0=무시, 1=완전 정규화 |
k | 반환 문서 수 | 4 | 검색 결과 수 |
TF-IDF
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# TF-IDF 벡터화
texts = [doc.page_content for doc in chunks]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(texts)
# 쿼리 검색
query_vector = vectorizer.transform(["RAG 인덱싱 과정"])
similarities = cosine_similarity(query_vector, tfidf_matrix).flatten()
# 상위 K개 결과
top_k_indices = np.argsort(similarities)[-4:][::-1]
results = [chunks[i] for i in top_k_indices]
Sparse Retrieval 장단점
| 장점 | 단점 |
|---|
| 키워드 정확 매칭 | 동의어/패러프레이즈 미처리 |
| 고유명사, 코드 검색에 강함 | 의미적 유사도 무시 |
| 임베딩 모델 불필요 | 다국어 처리 제한 |
| 빠른 속도 | 짧은 쿼리에서 성능 저하 |
Dense vs Sparse 비교
| 항목 | Dense Retrieval | Sparse Retrieval |
|---|
| 검색 기준 | 의미적 유사도 | 키워드 매칭 |
| 동의어 처리 | O | X |
| 정확 키워드 매칭 | 약함 | 우수 |
| 고유명사 검색 | 약함 | 우수 |
| 다국어 | O (모델 의존) | 제한적 |
| 속도 | 보통 | 빠름 |
| 비용 | 임베딩 비용 | 무료 |
| 인프라 | 벡터 DB 필요 | 인메모리 가능 |
사용 시나리오
| 시나리오 | 추천 | 이유 |
|---|
| 일반 Q&A | Dense | 의미 검색이 중요 |
| 기술 문서 검색 | Sparse → Hybrid | 정확한 용어 매칭 필요 |
| 코드 검색 | Sparse | 함수명, 변수명 정확 매칭 |
| 다국어 검색 | Dense | 다국어 임베딩 모델 활용 |
| 프로토타이핑 | Sparse (BM25) | 임베딩 없이 빠르게 시작 |
| 프로덕션 | Hybrid | 두 장점을 결합 |
단일 전략으로는 Dense Retrieval이 대부분의 경우에서 더 나은 성능을 보입니다. 하지만 프로덕션에서는 Dense + Sparse를 결합한 Hybrid Search가 가장 안정적입니다.