ElasticsearchHybridRetriever

ElasticsearchHybridRetriever

단일 컴포넌트로 하이브리드 리트리버를 구현한 SuperComponent예요. 백엔드 Document Store로 Elasticsearch를 사용해요.

하이브리드 리트리버는 기존의 키워드 기반 검색(BM25)과 임베딩 기반 검색을 모두 사용해 문서를 검색하며, 두 접근법의 장점을 결합해요. 그런 다음 리트리버가 두 방법의 결과를 병합하고 다시 순위를 매겨요.

출처: 문서

본문

  • 파이프라인에서의 일반적인 위치: 1. RAG 파이프라인에서 TextEmbedder 뒤, PromptBuilder 앞. 2. 하이브리드 검색 파이프라인의 마지막 컴포넌트. 3. 추출형 QA 파이프라인에서 TextEmbedder 뒤, TransformersExtractiveReader 앞.
  • 필수 초기화 변수: document_store(ElasticsearchDocumentStore 인스턴스), embedder(TextEmbedder 프로토콜을 구현하는 임베더)
  • 필수 실행 변수: query(쿼리 문자열)
  • 출력 변수: documents(쿼리와 일치하는 문서 리스트)

개요 (Overview)

ElasticsearchHybridRetriever는 두 가지 검색 방법을 결합해요.

  • BM25 검색: BM25 알고리즘으로 용어 빈도와 역문서 빈도를 바탕으로 문서를 찾는 키워드 기반 검색이에요. ElasticsearchBM25Retriever 컴포넌트를 기반으로 하며, 이름, ID, 명확히 정의된 용어의 정확한 일치를 찾는 데 적합해요.
  • 임베딩 기반 검색: 벡터 유사도로 쿼리와 의미적으로 유사한 문서를 찾는 시맨틱 검색이에요. ElasticsearchEmbeddingRetriever 컴포넌트를 기반으로 하며, 의미적 검색에 적합해요.

이 컴포넌트는 다음을 자동으로 처리해요.

  • 제공된 임베더를 사용해 쿼리를 임베딩으로 변환하고,
  • 두 검색 방법을 병렬로 실행하고,
  • 지정된 결합 모드(기본값: Reciprocal Rank Fusion)로 결과를 병합하고 다시 순위를 매겨요.

설치 (Installation)

Elasticsearch를 설치한 뒤 인스턴스를 시작하세요. Haystack은 Elasticsearch 8을 지원해요.

Docker가 설정되어 있다면 Docker 이미지를 받아 실행하는 것을 권장해요.

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.19.7
docker run -p 9200:9200 -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" -e "xpack.security.enabled=false" elasticsearch:8.19.7

대안으로 Elasticsearch 통합 GitHub에서 제공하는 docker-compose.yml을 사용해 Elasticsearch를 실행하는 Docker 컨테이너를 시작할 수 있어요.

docker compose up

실행 중인 Elasticsearch 인스턴스가 있으면 elasticsearch-haystack 통합을 설치하세요.

pip install elasticsearch-haystack

선택적 파라미터 (Optional Parameters)

이 리트리버는 다양한 선택적 파라미터를 받아요. 가장 최신 파라미터 목록은 API Reference에서 확인할 수 있어요.

top_k_bm25, fuzziness, filters_bm25, scale_score, filter_policy_bm25, top_k_embedding, filters_embedding, num_candidates, filter_policy_embedding 파라미터로 내부의 BM25 및 임베딩 리트리버 컴포넌트에 추가 파라미터를 전달할 수 있어요.

DocumentJoiner 파라미터(join_mode, weights, top_k, sort_by_score)는 모두 ElasticsearchHybridRetriever 클래스에 직접 노출되어 있어요.

사용법 (Usage)

단독 사용 (On its own)

이 리트리버는 실행하려면 문서(임베딩 포함)가 채워진 ElasticsearchDocumentStore가 필요해요.

from haystack import Document
from haystack_integrations.components.embedders.sentence_transformers import (
    SentenceTransformersTextEmbedder,
    SentenceTransformersDocumentEmbedder,
)
from haystack_integrations.components.retrievers.elasticsearch import (
    ElasticsearchHybridRetriever,
)
from haystack_integrations.document_stores.elasticsearch import (
    ElasticsearchDocumentStore,
)

