MultiQueryTextRetriever

MultiQueryTextRetriever

텍스트 기반 Retriever로 여러 개의 검색어를 병렬로 사용해 문서를 검색해 주는 컴포넌트예요.

출처: 문서

본문

MultiQueryTextRetriever는 여러 검색어를 병렬로 검색해 리트리벌 재현율(recall)을 높여요. 텍스트 기반 Retriever(예: InMemoryBM25Retriever)를 감싸고, 스레드 풀로 여러 검색어 문자열을 동시에 처리하죠.

이 컴포넌트는:

  • 검색어를 병렬 처리해 성능을 높여요.
  • 문서 내용을 기준으로 결과의 중복을 자동으로 제거해요.
  • 최종 결과를 관련성 점수순으로 정렬해요.

이 Retriever는 단일 사용자 검색어에서 여러 검색어 변형을 만들어 내는 QueryExpander와 함께 쓸 때 특히 효과적이에요. 이 변형들로 검색하면 원래 검색어와 다른 키워드를 쓰는 문서도 찾을 수 있어요.

문서가 사용자 검색어와 다른 단어를 쓰거나, 키워드 기반 검색(BM25)에 검색어 확장을 쓰고 싶을 때 MultiQueryTextRetriever를 쓰세요. 여러 검색어를 돌리면 시간이 더 걸리지만 max_workers를 늘려 병렬 실행하면 빨라져요.

언제 MultiQueryEmbeddingRetriever를 쓸까?

정확한 키워드 일치보다 의미가 더 중요한 시맨틱 검색이 필요하다면 MultiQueryEmbeddingRetriever를 쓰세요. 임베딩 기반 Retriever와 동작하고 Text Embedder가 필요해요.

추가 Retriever 파라미터 넘기기

retriever_kwargs를 사용해 내부 Retriever에 추가 파라미터를 넘길 수 있어요.

result = multiquery_retriever.run(
    queries=["renewable energy", "sustainable power"],
    retriever_kwargs={"top_k": 5},
)

더 알아보기 (Learn more)

단독으로 쓰기

이 예제에서는 "renewable energy", "geothermal", "hydropower" 세 개의 검색어를 직접 Retriever에 넘겨요. Retriever는 각 검색어에 대해 BM25 검색(검색어당 최대 2개 문서)을 실행한 뒤, 모든 결과를 합치고 중복을 제거한 다음 점수순으로 정렬해요.

from haystack import Document
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.retrievers import (
    InMemoryBM25Retriever,
    MultiQueryTextRetriever,
)


documents = [
    Document(
        content="Renewable energy is energy that is collected from renewable resources.",
    ),
    Document(
        content="Solar energy is a type of green energy that is harnessed from the sun.",
    ),
    Document(
        content="Wind energy is another type of green energy that is generated by wind turbines.",
    ),
    Document(
        content="Hydropower is a form of renewable energy using the flow of water to generate electricity.",
    ),
    Document(
        content="Geothermal energy is heat that comes from the sub-surface of the earth.",
    ),
]

document_store = InMemoryDocumentStore()
document_store.write_documents(documents)

retriever = MultiQueryTextRetriever(
    retriever=InMemoryBM25Retriever(document_store=document_store, top_k=2),
)

results = retriever.run(queries=["renewable energy", "geothermal", "hydropower"])

for doc in results["documents"]:
    print(f"Content: {doc.content}, Score: {doc.score:.4f}")

QueryExpander와 함께 파이프라인에서 쓰기

이 파이프라인은 "sustainable power" 하나의 검색어를 LLM으로 여러 변형(예: "renewable energy sources", "green electricity", "clean power")으로 확장해요. Retriever는 각 변형을 검색하고 결과를 합치죠. 이렇게 하면 "sustainable power"라는 단어가 없는 "solar energy"나 "hydropower" 문서도 찾을 수 있어요.

from haystack import Document, Pipeline
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.query import QueryExpander
from haystack.components.retrievers import (
    InMemoryBM25Retriever,
    MultiQueryTextRetriever,
)


documents = [
    Document(
        content="Renewable energy is energy that is collected from renewable resources.",
    ),
    Document(
        content="Solar energy is a type of green energy that is harnessed from the sun.",
    ),
    Document(
        content="Wind energy is another type of green energy that is generated by wind turbines.",
    ),
    Document(
        content="Hydropower is a form of renewable energy using the flow of water to generate electricity.",
    ),
    Document(
        content="Geothermal energy is heat that comes from the sub-surface of the earth.",
    ),
]

document_store = InMemoryDocumentStore()
document_store.write_documents(documents)

