앙상블 검색기(Ensemble Retriever)

검색기

문서 검색을 위한 다양한 Retriever 패턴과 최적화 기법을 다룬다.

AI
RAG
LangChain
Agent
저자

Kwangmin Kim

공개

2024년 05월 03일

EnsembleRetriever는 여러 검색기를 결합하여 더 강력한 검색 결과를 제공하는 LangChain의 기능이다. 다양한 검색 알고리즘의 장점을 활용하여 단일 알고리즘보다 더 나은 성능을 달성할 수 있으며, 사용자들 사이에서 높은 호평을 받고 있다. 다수의 리트리버를 사용하여 나온 결과를 Reciprocal Rank Fusion(RRF) 알고리즘으로 재순위화한 후 하나의 통합 검색 결과를 LLM의 입력으로 제공한다.

주요 특징

  1. 여러 검색기 통합: 다양한 유형의 검색기를 입력으로 받아 결과를 결합한다.

  2. 결과 재순위화 (Reciprocal Rank Fusion): RRF 알고리즘을 사용하여 각 검색기의 결과를 단순 합집합이 아닌 지능형 순위 매김을 통해 통합한다.

    • 각 검색기들이 검색한 유사도 높은 문서들을 단순히 합치지 않고 다시 순위를 매긴다
    • Agent 개발자들 사이에서는 EnsembleRetriever(LangChain 용어)보다 RRF(일반 통용 용어)를 훨씬 더 일반적으로 사용한다
    • 논문에서 거의 모든 경우에 RRF가 더 좋은 성능을 보임을 보여준다
    • 실제 적용에서 RRF의 효과는 비슷한 특성의 리트리버를 결합할 때보다 다양한 특성의 리트리버를 결합할 때 극대화된다
  3. 하이브리드 검색: 주로 sparse retriever(예: BM25)와 dense retriever(예: 임베딩 유사도)를 결합하여 사용한다.

각 검색기의 장점

이러한 상호 보완적인 특성으로 인해 EnsembleRetriever는 다양한 검색 시나리오에서 향상된 성능을 제공한다.

자세한 내용은 LangChain 공식 문서를 참조하자.

1 환경 설정

# API 키를 환경변수로 관리하기 위한 설정
from dotenv import load_dotenv

# API 키 정보 로드
load_dotenv()
# LangSmith 추적을 설정합니다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력
logging.langsmith("CH10-Retriever")

2 기본 사용법: 두 개의 검색기 결합

EnsembleRetriever를 초기화하여 BM25RetrieverFAISS 검색기를 결합한다. 각 검색기는 고유한 검색 방식을 사용한다: - 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”에서 찾아 반환함.

Subscribe

Enjoy this blog? Get notified of new posts by email: