EnsembleRetriever는 여러 검색기를 결합하여 더 강력한 검색 결과를 제공하는 LangChain의 기능이다. 다양한 검색 알고리즘의 장점을 활용하여 단일 알고리즘보다 더 나은 성능을 달성할 수 있으며, 사용자들 사이에서 높은 호평을 받고 있다. 다수의 리트리버를 사용하여 나온 결과를 Reciprocal Rank Fusion(RRF) 알고리즘으로 재순위화한 후 하나의 통합 검색 결과를 LLM의 입력으로 제공한다.
주요 특징
여러 검색기 통합: 다양한 유형의 검색기를 입력으로 받아 결과를 결합한다.
결과 재순위화 (Reciprocal Rank Fusion): RRF 알고리즘을 사용하여 각 검색기의 결과를 단순 합집합이 아닌 지능형 순위 매김을 통해 통합한다.
- 각 검색기들이 검색한 유사도 높은 문서들을 단순히 합치지 않고 다시 순위를 매긴다
- Agent 개발자들 사이에서는 EnsembleRetriever(LangChain 용어)보다 RRF(일반 통용 용어)를 훨씬 더 일반적으로 사용한다
- 논문에서 거의 모든 경우에 RRF가 더 좋은 성능을 보임을 보여준다
- 실제 적용에서 RRF의 효과는 비슷한 특성의 리트리버를 결합할 때보다 다양한 특성의 리트리버를 결합할 때 극대화된다
하이브리드 검색: 주로
sparse retriever(예: BM25)와dense retriever(예: 임베딩 유사도)를 결합하여 사용한다.
각 검색기의 장점
- Sparse Retriever (BM25): 키워드 기반 검색에 효과적 → 사용자의 정확한 용어에 민감하게 반응
- Dense Retriever (임베딩 기반): 의미적 유사성 기반 검색에 효과적 → 의도와 맥락을 이해한 검색
이러한 상호 보완적인 특성으로 인해 EnsembleRetriever는 다양한 검색 시나리오에서 향상된 성능을 제공한다.
자세한 내용은 LangChain 공식 문서를 참조하자.
1 환경 설정
2 기본 사용법: 두 개의 검색기 결합
EnsembleRetriever를 초기화하여 BM25Retriever와 FAISS 검색기를 결합한다. 각 검색기는 고유한 검색 방식을 사용한다: - BM25Retriever: 키워드 기반 검색 (sparse search) - FAISS: 임베딩 기반 의미 검색 (dense search)
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
# 샘플 문서 데이터셋 (Apple 관련 문서들)
doc_list = [
"I like apples",
"I like apple company",
"I like apple's iphone",
"Apple is my favorite company",
"I like apple's ipad",
"I like apple's macbook",
]
# BM25 Retriever 초기화 (키워드 기반 검색)
bm25_retriever = BM25Retriever.from_texts(doc_list)
bm25_retriever.k = 1 # 검색 결과 개수
# FAISS Retriever 초기화 (의미 기반 검색)
embedding = OpenAIEmbeddings()
faiss_vectorstore = FAISS.from_texts(doc_list, embedding)
faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 1})
# Ensemble Retriever 생성 (가중치: BM25 70%, FAISS 30%)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, faiss_retriever],
weights=[0.7, 0.3], # BM25에 더 높은 가중치 부여
)2.1 예제 1: 키워드 기반 검색에 유리한 쿼리
ensemble_retriever 객체를 사용하여 관련 문서를 검색한다. 각 검색기의 결과를 비교하면 가중치의 영향을 확인할 수 있다.
# 검색 실행
query = "my favorite fruit is apple"
ensemble_result = ensemble_retriever.invoke(query)
bm25_result = bm25_retriever.invoke(query)
faiss_result = faiss_retriever.invoke(query)
# 결과 출력
print("[Ensemble Retriever 결과]")
for doc in ensemble_result:
print(f"Content: {doc.page_content}")
print()
print("[BM25 Retriever 결과]")
for doc in bm25_result:
print(f"Content: {doc.page_content}")
print()
print("[FAISS Retriever 결과]")
for doc in faiss_result:
print(f"Content: {doc.page_content}")
print()[Ensemble Retriever 결과]
I like apples
[BM25 Retriever 결과]
I like apples
[FAISS Retriever 결과]
Apple is my favorite company
결과 해석: - BM25는 정확한 키워드 매칭(“apple”과 “fruit”)으로 “I like apples” 문서를 찾음 - FAISS는 의미 유사성으로 “favorite”과 관련된 “Apple is my favorite company” 문서를 찾음 - Ensemble은 BM25 가중치(0.7)가 높아서 BM25 결과를 우선함
2.2 예제 2: 의미 검색이 더 유용한 복잡한 쿼리
더 자연스러운 문장 형태의 쿼리로 두 검색 방식의 차이를 관찰한다.
# 복잡한 쿼리 실행
query = "Apple company makes my favorite iphone"
ensemble_result = ensemble_retriever.invoke(query)
bm25_result = bm25_retriever.invoke(query)
faiss_result = faiss_retriever.invoke(query)
# 결과 출력
print("[Ensemble Retriever 결과]")
for doc in ensemble_result:
print(f"Content: {doc.page_content}")
print()
print("[BM25 Retriever]")
for doc in bm25_result:
print(f"Content: {doc.page_content}")
print()
print("[FAISS Retriever 결과]")
for doc in faiss_result:
print(f"Content: {doc.page_content}")
print()[Ensemble Retriever 결과]
Apple is my favorite company
I like apple's iphone
[BM25 Retriever 결과]
Apple is my favorite company
[FAISS Retriever 결과]
I like apple's iphone
결과 해석: - BM25는 “Apple”과 “company”라는 정확한 키워드에만 반응 → 한 문서만 반환 - FAISS는 “favorite”과 “iphone”의 의미 관계를 이해 → 더 정확한 문서 검색 - Ensemble은 두 결과를 결합 → 다양한 관점에서의 정보 제공 - 실제 사용에서는 의미 검색의 가중치를 높이는 것이 복잡한 쿼리에서 더 효과적
3 고급 기능: 런타임에 가중치 동적 변경
ConfigurableField를 사용하면 프로그램 실행 중(런타임)에 각 검색기의 가중치를 동적으로 변경할 수 있다. 이는 다양한 쿼리 유형에 대응하는 유연한 검색 시스템을 구축할 수 있다는 장점이 있다.
3.1 동적 가중치 설정 구성
from langchain_core.runnables import ConfigurableField
# ConfigurableField를 사용한 동적 가중치 설정
ensemble_retriever = EnsembleRetriever(
# 리트리버 목록을 설정합니다. 여기서는 bm25_retriever와 faiss_retriever를 사용합니다.
retrievers=[bm25_retriever, faiss_retriever],
).configurable_fields(
weights=ConfigurableField(
id="ensemble_weights", # 외부에서 참조할 식별자
name="Ensemble Weights",
description="BM25와 FAISS 검색기의 가중치 비율 (합계=1.0)",
)
)- 검색 시
config매개변수를 통해 검색 설정을 지정ensemble_weights옵션의 가중치를 [1, 0]으로 설정하여 모든 검색 결과의 가중치가 BM25 retriever 에 더 많이 부여 되도록
# BM25 전용 검색 설정
config = {"configurable": {"ensemble_weights": [1, 0]}}
# 동일한 쿼리로 검색 실행
docs = ensemble_retriever.invoke("my favorite fruit is apple", config=config)
print("[BM25만 사용 (가중치 [1.0, 0.0])]")
for i, doc in enumerate(docs, 1):
print(f"{i}. {doc.page_content}")[BM25만 사용 (가중치 [1.0, 0.0])]
1. I like apples
결과 해석: BM25만 사용할 때는 정확한 키워드 매칭만 이루어짐. “apple”과 “fruit”을 포함한 문서만 반환됨.
3.2 케이스 2: FAISS 가중치 극대화 (의미 기반 검색)
FAISS 검색기에 전체 가중치(1.0)를 할당하여 의미 기반 검색만 수행한다.
# FAISS 전용 검색 설정
config = {"configurable": {"ensemble_weights": [0, 1]}}
# 동일한 쿼리로 검색 실행
docs = ensemble_retriever.invoke("my favorite fruit is apple", config=config)
print("[FAISS만 사용 (가중치 [0.0, 1.0])]")
for i, doc in enumerate(docs, 1):
print(f"{i}. {doc.page_content}")[FAISS만 사용 (가중치 [0.0, 1.0])]
1. Apple is my favorite company
결과 해석: FAISS만 사용할 때는 의미 유사성으로 검색. “my favorite”이라는 표현을 “Apple is my favorite company”에서 찾아 반환함.