pipeline = Pipeline()
pipeline.add_component("query_expander", QueryExpander(n_expansions=3))
pipeline.add_component(
    "retriever",
    MultiQueryTextRetriever(
        retriever=InMemoryBM25Retriever(document_store=document_store, top_k=2),
    ),
)
pipeline.connect("query_expander.queries", "retriever.queries")

result = pipeline.run({"query_expander": {"query": "sustainable power"}})

for doc in result["retriever"]["documents"]:
    print(f"Score: {doc.score:.3f} | {doc.content}")
components:
  query_expander:
    type: haystack.components.query.query_expander.QueryExpander
    init_parameters:
      n_expansions: 3
  retriever:
    type: haystack.components.retrievers.multi_query_text_retriever.MultiQueryTextRetriever
    init_parameters:
      retriever:
        type: haystack.components.retrievers.in_memory.bm25_retriever.InMemoryBM25Retriever
        init_parameters:
          document_store:
            type: haystack.document_stores.in_memory.document_store.InMemoryDocumentStore
            init_parameters: {}
          top_k: 2

connections:
  - sender: query_expander.queries
    receiver: retriever.queries

RAG 파이프라인에서 쓰기

이 RAG 파이프라인은 검색어 확장을 사용해 질문에 답해요. 사용자가 "What types of energy come from natural sources?"라고 물으면 파이프라인은:

  1. LLM으로 질문을 여러 검색 검색어로 확장해요.
  2. 각 검색어 변형에 대한 관련 문서를 검색해요.
  3. 검색된 문서와 원래 질문을 담은 프롬프트를 만들어요.
  4. LLM에 프롬프트를 보내 답변을 생성해요.

질문은 query_expander(검색 검색어 생성용)와 prompt_builder(LLM용 최종 프롬프트) 양쪽으로 보내져요.

from haystack import Document, Pipeline
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.builders import ChatPromptBuilder
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.query import QueryExpander
from haystack.components.retrievers import (
    InMemoryBM25Retriever,
    MultiQueryTextRetriever,
)
from haystack.dataclasses import ChatMessage


documents = [
    Document(
        content="Renewable energy is energy that is collected from renewable resources.",
    ),
    Document(
        content="Solar energy is a type of green energy that is harnessed from the sun.",
    ),
    Document(
        content="Wind energy is another type of green energy that is generated by wind turbines.",
    ),
]

document_store = InMemoryDocumentStore()
document_store.write_documents(documents)

prompt_template = [
    ChatMessage.from_system(
        "You are a helpful assistant that answers questions based on the provided documents.",
    ),
    ChatMessage.from_user(
        "Given these documents, answer the question.\n"
        "Documents:\n"
        "{% for doc in documents %}"
        "{{ doc.content }}\n"
        "{% endfor %}\n"
        "Question: {{ question }}",
    ),
]

# Note: This assumes OPENAI_API_KEY environment variable is set
rag_pipeline = Pipeline()
rag_pipeline.add_component("query_expander", QueryExpander(n_expansions=2))
rag_pipeline.add_component(
    "retriever",
    MultiQueryTextRetriever(
        retriever=InMemoryBM25Retriever(document_store=document_store, top_k=2),
    ),
)
rag_pipeline.add_component(
    "prompt_builder",
    ChatPromptBuilder(
        template=prompt_template,
        required_variables=["documents", "question"],
    ),
)
rag_pipeline.add_component("llm", OpenAIChatGenerator())

rag_pipeline.connect("query_expander.queries", "retriever.queries")
rag_pipeline.connect("retriever.documents", "prompt_builder.documents")
rag_pipeline.connect("prompt_builder.prompt", "llm.messages")

question = "What types of energy come from natural sources?"
result = rag_pipeline.run(
    {"query_expander": {"query": question}, "prompt_builder": {"question": question}},
)

print(result["llm"]["replies"][0].text)
components:
  query_expander:
    type: haystack.components.query.query_expander.QueryExpander
    init_parameters:
      n_expansions: 2
  retriever:
    type: haystack.components.retrievers.multi_query_text_retriever.MultiQueryTextRetriever
    init_parameters:
      retriever:
        type: haystack.components.retrievers.in_memory.bm25_retriever.InMemoryBM25Retriever
        init_parameters:
          document_store:
            type: haystack.document_stores.in_memory.document_store.InMemoryDocumentStore
            init_parameters: {}
          top_k: 2
  prompt_builder:
    type: haystack.components.builders.chat_prompt_builder.ChatPromptBuilder
    init_parameters:
      required_variables:
        - documents
        - question
  llm:
    type: haystack.components.generators.chat.openai.OpenAIChatGenerator
    init_parameters: {}

connections:
  - sender: query_expander.queries
    receiver: retriever.queries
  - sender: retriever.documents
    receiver: prompt_builder.documents
  - sender: prompt_builder.prompt
    receiver: llm.messages