document_store = ElasticsearchDocumentStore(hosts="http://localhost:9200/")

model = "sentence-transformers/all-MiniLM-L6-v2"

documents = [
    Document(content="There are over 7,000 languages spoken around the world today."),
    Document(
        content="Elephants have been observed to behave in a way that indicates a high level of self-awareness, such as recognizing themselves in mirrors.",
    ),
    Document(
        content="In certain parts of the world, like the Maldives, Puerto Rico, and San Diego, you can witness the phenomenon of bioluminescent waves.",
    ),
]

doc_embedder = SentenceTransformersDocumentEmbedder(model=model)
docs_with_embeddings = doc_embedder.run(documents)
document_store.write_documents(docs_with_embeddings["documents"])

embedder = SentenceTransformersTextEmbedder(model=model)

retriever = ElasticsearchHybridRetriever(
    document_store=document_store,
    embedder=embedder,
)

results = retriever.run(query="How many languages are spoken around the world today?")
print(results["documents"])

파이프라인에서 사용 (In a pipeline)

문서를 임베딩과 함께 저장하는 인덱싱 파이프라인과, ElasticsearchHybridRetriever로 하이브리드 검색을 수행하는 쿼리 파이프라인을 보여주는 전체 예시예요.

OPENAI_API_KEY를 환경 변수로 설정한 뒤 다음 코드를 실행하세요.

from haystack import Document, Pipeline
from haystack.components.builders import ChatPromptBuilder
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack_integrations.components.embedders.sentence_transformers import (
    SentenceTransformersDocumentEmbedder,
    SentenceTransformersTextEmbedder,
)
from haystack.components.writers import DocumentWriter
from haystack.dataclasses import ChatMessage
from haystack.document_stores.types import DuplicatePolicy
from haystack_integrations.components.retrievers.elasticsearch import (
    ElasticsearchHybridRetriever,
)
from haystack_integrations.document_stores.elasticsearch import (
    ElasticsearchDocumentStore,
)

document_store = ElasticsearchDocumentStore(hosts="http://localhost:9200/")

model = "sentence-transformers/all-MiniLM-L6-v2"

documents = [
    Document(content="There are over 7,000 languages spoken around the world today."),
    Document(
        content="Elephants have been observed to behave in a way that indicates a high level of self-awareness, such as recognizing themselves in mirrors.",
    ),
    Document(
        content="In certain parts of the world, like the Maldives, Puerto Rico, and San Diego, you can witness the phenomenon of bioluminescent waves.",
    ),
]

# Indexing Pipeline
indexing_pipeline = Pipeline()
indexing_pipeline.add_component(
    "doc_embedder",
    SentenceTransformersDocumentEmbedder(model=model),
)
indexing_pipeline.add_component(
    "doc_writer",
    DocumentWriter(document_store=document_store, policy=DuplicatePolicy.SKIP),
)
indexing_pipeline.connect("doc_embedder", "doc_writer")
indexing_pipeline.run({"doc_embedder": {"documents": documents}})

# Query Pipeline
prompt_template = [
    ChatMessage.from_user(
        """
    Given these documents, answer the question.\nDocuments:
    {% for doc in documents %}
        {{ doc.content }}
    {% endfor %}

    \nQuestion: {{question}}
    \nAnswer:
    """,
    ),
]

embedder = SentenceTransformersTextEmbedder(model=model)
retriever = ElasticsearchHybridRetriever(
    document_store=document_store,
    embedder=embedder,
    top_k_bm25=3,
    top_k_embedding=3,
    join_mode="reciprocal_rank_fusion",
)

query_pipeline = Pipeline()
query_pipeline.add_component("retriever", retriever)
query_pipeline.add_component(
    "prompt_builder",
    ChatPromptBuilder(template=prompt_template, required_variables="*"),
)
query_pipeline.add_component("llm", OpenAIChatGenerator())
query_pipeline.connect("retriever.documents", "prompt_builder.documents")
query_pipeline.connect("prompt_builder.prompt", "llm.messages")

question = "How many languages are spoken around the world today?"
result = query_pipeline.run(
    {
        "retriever": {"query": question},
        "prompt_builder": {"question": question},
    },
)

print(result["llm"]["replies"][0].text)

더 알아보기 (Learn more)