검색 (Retrieval)
검색은 RAG 파이프라인의 두 번째 단계로, 사용자 질문과 관련된 문서 청크를 벡터 데이터베이스에서 찾아오는 과정입니다. 검색 품질이 곧 답변 품질을 결정합니다.
검색 파이프라인
벡터 검색 (Dense Retrieval)
벡터 검색은 쿼리와 문서를 동일한 임베딩 공간에 매핑한 뒤, 벡터 간 거리 로 관련성을 판단합니다.
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,
persist_directory = "./chroma_db" ,
)
# 기본 유사도 검색
results = vectorstore.similarity_search(
query = "RAG의 검색 단계에서 가장 중요한 것은?" ,
k = 4 ,
)
for doc in results:
print ( f "[ { doc.metadata.get( 'source' , 'unknown' ) } ] { doc.page_content[: 100 ] } ..." )
유사도 메트릭
벡터 간 유사도를 측정하는 세 가지 주요 메트릭입니다.
Cosine Similarity
Euclidean Distance (L2)
Dot Product (내적)
두 벡터의 방향 이 얼마나 유사한지를 측정합니다. 크기(길이)에 영향을 받지 않아 텍스트 검색에 가장 널리 사용됩니다. cosine ( A , B ) = A ⋅ B ∥ A ∥ ∥ B ∥ \text{cosine}(A, B) = \frac{A \cdot B}{\|A\| \|B\|} cosine ( A , B ) = ∥ A ∥∥ B ∥ A ⋅ B
범위 : -1 ~ 1 (1에 가까울수록 유사)
적합한 경우 : 문서 길이가 다양할 때, 정규화된 임베딩
# Chroma 기본값
vectorstore = Chroma(
collection_name = "docs" ,
embedding_function = embeddings,
collection_metadata = { "hnsw:space" : "cosine" },
)
두 벡터 간 직선 거리 를 측정합니다. 거리가 짧을수록 유사합니다. L2 ( A , B ) = ∑ i = 1 n ( A i − B i ) 2 \text{L2}(A, B) = \sqrt{\sum_{i=1}^{n}(A_i - B_i)^2} L2 ( A , B ) = ∑ i = 1 n ( A i − B i ) 2
범위 : 0 ~ ∞ (0에 가까울수록 유사)
적합한 경우 : 벡터 크기가 의미 있을 때
vectorstore = Chroma(
collection_name = "docs" ,
embedding_function = embeddings,
collection_metadata = { "hnsw:space" : "l2" },
)
두 벡터의 방향과 크기 모두를 고려합니다. 정규화된 벡터에서는 Cosine Similarity와 동일합니다. dot ( A , B ) = ∑ i = 1 n A i × B i \text{dot}(A, B) = \sum_{i=1}^{n} A_i \times B_i dot ( A , B ) = ∑ i = 1 n A i × B i
범위 : -∞ ~ ∞ (클수록 유사)
적합한 경우 : 정규화된 임베딩, 빠른 계산이 필요할 때
vectorstore = Chroma(
collection_name = "docs" ,
embedding_function = embeddings,
collection_metadata = { "hnsw:space" : "ip" }, # inner product
)
메트릭 비교
메트릭 방향 고려 크기 고려 계산 속도 권장 사용 Cosine O X 보통 기본 선택 (대부분의 경우)Euclidean (L2) O O 보통 클러스터링, 이상치 탐지 Dot Product O O 빠름 정규화된 임베딩, 대규모 검색
대부분의 경우 Cosine Similarity 를 기본으로 사용하세요. OpenAI, Cohere 등 대부분의 임베딩 모델이 정규화된 벡터를 반환하므로, Cosine과 Dot Product 결과가 동일합니다.
검색 파라미터
Top-K
검색 결과에서 상위 K개의 문서를 반환합니다.
# top-k 설정
retriever = vectorstore.as_retriever(
search_kwargs = { "k" : 4 }, # 상위 4개 반환
)
K 값 장점 단점 K=2~3 노이즈 최소, 빠른 응답 관련 정보 누락 가능 K=4~6 균형 잡힌 선택 - K=8~10 포괄적 정보 수집 노이즈 증가, 토큰 소비 K=10+ 최대 커버리지 비용 증가, 성능 저하
Score Threshold
유사도 점수가 임계값 이상인 문서만 반환합니다.
retriever = vectorstore.as_retriever(
search_type = "similarity_score_threshold" ,
search_kwargs = {
"score_threshold" : 0.7 , # 유사도 0.7 이상만
"k" : 10 , # 최대 10개
},
)
MMR (Maximal Marginal Relevance)
관련성과 다양성 을 동시에 고려합니다. 중복된 내용의 문서를 줄이고 다양한 관점을 포함합니다.
retriever = vectorstore.as_retriever(
search_type = "mmr" ,
search_kwargs = {
"k" : 4 ,
"fetch_k" : 20 , # 후보 20개에서
"lambda_mult" : 0.5 , # 관련성 vs 다양성 균형 (0=다양성, 1=관련성)
},
)
검색 결과 확인
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,
persist_directory = "./chroma_db" ,
)
# 유사도 점수와 함께 검색
results = vectorstore.similarity_search_with_score(
query = "RAG에서 검색 품질을 높이는 방법은?" ,
k = 4 ,
)
for doc, score in results:
print ( f "[Score: { score :.4f} ] { doc.page_content[: 80 ] } ..." )
print ( f " Source: { doc.metadata.get( 'source' , 'N/A' ) } " )
print ()
메타데이터 필터링
메타데이터를 활용하면 검색 범위를 좁혀 정확도를 높일 수 있습니다.
# 특정 소스의 문서만 검색
results = vectorstore.similarity_search(
query = "인덱싱 방법" ,
k = 4 ,
filter = { "source_type" : "pdf" },
)
# 여러 조건 결합 (Chroma)
results = vectorstore.similarity_search(
query = "인덱싱 방법" ,
k = 4 ,
filter = {
"$and" : [
{ "source_type" : { "$eq" : "pdf" }},
{ "word_count" : { "$gte" : 100 }},
]
},
)
검색 후처리 기초
검색된 문서를 LLM에 전달하기 전에 간단한 후처리를 수행할 수 있습니다.
def post_process_results ( results , min_score = 0.5 , max_docs = 4 ):
"""검색 결과 후처리"""
# 1. 낮은 유사도 필터링
filtered = [(doc, score) for doc, score in results if score >= min_score]
# 2. 중복 내용 제거
seen_content = set ()
unique = []
for doc, score in filtered:
content_hash = hash (doc.page_content[: 200 ])
if content_hash not in seen_content:
seen_content.add(content_hash)
unique.append((doc, score))
# 3. 최대 문서 수 제한
return unique[:max_docs]
검색 전략의 상세 비교 (Dense, Sparse, Hybrid 등)는 검색 전략 페이지에서, 재순위화 기법은 재순위화 페이지에서 다룹니다.