chore: import upstream snapshot with attribution
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:28 +08:00
commit c56bef871b
9296 changed files with 1854228 additions and 0 deletions
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import sys
from typing import TYPE_CHECKING
from lazy_imports import LazyImporter
_import_structure = {
"auto_merging_retriever": ["AutoMergingRetriever"],
"filter_retriever": ["FilterRetriever"],
"in_memory": ["InMemoryBM25Retriever", "InMemoryEmbeddingRetriever"],
"multi_retriever": ["MultiRetriever"],
"multi_query_embedding_retriever": ["MultiQueryEmbeddingRetriever"],
"multi_query_text_retriever": ["MultiQueryTextRetriever"],
"sentence_window_retriever": ["SentenceWindowRetriever"],
"text_embedding_retriever": ["TextEmbeddingRetriever"],
}
if TYPE_CHECKING:
from .auto_merging_retriever import AutoMergingRetriever as AutoMergingRetriever
from .filter_retriever import FilterRetriever as FilterRetriever
from .in_memory import InMemoryBM25Retriever as InMemoryBM25Retriever
from .in_memory import InMemoryEmbeddingRetriever as InMemoryEmbeddingRetriever
from .multi_query_embedding_retriever import MultiQueryEmbeddingRetriever as MultiQueryEmbeddingRetriever
from .multi_query_text_retriever import MultiQueryTextRetriever as MultiQueryTextRetriever
from .multi_retriever import MultiRetriever as MultiRetriever
from .sentence_window_retriever import SentenceWindowRetriever as SentenceWindowRetriever
from .text_embedding_retriever import TextEmbeddingRetriever as TextEmbeddingRetriever
else:
sys.modules[__name__] = LazyImporter(name=__name__, module_file=__file__, import_structure=_import_structure)
@@ -0,0 +1,226 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from collections import defaultdict
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.document_stores.types import DocumentStore
@component
class AutoMergingRetriever:
"""
A retriever which returns parent documents of the matched leaf nodes documents, based on a threshold setting.
The AutoMergingRetriever assumes you have a hierarchical tree structure of documents, where the leaf nodes
are indexed in a document store. See the HierarchicalDocumentSplitter for more information on how to create
such a structure. During retrieval, if the number of matched leaf documents below the same parent is
higher than a defined threshold, the retriever will return the parent document instead of the individual leaf
documents.
The rational is, given that a paragraph is split into multiple chunks represented as leaf documents, and if for
a given query, multiple chunks are matched, the whole paragraph might be more informative than the individual
chunks alone.
Currently the AutoMergingRetriever can only be used by the following DocumentStores:
- [AstraDB](https://haystack.deepset.ai/integrations/astradb)
- [ElasticSearch](https://haystack.deepset.ai/docs/latest/documentstore/elasticsearch)
- [OpenSearch](https://haystack.deepset.ai/docs/latest/documentstore/opensearch)
- [PGVector](https://haystack.deepset.ai/docs/latest/documentstore/pgvector)
- [Qdrant](https://haystack.deepset.ai/docs/latest/documentstore/qdrant)
```python
from haystack import Document
from haystack.components.preprocessors import HierarchicalDocumentSplitter
from haystack.components.retrievers.auto_merging_retriever import AutoMergingRetriever
from haystack.document_stores.in_memory import InMemoryDocumentStore
# create a hierarchical document structure with 3 levels, where the parent document has 3 children
text = "The sun rose early in the morning. It cast a warm glow over the trees. Birds began to sing."
original_document = Document(content=text)
builder = HierarchicalDocumentSplitter(block_sizes={10, 3}, split_overlap=0, split_by="word")
docs = builder.run([original_document])["documents"]
# store level-1 parent documents and initialize the retriever
doc_store_parents = InMemoryDocumentStore()
for doc in docs:
if doc.meta["__children_ids"] and doc.meta["__level"] in [0,1]: # store the root document and level 1 documents
doc_store_parents.write_documents([doc])
retriever = AutoMergingRetriever(doc_store_parents, threshold=0.5)
# assume we retrieved 2 leaf docs from the same parent, the parent document should be returned,
# since it has 3 children and the threshold=0.5, and we retrieved 2 children (2/3 > 0.66(6))
leaf_docs = [doc for doc in docs if not doc.meta["__children_ids"]]
retrieved_docs = retriever.run(leaf_docs[4:6])
print(retrieved_docs["documents"])
# [Document(id=538..),
# content: 'warm glow over the trees. Birds began to sing.',
# meta: {'block_size': 10, 'parent_id': '835..', 'children_ids': ['c17...', '3ff...', '352...'], 'level': 1, 'source_id': '835...',
# 'page_number': 1, 'split_id': 1, 'split_idx_start': 45})]}
```
""" # noqa: E501
def __init__(self, document_store: DocumentStore, threshold: float = 0.5) -> None:
"""
Initialize the AutoMergingRetriever.
:param document_store: DocumentStore from which to retrieve the parent documents
:param threshold: Threshold to decide whether the parent instead of the individual documents is returned
"""
if not 0 < threshold < 1:
raise ValueError("The threshold parameter must be between 0 and 1.")
self.document_store = document_store
self.threshold = threshold
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(self, document_store=self.document_store, threshold=self.threshold)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "AutoMergingRetriever":
"""
Deserializes the component from a dictionary.
:param data:
Dictionary with serialized data.
:returns:
An instance of the component.
"""
return default_from_dict(cls, data)
@staticmethod
def _check_valid_documents(matched_leaf_documents: list[Document]) -> None:
# check if the matched leaf documents have the required meta fields
if not all(doc.meta.get("__parent_id") for doc in matched_leaf_documents):
raise ValueError("The matched leaf documents do not have the required meta field '__parent_id'")
if not all(doc.meta.get("__level") for doc in matched_leaf_documents):
raise ValueError("The matched leaf documents do not have the required meta field '__level'")
if not all(doc.meta.get("__block_size") for doc in matched_leaf_documents):
raise ValueError("The matched leaf documents do not have the required meta field '__block_size'")
@component.output_types(documents=list[Document])
def run(self, documents: list[Document]) -> dict[str, list[Document]]:
"""
Run the AutoMergingRetriever.
Recursively groups documents by their parents and merges them if they meet the threshold,
continuing up the hierarchy until no more merges are possible.
:param documents: List of leaf documents that were matched by a retriever
:returns:
List of documents (could be a mix of different hierarchy levels)
"""
AutoMergingRetriever._check_valid_documents(documents)
def _get_parent_doc(parent_id: str) -> Document:
parent_docs = self.document_store.filter_documents({"field": "id", "operator": "==", "value": parent_id})
if len(parent_docs) != 1:
raise ValueError(f"Expected 1 parent document with id {parent_id}, found {len(parent_docs)}")
parent_doc = parent_docs[0]
if not parent_doc.meta.get("__children_ids"):
raise ValueError(f"Parent document with id {parent_id} does not have any children.")
return parent_doc
def _try_merge_level(docs_to_merge: list[Document], docs_to_return: list[Document]) -> list[Document]:
parent_doc_id_to_child_docs: dict[str, list[Document]] = defaultdict(list) # to group documents by parent
for doc in docs_to_merge:
if doc.meta.get("__parent_id"): # only docs that have parents
parent_doc_id_to_child_docs[doc.meta["__parent_id"]].append(doc)
else:
docs_to_return.append(doc) # keep docs that have no parents
# Process each parent group
merged_docs = []
for parent_doc_id, child_docs in parent_doc_id_to_child_docs.items():
parent_doc = _get_parent_doc(parent_doc_id)
# Calculate merge score
score = len(child_docs) / len(parent_doc.meta["__children_ids"])
if score > self.threshold:
merged_docs.append(parent_doc) # Merge into parent
else:
docs_to_return.extend(child_docs) # Keep children separate
# if no new merges were made, we're done
if not merged_docs:
return merged_docs + docs_to_return
# Recursively try to merge the next level
return _try_merge_level(merged_docs, docs_to_return)
return {"documents": _try_merge_level(documents, [])}
@component.output_types(documents=list[Document])
async def run_async(self, documents: list[Document]) -> dict[str, list[Document]]:
"""
Asynchronously run the AutoMergingRetriever.
Recursively groups documents by their parents and merges them if they meet the threshold,
continuing up the hierarchy until no more merges are possible.
:param documents: List of leaf documents that were matched by a retriever
:returns:
List of documents (could be a mix of different hierarchy levels)
"""
AutoMergingRetriever._check_valid_documents(documents)
async def _get_parent_doc(parent_id: str) -> Document:
# 'ignore' since filter_documents_async is not defined in the Protocol but exists in the implementations
parent_docs = await self.document_store.filter_documents_async( # type: ignore[attr-defined]
{"field": "id", "operator": "==", "value": parent_id}
)
if len(parent_docs) != 1:
raise ValueError(f"Expected 1 parent document with id {parent_id}, found {len(parent_docs)}")
parent_doc = parent_docs[0]
if not parent_doc.meta.get("__children_ids"):
raise ValueError(f"Parent document with id {parent_id} does not have any children.")
return parent_doc
async def _try_merge_level(docs_to_merge: list[Document], docs_to_return: list[Document]) -> list[Document]:
parent_doc_id_to_child_docs: dict[str, list[Document]] = defaultdict(list) # to group documents by parent
for doc in docs_to_merge:
if doc.meta.get("__parent_id"): # only docs that have parents
parent_doc_id_to_child_docs[doc.meta["__parent_id"]].append(doc)
else:
docs_to_return.append(doc) # keep docs that have no parents
# Process each parent group
merged_docs = []
for parent_doc_id, child_docs in parent_doc_id_to_child_docs.items():
parent_doc = await _get_parent_doc(parent_doc_id)
# Calculate merge score
score = len(child_docs) / len(parent_doc.meta["__children_ids"])
if score > self.threshold:
merged_docs.append(parent_doc) # Merge into parent
else:
docs_to_return.extend(child_docs) # Keep children separate
# if no new merges were made, we're done
if not merged_docs:
return merged_docs + docs_to_return
# Recursively try to merge the next level
return await _try_merge_level(merged_docs, docs_to_return)
return {"documents": await _try_merge_level(documents, [])}
@@ -0,0 +1,104 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.document_stores.types import DocumentStore
@component
class FilterRetriever:
"""
Retrieves documents that match the provided filters.
### Usage example
```python
from haystack import Document
from haystack.components.retrievers import FilterRetriever
from haystack.document_stores.in_memory import InMemoryDocumentStore
docs = [
Document(content="Python is a popular programming language", meta={"lang": "en"}),
Document(content="python ist eine beliebte Programmiersprache", meta={"lang": "de"}),
]
doc_store = InMemoryDocumentStore()
doc_store.write_documents(docs)
retriever = FilterRetriever(doc_store, filters={"field": "lang", "operator": "==", "value": "en"})
# if passed in the run method, filters override those provided at initialization
result = retriever.run(filters={"field": "lang", "operator": "==", "value": "de"})
print(result["documents"])
```
"""
def __init__(self, document_store: DocumentStore, filters: dict[str, Any] | None = None) -> None:
"""
Create the FilterRetriever component.
:param document_store:
An instance of a Document Store to use with the Retriever.
:param filters:
A dictionary with filters to narrow down the search space.
"""
self.document_store = document_store
self.filters = filters
def _get_telemetry_data(self) -> dict[str, Any]:
"""
Data that is sent to Posthog for usage analytics.
"""
return {"document_store": type(self.document_store).__name__}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(self, document_store=self.document_store, filters=self.filters)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "FilterRetriever":
"""
Deserializes the component from a dictionary.
:param data:
The dictionary to deserialize from.
:returns:
The deserialized component.
"""
return default_from_dict(cls, data)
@component.output_types(documents=list[Document])
def run(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
"""
Run the FilterRetriever on the given input data.
:param filters:
A dictionary with filters to narrow down the search space.
If not specified, the FilterRetriever uses the values provided at initialization.
:returns:
A list of retrieved documents.
"""
return {"documents": self.document_store.filter_documents(filters=filters or self.filters)}
@component.output_types(documents=list[Document])
async def run_async(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
"""
Asynchronously run the FilterRetriever on the given input data.
:param filters:
A dictionary with filters to narrow down the search space.
If not specified, the FilterRetriever uses the values provided at initialization.
:returns:
A list of retrieved documents.
"""
# 'ignore' since filter_documents_async is not defined in the Protocol but exists in the implementations
out_documents = await self.document_store.filter_documents_async(filters=filters or self.filters) # type: ignore[attr-defined]
return {"documents": out_documents}
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import sys
from typing import TYPE_CHECKING
from lazy_imports import LazyImporter
_import_structure = {"bm25_retriever": ["InMemoryBM25Retriever"], "embedding_retriever": ["InMemoryEmbeddingRetriever"]}
if TYPE_CHECKING:
from .bm25_retriever import InMemoryBM25Retriever as InMemoryBM25Retriever
from .embedding_retriever import InMemoryEmbeddingRetriever as InMemoryEmbeddingRetriever
else:
sys.modules[__name__] = LazyImporter(name=__name__, module_file=__file__, import_structure=_import_structure)
@@ -0,0 +1,196 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import FilterPolicy
@component
class InMemoryBM25Retriever:
"""
Retrieves documents that are most similar to the query using keyword-based algorithm.
Use this retriever with the InMemoryDocumentStore.
### Usage example
```python
from haystack import Document
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack.document_stores.in_memory import InMemoryDocumentStore
docs = [
Document(content="Python is a popular programming language"),
Document(content="python ist eine beliebte Programmiersprache"),
]
doc_store = InMemoryDocumentStore()
doc_store.write_documents(docs)
retriever = InMemoryBM25Retriever(doc_store)
result = retriever.run(query="Programmiersprache")
print(result["documents"])
```
"""
def __init__(
self,
document_store: InMemoryDocumentStore,
filters: dict[str, Any] | None = None,
top_k: int = 10,
scale_score: bool = False,
filter_policy: FilterPolicy = FilterPolicy.REPLACE,
) -> None:
"""
Create the InMemoryBM25Retriever component.
:param document_store:
An instance of InMemoryDocumentStore where the retriever should search for relevant documents.
:param filters:
A dictionary with filters to narrow down the retriever's search space in the document store.
:param top_k:
The maximum number of documents to retrieve.
:param scale_score:
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
When `False`, uses raw similarity scores.
:param filter_policy: The filter policy to apply during retrieval.
Filter policy determines how filters are applied when retrieving documents. You can choose:
- `REPLACE` (default): Overrides the initialization filters with the filters specified at runtime.
Use this policy to dynamically change filtering for specific queries.
- `MERGE`: Combines runtime filters with initialization filters to narrow down the search.
:raises TypeError: If the document_store is not an instance of InMemoryDocumentStore.
:raises ValueError:
If the specified `top_k` is not > 0.
"""
if not isinstance(document_store, InMemoryDocumentStore):
raise TypeError("document_store must be an instance of InMemoryDocumentStore")
self.document_store = document_store
if top_k <= 0:
raise ValueError(f"top_k must be greater than 0. Currently, the top_k is {top_k}")
self.filters = filters
self.top_k = top_k
self.scale_score = scale_score
self.filter_policy = filter_policy
def _get_telemetry_data(self) -> dict[str, Any]:
"""
Data that is sent to Posthog for usage analytics.
"""
return {"document_store": type(self.document_store).__name__}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
document_store=self.document_store,
filters=self.filters,
top_k=self.top_k,
scale_score=self.scale_score,
filter_policy=self.filter_policy.value,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "InMemoryBM25Retriever":
"""
Deserializes the component from a dictionary.
:param data:
The dictionary to deserialize from.
:returns:
The deserialized component.
"""
init_params = data.get("init_parameters", {})
if "filter_policy" in init_params:
init_params["filter_policy"] = FilterPolicy.from_str(init_params["filter_policy"])
return default_from_dict(cls, data)
@component.output_types(documents=list[Document])
def run(
self,
query: str,
filters: dict[str, Any] | None = None,
top_k: int | None = None,
scale_score: bool | None = None,
) -> dict[str, list[Document]]:
"""
Run the InMemoryBM25Retriever on the given input data.
:param query:
The query string for the Retriever.
:param filters:
A dictionary with filters to narrow down the search space when retrieving documents.
:param top_k:
The maximum number of documents to return.
:param scale_score:
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
When `False`, uses raw similarity scores.
:returns:
The retrieved documents.
:raises ValueError:
If the specified DocumentStore is not found or is not a InMemoryDocumentStore instance.
"""
if self.filter_policy == FilterPolicy.MERGE and filters:
filters = {**(self.filters or {}), **filters}
else:
filters = filters or self.filters
if top_k is None:
top_k = self.top_k
if scale_score is None:
scale_score = self.scale_score
docs = self.document_store.bm25_retrieval(query=query, filters=filters, top_k=top_k, scale_score=scale_score)
return {"documents": docs}
@component.output_types(documents=list[Document])
async def run_async(
self,
query: str,
filters: dict[str, Any] | None = None,
top_k: int | None = None,
scale_score: bool | None = None,
) -> dict[str, list[Document]]:
"""
Run the InMemoryBM25Retriever on the given input data.
:param query:
The query string for the Retriever.
:param filters:
A dictionary with filters to narrow down the search space when retrieving documents.
:param top_k:
The maximum number of documents to return.
:param scale_score:
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
When `False`, uses raw similarity scores.
:returns:
The retrieved documents.
:raises ValueError:
If the specified DocumentStore is not found or is not a InMemoryDocumentStore instance.
"""
if self.filter_policy == FilterPolicy.MERGE and filters:
filters = {**(self.filters or {}), **filters}
else:
filters = filters or self.filters
if top_k is None:
top_k = self.top_k
if scale_score is None:
scale_score = self.scale_score
docs = await self.document_store.bm25_retrieval_async(
query=query, filters=filters, top_k=top_k, scale_score=scale_score
)
return {"documents": docs}
@@ -0,0 +1,236 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import FilterPolicy
@component
class InMemoryEmbeddingRetriever:
"""
Retrieves documents that are most semantically similar to the query.
Use this retriever with the InMemoryDocumentStore.
When using this retriever, make sure it has query and document embeddings available.
In indexing pipelines, use a DocumentEmbedder to embed documents.
In query pipelines, use a TextEmbedder to embed queries and send them to the retriever.
### Usage example
```python
from haystack import Document
from haystack.components.embedders import OpenAIDocumentEmbedder, OpenAITextEmbedder
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from haystack.document_stores.in_memory import InMemoryDocumentStore
docs = [
Document(content="Python is a popular programming language"),
Document(content="python ist eine beliebte Programmiersprache"),
]
doc_embedder = OpenAIDocumentEmbedder()
docs_with_embeddings = doc_embedder.run(docs)["documents"]
doc_store = InMemoryDocumentStore()
doc_store.write_documents(docs_with_embeddings)
retriever = InMemoryEmbeddingRetriever(doc_store)
query="Programmiersprache"
text_embedder = OpenAITextEmbedder()
query_embedding = text_embedder.run(query)["embedding"]
result = retriever.run(query_embedding=query_embedding)
print(result["documents"])
```
"""
def __init__(
self,
document_store: InMemoryDocumentStore,
filters: dict[str, Any] | None = None,
top_k: int = 10,
scale_score: bool = False,
return_embedding: bool = False,
filter_policy: FilterPolicy = FilterPolicy.REPLACE,
) -> None:
"""
Create the InMemoryEmbeddingRetriever component.
:param document_store:
An instance of InMemoryDocumentStore where the retriever should search for relevant documents.
:param filters:
A dictionary with filters to narrow down the retriever's search space in the document store.
:param top_k:
The maximum number of documents to retrieve.
:param scale_score:
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
When `False`, uses raw similarity scores.
:param return_embedding:
When `True`, returns the embedding of the retrieved documents.
When `False`, returns just the documents, without their embeddings.
:param filter_policy: The filter policy to apply during retrieval.
Filter policy determines how filters are applied when retrieving documents. You can choose:
- `REPLACE` (default): Overrides the initialization filters with the filters specified at runtime.
Use this policy to dynamically change filtering for specific queries.
- `MERGE`: Combines runtime filters with initialization filters to narrow down the search.
:raises TypeError: If the document_store is not an instance of InMemoryDocumentStore.
:raises ValueError:
If the specified top_k is not > 0.
"""
if not isinstance(document_store, InMemoryDocumentStore):
raise TypeError("document_store must be an instance of InMemoryDocumentStore")
self.document_store = document_store
if top_k <= 0:
raise ValueError(f"top_k must be greater than 0. Currently, top_k is {top_k}")
self.filters = filters
self.top_k = top_k
self.scale_score = scale_score
self.return_embedding = return_embedding
self.filter_policy = filter_policy
def _get_telemetry_data(self) -> dict[str, Any]:
"""
Data that is sent to Posthog for usage analytics.
"""
return {"document_store": type(self.document_store).__name__}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
document_store=self.document_store,
filters=self.filters,
top_k=self.top_k,
scale_score=self.scale_score,
return_embedding=self.return_embedding,
filter_policy=self.filter_policy.value,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "InMemoryEmbeddingRetriever":
"""
Deserializes the component from a dictionary.
:param data:
The dictionary to deserialize from.
:returns:
The deserialized component.
"""
init_params = data.get("init_parameters", {})
if "filter_policy" in init_params:
init_params["filter_policy"] = FilterPolicy.from_str(init_params["filter_policy"])
return default_from_dict(cls, data)
@component.output_types(documents=list[Document])
def run(
self,
query_embedding: list[float],
filters: dict[str, Any] | None = None,
top_k: int | None = None,
scale_score: bool | None = None,
return_embedding: bool | None = None,
) -> dict[str, list[Document]]:
"""
Run the InMemoryEmbeddingRetriever on the given input data.
:param query_embedding:
Embedding of the query.
:param filters:
A dictionary with filters to narrow down the search space when retrieving documents.
:param top_k:
The maximum number of documents to return.
:param scale_score:
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
When `False`, uses raw similarity scores.
:param return_embedding:
When `True`, returns the embedding of the retrieved documents.
When `False`, returns just the documents, without their embeddings.
:returns:
The retrieved documents.
:raises ValueError:
If the specified DocumentStore is not found or is not an InMemoryDocumentStore instance.
"""
if self.filter_policy == FilterPolicy.MERGE and filters:
filters = {**(self.filters or {}), **filters}
else:
filters = filters or self.filters
if top_k is None:
top_k = self.top_k
if scale_score is None:
scale_score = self.scale_score
if return_embedding is None:
return_embedding = self.return_embedding
docs = self.document_store.embedding_retrieval(
query_embedding=query_embedding,
filters=filters,
top_k=top_k,
scale_score=scale_score,
return_embedding=return_embedding,
)
return {"documents": docs}
@component.output_types(documents=list[Document])
async def run_async(
self,
query_embedding: list[float],
filters: dict[str, Any] | None = None,
top_k: int | None = None,
scale_score: bool | None = None,
return_embedding: bool | None = None,
) -> dict[str, list[Document]]:
"""
Run the InMemoryEmbeddingRetriever on the given input data.
:param query_embedding:
Embedding of the query.
:param filters:
A dictionary with filters to narrow down the search space when retrieving documents.
:param top_k:
The maximum number of documents to return.
:param scale_score:
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
When `False`, uses raw similarity scores.
:param return_embedding:
When `True`, returns the embedding of the retrieved documents.
When `False`, returns just the documents, without their embeddings.
:returns:
The retrieved documents.
:raises ValueError:
If the specified DocumentStore is not found or is not an InMemoryDocumentStore instance.
"""
if self.filter_policy == FilterPolicy.MERGE and filters:
filters = {**(self.filters or {}), **filters}
else:
filters = filters or self.filters
if top_k is None:
top_k = self.top_k
if scale_score is None:
scale_score = self.scale_score
if return_embedding is None:
return_embedding = self.return_embedding
docs = await self.document_store.embedding_retrieval_async(
query_embedding=query_embedding,
filters=filters,
top_k=top_k,
scale_score=scale_score,
return_embedding=return_embedding,
)
return {"documents": docs}
@@ -0,0 +1,237 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.components.embedders.types.protocol import TextEmbedder
from haystack.components.retrievers.types import EmbeddingRetriever
from haystack.core.serialization import component_to_dict
from haystack.utils.async_utils import _execute_component_async
from haystack.utils.misc import _deduplicate_documents
@component
class MultiQueryEmbeddingRetriever:
"""
A component that retrieves documents using multiple queries in parallel with an embedding-based retriever.
This component takes a list of text queries, converts them to embeddings using a query embedder,
and then uses an embedding-based retriever to find relevant documents for each query in parallel.
The results are combined and sorted by relevance score.
### Usage example
```python
from haystack import Document
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import DuplicatePolicy
from haystack.components.embedders import OpenAITextEmbedder
from haystack.components.embedders import OpenAIDocumentEmbedder
from haystack.components.retrievers import InMemoryEmbeddingRetriever
from haystack.components.writers import DocumentWriter
from haystack.components.retrievers import MultiQueryEmbeddingRetriever
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="Geothermal energy is heat that comes from the sub-surface of the earth."),
Document(content="Biomass energy is produced from organic materials, such as plant and animal waste."),
Document(content="Fossil fuels, such as coal, oil, and natural gas, are non-renewable energy sources."),
]
# Populate the document store
doc_store = InMemoryDocumentStore()
doc_embedder = OpenAIDocumentEmbedder()
doc_writer = DocumentWriter(document_store=doc_store, policy=DuplicatePolicy.SKIP)
documents = doc_embedder.run(documents)["documents"]
doc_writer.run(documents=documents)
# Run the multi-query retriever
in_memory_retriever = InMemoryEmbeddingRetriever(document_store=doc_store, top_k=1)
query_embedder = OpenAITextEmbedder()
multi_query_retriever = MultiQueryEmbeddingRetriever(
retriever=in_memory_retriever,
query_embedder=query_embedder,
max_workers=3
)
queries = ["Geothermal energy", "natural gas", "turbines"]
result = multi_query_retriever.run(queries=queries)
for doc in result["documents"]:
print(f"Content: {doc.content}, Score: {doc.score}")
# >> Content: Geothermal energy is heat that comes from the sub-surface of the earth., Score: 0.8509603046266574
# >> Content: Renewable energy is energy that is collected from renewable resources., Score: 0.42763211298893034
# >> Content: Solar energy is a type of green energy that is harnessed from the sun., Score: 0.40077417016494354
# >> Content: Fossil fuels, such as coal, oil, and natural gas, are non-renewable energy sources., Score: 0.3774863680
# >> Content: Wind energy is another type of green energy that is generated by wind turbines., Score: 0.30914239725622
# >> Content: Biomass energy is produced from organic materials, such as plant and animal waste., Score: 0.25173074243
```
""" # noqa E501
def __init__(self, *, retriever: EmbeddingRetriever, query_embedder: TextEmbedder, max_workers: int = 3) -> None:
"""
Initialize MultiQueryEmbeddingRetriever.
:param retriever: The embedding-based retriever to use for document retrieval.
:param query_embedder: The query embedder to convert text queries to embeddings.
:param max_workers: Maximum number of worker threads for parallel processing.
"""
self.retriever = retriever
self.query_embedder = query_embedder
self.max_workers = max_workers
def warm_up(self) -> None:
"""
Warm up the query embedder and the retriever.
"""
for inner in (self.query_embedder, self.retriever):
if hasattr(inner, "warm_up"):
inner.warm_up()
async def warm_up_async(self) -> None:
"""
Warm up the query embedder and the retriever on the serving event loop.
"""
for inner in (self.query_embedder, self.retriever):
if hasattr(inner, "warm_up_async"):
await inner.warm_up_async()
elif hasattr(inner, "warm_up"):
inner.warm_up()
def close(self) -> None:
"""
Release the query embedder's and the retriever's resources.
"""
for inner in (self.query_embedder, self.retriever):
if hasattr(inner, "close"):
inner.close()
async def close_async(self) -> None:
"""
Release the query embedder's and the retriever's async resources.
"""
for inner in (self.query_embedder, self.retriever):
if hasattr(inner, "close_async"):
await inner.close_async()
elif hasattr(inner, "close"):
inner.close()
@component.output_types(documents=list[Document])
def run(self, queries: list[str], retriever_kwargs: dict[str, Any] | None = None) -> dict[str, list[Document]]:
"""
Retrieve documents using multiple queries in parallel.
:param queries: List of text queries to process.
:param retriever_kwargs: Optional dictionary of arguments to pass to the retriever's run method.
:returns:
A dictionary containing:
- `documents`: List of retrieved documents sorted by relevance score.
"""
docs: list[Document] = []
retriever_kwargs = retriever_kwargs or {}
self.warm_up()
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
queries_results = executor.map(lambda query: self._run_on_thread(query, retriever_kwargs), queries)
for result in queries_results:
if not result:
continue
docs.extend(result)
# de-duplicate and sort
docs = _deduplicate_documents(docs)
docs.sort(key=lambda x: x.score or 0.0, reverse=True)
return {"documents": docs}
@component.output_types(documents=list[Document])
async def run_async(
self, queries: list[str], retriever_kwargs: dict[str, Any] | None = None
) -> dict[str, list[Document]]:
"""
Retrieve documents using multiple queries concurrently.
Uses each component's `run_async` method if available, otherwise falls back to running `run`
in a thread executor. Queries are processed concurrently using asyncio.gather.
:param queries: List of text queries to process.
:param retriever_kwargs: Optional dictionary of arguments to pass to the retriever's run method.
:returns:
A dictionary containing:
- `documents`: List of retrieved documents sorted by relevance score.
"""
retriever_kwargs = retriever_kwargs or {}
await self.warm_up_async()
results = await asyncio.gather(*[self._run_one_async(q, retriever_kwargs) for q in queries])
docs: list[Document] = [doc for result in results if result for doc in result]
docs = _deduplicate_documents(docs)
docs.sort(key=lambda x: x.score or 0.0, reverse=True)
return {"documents": docs}
def _run_on_thread(self, query: str, retriever_kwargs: dict[str, Any] | None = None) -> list[Document] | None:
"""
Process a single query on a separate thread.
:param query: The text query to process.
:param retriever_kwargs: Arguments to pass to the retriever's run method.
:returns:
List of retrieved documents or None if no results.
"""
embedding_result = self.query_embedder.run(text=query)
query_embedding = embedding_result["embedding"]
result = self.retriever.run(query_embedding=query_embedding, **(retriever_kwargs or {}))
if result and "documents" in result:
return result["documents"]
return None
async def _run_one_async(self, query: str, retriever_kwargs: dict[str, Any]) -> list[Document] | None:
"""
Process a single query asynchronously.
:param query: The text query to process.
:param retriever_kwargs: Arguments to pass to the retriever's run method.
:returns:
List of retrieved documents or None if no results.
"""
embedding_result = await _execute_component_async(self.query_embedder, text=query)
query_embedding = embedding_result["embedding"]
result = await _execute_component_async(self.retriever, query_embedding=query_embedding, **retriever_kwargs)
if result and "documents" in result:
return result["documents"]
return None
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
A dictionary representing the serialized component.
"""
return default_to_dict(
self,
retriever=component_to_dict(obj=self.retriever, name="retriever"),
query_embedder=component_to_dict(obj=self.query_embedder, name="query_embedder"),
max_workers=self.max_workers,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "MultiQueryEmbeddingRetriever":
"""
Deserializes the component from a dictionary.
:param data: The dictionary to deserialize from.
:returns:
The deserialized component.
"""
return default_from_dict(cls, data)
@@ -0,0 +1,206 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.components.retrievers.types import TextRetriever
from haystack.core.serialization import component_to_dict
from haystack.utils.async_utils import _execute_component_async
from haystack.utils.misc import _deduplicate_documents
@component
class MultiQueryTextRetriever:
"""
A component that retrieves documents using multiple queries in parallel with a text-based retriever.
This component takes a list of text queries and uses a text-based retriever to find relevant documents for each
query in parallel, using a thread pool to manage concurrent execution. The results are combined and sorted by
relevance score.
You can use this component in combination with QueryExpander component to enhance the retrieval process.
### Usage example
```python
from haystack import Document
from haystack.components.writers import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import DuplicatePolicy
from haystack.components.retrievers import InMemoryBM25Retriever
from haystack.components.query import QueryExpander
from haystack.components.retrievers.multi_query_text_retriever import 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()
doc_writer = DocumentWriter(document_store=document_store, policy=DuplicatePolicy.SKIP)
doc_writer.run(documents=documents)
in_memory_retriever = InMemoryBM25Retriever(document_store=document_store, top_k=1)
multiquery_retriever = MultiQueryTextRetriever(retriever=in_memory_retriever)
results = multiquery_retriever.run(queries=["renewable energy?", "Geothermal", "Hydropower"])
for doc in results["documents"]:
print(f"Content: {doc.content}, Score: {doc.score}")
# >>
# >> Content: Geothermal energy is heat that comes from the sub-surface of the earth., Score: 1.6474448833731097
# >> Content: Hydropower is a form of renewable energy using the flow of water to generate electricity., Score: 1.615
# >> Content: Renewable energy is energy that is collected from renewable resources., Score: 1.5255309812344944
```
""" # noqa E501
def __init__(self, *, retriever: TextRetriever, max_workers: int = 3) -> None:
"""
Initialize MultiQueryTextRetriever.
:param retriever: The text-based retriever to use for document retrieval.
:param max_workers: Maximum number of worker threads for parallel processing. Default is 3.
"""
self.retriever = retriever
self.max_workers = max_workers
def warm_up(self) -> None:
"""
Warm up the retriever.
"""
if hasattr(self.retriever, "warm_up"):
self.retriever.warm_up()
async def warm_up_async(self) -> None:
"""
Warm up the retriever on the serving event loop.
"""
if hasattr(self.retriever, "warm_up_async"):
await self.retriever.warm_up_async()
elif hasattr(self.retriever, "warm_up"):
self.retriever.warm_up()
def close(self) -> None:
"""
Release the retriever's resources.
"""
if hasattr(self.retriever, "close"):
self.retriever.close()
async def close_async(self) -> None:
"""
Release the retriever's async resources.
"""
if hasattr(self.retriever, "close_async"):
await self.retriever.close_async()
elif hasattr(self.retriever, "close"):
self.retriever.close()
@component.output_types(documents=list[Document])
def run(self, queries: list[str], retriever_kwargs: dict[str, Any] | None = None) -> dict[str, list[Document]]:
"""
Retrieve documents using multiple queries in parallel.
:param queries: List of text queries to process.
:param retriever_kwargs: Optional dictionary of arguments to pass to the retriever's run method.
:returns:
A dictionary containing:
`documents`: List of retrieved documents sorted by relevance score.
"""
docs: list[Document] = []
retriever_kwargs = retriever_kwargs or {}
self.warm_up()
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
queries_results = executor.map(lambda query: self._run_on_thread(query, retriever_kwargs), queries)
for result in queries_results:
if not result:
continue
docs.extend(result)
# de-duplicate and sort
docs = _deduplicate_documents(docs)
docs.sort(key=lambda x: x.score or 0.0, reverse=True)
return {"documents": docs}
@component.output_types(documents=list[Document])
async def run_async(
self, queries: list[str], retriever_kwargs: dict[str, Any] | None = None
) -> dict[str, list[Document]]:
"""
Retrieve documents using multiple queries concurrently.
Uses the retriever's `run_async` method if available, otherwise falls back to running `run`
in a thread executor. Queries are processed concurrently using asyncio.gather.
:param queries: List of text queries to process.
:param retriever_kwargs: Optional dictionary of arguments to pass to the retriever's run method.
:returns:
A dictionary containing:
`documents`: List of retrieved documents sorted by relevance score.
"""
retriever_kwargs = retriever_kwargs or {}
await self.warm_up_async()
results = await asyncio.gather(*[self._run_one_async(q, retriever_kwargs) for q in queries])
docs: list[Document] = [doc for result in results if result for doc in result]
docs = _deduplicate_documents(docs)
docs.sort(key=lambda x: x.score or 0.0, reverse=True)
return {"documents": docs}
def _run_on_thread(self, query: str, retriever_kwargs: dict[str, Any] | None = None) -> list[Document] | None:
"""
Process a single query on a separate thread.
:param query: The text query to process.
:param retriever_kwargs: Optional dictionary of arguments to pass to the retriever's run method.
:returns:
List of retrieved documents or None if no results.
"""
result = self.retriever.run(query=query, **(retriever_kwargs or {}))
if result and "documents" in result:
return result["documents"]
return None
async def _run_one_async(self, query: str, retriever_kwargs: dict[str, Any]) -> list[Document] | None:
"""
Process a single query asynchronously.
:param query: The text query to process.
:param retriever_kwargs: Arguments to pass to the retriever's run method.
:returns:
List of retrieved documents or None if no results.
"""
result = await _execute_component_async(self.retriever, query=query, **retriever_kwargs)
if result and "documents" in result:
return result["documents"]
return None
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
The serialized component as a dictionary.
"""
return default_to_dict(
self, retriever=component_to_dict(obj=self.retriever, name="retriever"), max_workers=self.max_workers
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "MultiQueryTextRetriever":
"""
Deserializes the component from a dictionary.
:param data: The dictionary to deserialize from.
:returns:
The deserialized component.
"""
return default_from_dict(cls, data)
@@ -0,0 +1,363 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import asyncio
from concurrent.futures import ThreadPoolExecutor, as_completed
from math import inf
from typing import Any, Literal
from haystack import component, default_from_dict, default_to_dict
from haystack.components.retrievers.types.protocol import TextRetriever
from haystack.core.serialization import component_from_dict, component_to_dict, import_class_by_name
from haystack.dataclasses import Document
from haystack.utils.async_utils import _execute_component_async
from haystack.utils.experimental import _experimental
from haystack.utils.misc import _deduplicate_documents, _reciprocal_rank_fusion
@_experimental
@component
class MultiRetriever:
"""
A component that accepts text retrievers and runs them in parallel, combining their results.
> **Note:** This component is experimental and may change or be removed in future releases without prior
deprecation notice.
All retrievers must implement the `TextRetriever` protocol. Use `TextEmbeddingRetriever` to wrap an
embedding-based retriever before passing it to this component.
Each retriever is queried concurrently using a thread pool.
The results are deduplicated and returned as a single list of documents.
### Usage example
```python
from haystack import Document
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import DuplicatePolicy
from haystack.components.retrievers import InMemoryBM25Retriever, InMemoryEmbeddingRetriever
from haystack.components.retrievers import TextEmbeddingRetriever, MultiRetriever
from haystack.components.embedders import OpenAITextEmbedder, OpenAIDocumentEmbedder
from haystack.components.writers import DocumentWriter
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."),
]
# Populate the document store
doc_store = InMemoryDocumentStore()
doc_embedder = OpenAIDocumentEmbedder()
doc_writer = DocumentWriter(document_store=doc_store, policy=DuplicatePolicy.SKIP)
doc_writer.run(documents=doc_embedder.run(documents)["documents"])
# Run the multi-retriever with all retrievers
retriever = MultiRetriever(
retrievers={
"bm25": InMemoryBM25Retriever(document_store=doc_store),
"embedding": TextEmbeddingRetriever(
retriever=InMemoryEmbeddingRetriever(document_store=doc_store),
text_embedder=OpenAITextEmbedder(),
),
},
top_k=3,
)
# Run all retrievers
result = retriever.run(query="green energy sources")
# Run only the BM25 retriever
result = retriever.run(query="green energy sources", active_retrievers=["bm25"])
for doc in result["documents"]:
print(doc.content)
```
"""
def __init__(
self,
*,
retrievers: dict[str, TextRetriever],
filters: dict[str, Any] | None = None,
top_k_per_retriever: int | None = None,
top_k: int | None = None,
max_workers: int = 4,
join_mode: Literal["concatenate", "reciprocal_rank_fusion"] = "reciprocal_rank_fusion",
) -> None:
"""
Create the MultiRetriever component.
:param retrievers:
A dictionary mapping names to text retrievers (implementing the `TextRetriever` protocol) to run in
parallel.
:param filters:
A dictionary of filters to apply when retrieving documents.
:param top_k_per_retriever:
The maximum number of documents to return per retriever. If set, this will override the `top_k`
parameter for each retriever. If None, the `top_k` parameter of retrievers will be used.
:param top_k:
The maximum number of documents to return overall, extracted from the combined results of all
retrievers. When set, the results are always merged using reciprocal rank fusion (regardless of
`join_mode`) so that the combined list has a consistent global ranking before it is truncated to
`top_k`. If None, all results are returned.
:param max_workers:
The maximum number of threads to use for parallel retrieval.
:param join_mode:
How to merge results from multiple retrievers. Available modes:
- `concatenate`: Combines all results into a single list and deduplicates.
- `reciprocal_rank_fusion`: Deduplicates and assigns scores based on reciprocal rank fusion.
"""
self.retrievers = retrievers
self.filters = filters
self.top_k_per_retriever = top_k_per_retriever
self.top_k = top_k
self.max_workers = max_workers
self.join_mode = join_mode
def _merge_results(self, document_lists: list[list[Document]], top_k: int | None = None) -> list[Document]:
"""
Merge per-retriever result lists according to `join_mode`.
In `concatenate` mode, all lists are flattened and deduplicated. In `reciprocal_rank_fusion` mode, results
are deduplicated and re-scored using RRF, then returned in descending score order. When `top_k` is set, RRF
is always used so the combined results have a consistent global ranking, and only the top `top_k` documents
are returned.
"""
# When top_k is set we always use reciprocal rank fusion to merge the results, regardless of join_mode,
# so that truncation is applied to a consistently ranked list.
if top_k is not None or self.join_mode == "reciprocal_rank_fusion":
documents = _reciprocal_rank_fusion(document_lists)
merged = sorted(documents, key=lambda d: d.score if d.score is not None else -inf, reverse=True)
return merged[:top_k] if top_k is not None else merged
return _deduplicate_documents([doc for docs in document_lists for doc in docs])
def _resolve_retrievers(self, active_retrievers: list[str] | None) -> dict[str, TextRetriever]:
"""
Returns the subset of retrievers to run based on the active_retrievers list.
:param active_retrievers:
A list of retriever names to run. If None, all retrievers are returned.
:returns:
A dictionary of retriever names to retriever instances.
:raises ValueError:
If any name in `active_retrievers` does not match a retriever name.
"""
if active_retrievers is None:
return self.retrievers
unknown = set(active_retrievers) - self.retrievers.keys()
if unknown:
raise ValueError(
f"Unknown retriever name(s): {sorted(unknown)}. Available retrievers: {sorted(self.retrievers.keys())}"
)
return {name: self.retrievers[name] for name in active_retrievers}
def warm_up(self) -> None:
"""
Warm up the retrievers.
"""
for retriever in self.retrievers.values():
if hasattr(retriever, "warm_up"):
retriever.warm_up()
async def warm_up_async(self) -> None:
"""
Warm up the retrievers on the serving event loop.
"""
for retriever in self.retrievers.values():
if hasattr(retriever, "warm_up_async"):
await retriever.warm_up_async()
elif hasattr(retriever, "warm_up"):
retriever.warm_up()
def close(self) -> None:
"""
Release the retrievers' resources.
"""
for retriever in self.retrievers.values():
if hasattr(retriever, "close"):
retriever.close()
async def close_async(self) -> None:
"""
Release the retrievers' async resources.
"""
for retriever in self.retrievers.values():
if hasattr(retriever, "close_async"):
await retriever.close_async()
elif hasattr(retriever, "close"):
retriever.close()
@component.output_types(documents=list[Document])
def run(
self,
query: str,
filters: dict[str, Any] | None = None,
top_k_per_retriever: int | None = None,
top_k: int | None = None,
*,
active_retrievers: list[str] | None = None,
) -> dict[str, list[Document]]:
"""
Runs retrievers in parallel on the given query and returns deduplicated results.
:param query:
The query to run the retrievers on.
:param filters:
Filters to apply. Defaults to the value set at initialization.
:param top_k_per_retriever:
The maximum number of documents to return per retriever. When set, this will override the `top_k`
parameter for each retriever. If None, the `top_k` parameter set for retrievers will be used.
Defaults to the value set at initialization.
:param top_k:
The maximum number of documents to return overall, extracted from the combined results of all
retrievers. When set, the results are always merged using reciprocal rank fusion (regardless of
`join_mode`) so that the combined list has a consistent global ranking before it is truncated to
`top_k`. If None, all results are returned. Defaults to the value set at initialization.
:param active_retrievers:
Names of retrievers to run. Defaults to all. Must match keys in the `retrievers` dictionary.
:returns:
A dictionary with the keys:
- "documents": A deduplicated list of retrieved documents.
:raises ValueError:
If any name in `active_retrievers` does not match a retriever name.
"""
self.warm_up()
resolved_top_k_per_retriever = (
top_k_per_retriever if top_k_per_retriever is not None else self.top_k_per_retriever
)
resolved_top_k = top_k if top_k is not None else self.top_k
resolved_filters = filters if filters is not None else self.filters
retrievers_to_run = self._resolve_retrievers(active_retrievers)
results_by_name: dict[str, list[Document]] = {}
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
future_to_name = {}
for name, retriever in retrievers_to_run.items():
run_kwargs: dict[str, Any] = {"query": query}
if resolved_top_k_per_retriever is not None:
run_kwargs["top_k"] = resolved_top_k_per_retriever
if resolved_filters is not None:
run_kwargs["filters"] = resolved_filters
future_to_name[executor.submit(retriever.run, **run_kwargs)] = name
for future in as_completed(future_to_name):
name = future_to_name[future]
try:
results_by_name[name] = future.result().get("documents", [])
except Exception as e:
raise RuntimeError(f"Retriever '{name}' failed: {e}") from e
document_lists = [results_by_name[name] for name in retrievers_to_run]
return {"documents": self._merge_results(document_lists, top_k=resolved_top_k)}
@component.output_types(documents=list[Document])
async def run_async(
self,
query: str,
filters: dict[str, Any] | None = None,
top_k_per_retriever: int | None = None,
top_k: int | None = None,
*,
active_retrievers: list[str] | None = None,
) -> dict[str, list[Document]]:
"""
Runs retrievers concurrently on the given query and returns deduplicated results.
Uses each retriever's `run_async` method if available, otherwise runs `run` in a thread executor.
:param query:
The query to run the retrievers on.
:param filters:
Filters to apply. Defaults to the value set at initialization.
:param top_k_per_retriever:
The maximum number of documents to return per retriever. When set, this will override the `top_k`
parameter for each retriever. If None, the `top_k` parameter set for retrievers will be used.
Defaults to the value set at initialization.
:param top_k:
The maximum number of documents to return overall, extracted from the combined results of all
retrievers. When set, the results are always merged using reciprocal rank fusion (regardless of
`join_mode`) so that the combined list has a consistent global ranking before it is truncated to
`top_k`. If None, all results are returned. Defaults to the value set at initialization.
:param active_retrievers:
Names of retrievers to run. Defaults to all. Must match keys in the `retrievers` dictionary.
:returns:
A dictionary with the keys:
- "documents": A deduplicated list of retrieved documents.
:raises ValueError:
If any name in `active_retrievers` does not match a retriever name.
"""
await self.warm_up_async()
resolved_top_k_per_retriever = (
top_k_per_retriever if top_k_per_retriever is not None else self.top_k_per_retriever
)
resolved_top_k = top_k if top_k is not None else self.top_k
resolved_filters = filters if filters is not None else self.filters
retrievers_to_run = self._resolve_retrievers(active_retrievers)
run_kwargs: dict[str, Any] = {"query": query}
if resolved_top_k_per_retriever is not None:
run_kwargs["top_k"] = resolved_top_k_per_retriever
if resolved_filters is not None:
run_kwargs["filters"] = resolved_filters
async def _run_one(name: str, retriever: TextRetriever) -> list[Document]:
try:
result = await _execute_component_async(retriever, **run_kwargs)
return result.get("documents", [])
except Exception as e:
raise RuntimeError(f"Retriever '{name}' failed: {e}") from e
document_lists = list(await asyncio.gather(*[_run_one(name, r) for name, r in retrievers_to_run.items()]))
return {"documents": self._merge_results(document_lists, top_k=resolved_top_k)}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
retrievers={name: component_to_dict(obj=r, name=name) for name, r in self.retrievers.items()},
filters=self.filters,
top_k_per_retriever=self.top_k_per_retriever,
top_k=self.top_k,
max_workers=self.max_workers,
join_mode=self.join_mode,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "MultiRetriever":
"""
Creates an instance of the component from a dictionary.
:param data:
Dictionary with the data to create the component.
"""
retrievers_data = data.get("init_parameters", {}).get("retrievers", {})
if retrievers_data:
retrievers = {}
for name, retriever_data in retrievers_data.items():
try:
imported_class = import_class_by_name(retriever_data["type"])
except ImportError as e:
raise ImportError(
f"Could not import class {retriever_data['type']} for retriever '{name}'. Error: {str(e)}"
) from e
retrievers[name] = component_from_dict(cls=imported_class, data=retriever_data, name=name)
data["init_parameters"]["retrievers"] = retrievers
return default_from_dict(cls, data)
@@ -0,0 +1,321 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict, logging
from haystack.document_stores.types import DocumentStore
logger = logging.getLogger(__name__)
@component
class SentenceWindowRetriever:
"""
Retrieves neighboring documents from a DocumentStore to provide context for query results.
This component is intended to be used after a Retriever (e.g., BM25Retriever, EmbeddingRetriever).
It enhances retrieved results by fetching adjacent document chunks to give
additional context for the user.
The documents must include metadata indicating their origin and position:
- `source_id` is used to group sentence chunks belonging to the same original document.
- `split_id` represents the position/order of the chunk within the document.
The number of adjacent documents to include on each side of the retrieved document can be configured using the
`window_size` parameter. You can also specify which metadata fields to use for source and split ID
via `source_id_meta_field` and `split_id_meta_field`.
The SentenceWindowRetriever is compatible with the following DocumentStores:
- [Astra](https://docs.haystack.deepset.ai/docs/astradocumentstore)
- [Elasticsearch](https://docs.haystack.deepset.ai/docs/elasticsearch-document-store)
- [OpenSearch](https://docs.haystack.deepset.ai/docs/opensearch-document-store)
- [Pgvector](https://docs.haystack.deepset.ai/docs/pgvectordocumentstore)
- [Pinecone](https://docs.haystack.deepset.ai/docs/pinecone-document-store)
- [Qdrant](https://docs.haystack.deepset.ai/docs/qdrant-document-store)
### Usage example
```python
from haystack import Document, Pipeline
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack.components.retrievers import SentenceWindowRetriever
from haystack.components.preprocessors import DocumentSplitter
from haystack.document_stores.in_memory import InMemoryDocumentStore
splitter = DocumentSplitter(split_length=10, split_overlap=5, split_by="word")
text = (
"This is a text with some words. There is a second sentence. And there is also a third sentence. "
"It also contains a fourth sentence. And a fifth sentence. And a sixth sentence. And a seventh sentence"
)
doc = Document(content=text)
docs = splitter.run([doc])
doc_store = InMemoryDocumentStore()
doc_store.write_documents(docs["documents"])
rag = Pipeline()
rag.add_component("bm25_retriever", InMemoryBM25Retriever(doc_store, top_k=1))
rag.add_component("sentence_window_retriever", SentenceWindowRetriever(document_store=doc_store, window_size=2))
rag.connect("bm25_retriever", "sentence_window_retriever")
rag.run({'bm25_retriever': {"query":"third"}})
# >> {'sentence_window_retriever': {'context_windows': ['some words. There is a second sentence.
# >> And there is also a third sentence. It also contains a fourth sentence. And a fifth sentence. And a sixth
# >> sentence. And a'], 'context_documents': [[Document(id=..., content: 'some words. There is a second sentence.
# >> And there is ', meta: {'source_id': '...', 'page_number': 1, 'split_id': 1, 'split_idx_start': 20,
# >> '_split_overlap': [{'doc_id': '...', 'range': (20, 43)}, {'doc_id': '...', 'range': (0, 30)}]}),
# >> Document(id=..., content: 'second sentence. And there is also a third sentence. It ',
# >> meta: {'source_id': '74ea87deb38012873cf8c07e...f19d01a26a098447113e1d7b83efd30c02987114', 'page_number': 1,
# >> 'split_id': 2, 'split_idx_start': 43, '_split_overlap': [{'doc_id': '...', 'range': (23, 53)}, {'doc_id': '.',
# >> 'range': (0, 26)}]}), Document(id=..., content: 'also a third sentence. It also contains a fourth sentence. ',
# >> meta: {'source_id': '...', 'page_number': 1, 'split_id': 3, 'split_idx_start': 73, '_split_overlap':
# >> [{'doc_id': '...', 'range': (30, 56)}, {'doc_id': '...', 'range': (0, 33)}]}), Document(id=..., content:
# >> 'also contains a fourth sentence. And a fifth sentence. And ', meta: {'source_id': '...', 'page_number': 1,
# >> 'split_id': 4, 'split_idx_start': 99, '_split_overlap': [{'doc_id': '...', 'range': (26, 59)},
# >> {'doc_id': '...', 'range': (0, 26)}]}), Document(id=..., content: 'And a fifth sentence. And a sixth sentence.
# >> And a ', meta: {'source_id': '...', 'page_number': 1, 'split_id': 5, 'split_idx_start': 132,
# >> '_split_overlap': [{'doc_id': '...', 'range': (33, 59)}, {'doc_id': '...', 'range': (0, 24)}]})]]}}}}
```
"""
def __init__(
self,
document_store: DocumentStore,
window_size: int = 3,
*,
source_id_meta_field: str | list[str] = "source_id",
split_id_meta_field: str = "split_id",
raise_on_missing_meta_fields: bool = True,
) -> None:
"""
Creates a new SentenceWindowRetriever component.
:param document_store: The Document Store to retrieve the surrounding documents from.
:param window_size: The number of documents to retrieve before and after the relevant one.
For example, `window_size: 2` fetches 2 preceding and 2 following documents.
:param source_id_meta_field: The metadata field that contains the source ID of the document.
This can be a single field or a list of fields. If multiple fields are provided, the retriever will
consider the document as part of the same source if all the fields match.
:param split_id_meta_field: The metadata field that contains the split ID of the document.
:param raise_on_missing_meta_fields: If True, raises an error if the documents do not contain the required
metadata fields. If False, it will skip retrieving the context for documents that are missing
the required metadata fields, but will still include the original document in the results.
"""
if window_size < 1:
raise ValueError("The window_size parameter must be greater than 0.")
self.window_size = window_size
self.document_store = document_store
self.source_id_meta_field = source_id_meta_field
# Use this to have an attribute that is always a list of source id meta fields.
self._source_id_meta_fields = (
source_id_meta_field if isinstance(source_id_meta_field, list) else [source_id_meta_field]
)
self.split_id_meta_field = split_id_meta_field
self.raise_on_missing_meta_fields = raise_on_missing_meta_fields
@staticmethod
def merge_documents_text(documents: list[Document]) -> str:
"""
Merge a list of document text into a single string.
This functions concatenates the textual content of a list of documents into a single string, eliminating any
overlapping content.
:param documents: List of Documents to merge.
"""
if any("split_idx_start" not in doc.meta for doc in documents):
# If any of the documents is missing the 'split_idx_start' metadata we just concatenate their content.
return "".join(doc.content for doc in documents if doc.content)
sorted_docs = sorted(documents, key=lambda doc: doc.meta["split_idx_start"])
merged_text = ""
last_idx_end = 0
for doc in sorted_docs:
if doc.content is None:
continue
start = doc.meta.get("split_idx_start", 0) # start of the current content
# if the start of the current content is before the end of the last appended content, adjust it
start = max(start, last_idx_end)
# append the non-overlapping part to the merged text
merged_text += doc.content[start - int(doc.meta["split_idx_start"]) :]
# update the last end index
last_idx_end = int(doc.meta["split_idx_start"]) + len(doc.content)
return merged_text
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(
self,
document_store=self.document_store,
window_size=self.window_size,
source_id_meta_field=self.source_id_meta_field,
split_id_meta_field=self.split_id_meta_field,
raise_on_missing_meta_fields=self.raise_on_missing_meta_fields,
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "SentenceWindowRetriever":
"""
Deserializes the component from a dictionary.
:returns:
Deserialized component.
"""
return default_from_dict(cls, data)
@component.output_types(context_windows=list[str], context_documents=list[Document])
def run(self, retrieved_documents: list[Document], window_size: int | None = None) -> dict[str, Any]:
"""
Based on the `source_id` and on the `doc.meta['split_id']` get surrounding documents from the document store.
Implements the logic behind the sentence-window technique, retrieving the surrounding documents of a given
document from the document store.
:param retrieved_documents: List of retrieved documents from the previous retriever.
:param window_size: The number of documents to retrieve before and after the relevant one. This will overwrite
the `window_size` parameter set in the constructor.
:returns:
A dictionary with the following keys:
- `context_windows`: A list of strings, where each string represents the concatenated text from the
context window of the corresponding document in `retrieved_documents`.
- `context_documents`: A list `Document` objects, containing the retrieved documents plus the context
document surrounding them. The documents are sorted by the `split_idx_start`
meta field.
"""
window_size = window_size or self.window_size
SentenceWindowRetriever._raise_if_windows_size_is_negative(window_size)
self._raise_if_documents_do_not_have_expected_metadata(retrieved_documents)
context_text = []
context_documents = []
for doc in retrieved_documents:
text, docs = self._retrieve_context_for_document(doc, window_size)
context_text.append(text)
context_documents.extend(docs)
return {"context_windows": context_text, "context_documents": context_documents}
@component.output_types(context_windows=list[str], context_documents=list[Document])
async def run_async(self, retrieved_documents: list[Document], window_size: int | None = None) -> dict[str, Any]:
"""
Based on the `source_id` and on the `doc.meta['split_id']` get surrounding documents from the document store.
Implements the logic behind the sentence-window technique, retrieving the surrounding documents of a given
document from the document store.
:param retrieved_documents: List of retrieved documents from the previous retriever.
:param window_size: The number of documents to retrieve before and after the relevant one. This will overwrite
the `window_size` parameter set in the constructor.
:returns:
A dictionary with the following keys:
- `context_windows`: A list of strings, where each string represents the concatenated text from the
context window of the corresponding document in `retrieved_documents`.
- `context_documents`: A list `Document` objects, containing the retrieved documents plus the context
document surrounding them. The documents are sorted by the `split_idx_start`
meta field.
"""
window_size = window_size or self.window_size
SentenceWindowRetriever._raise_if_windows_size_is_negative(window_size)
self._raise_if_documents_do_not_have_expected_metadata(retrieved_documents)
context_text = []
context_documents = []
for doc in retrieved_documents:
text, docs = await self._retrieve_context_for_document_async(doc, window_size)
context_text.append(text)
context_documents.extend(docs)
return {"context_windows": context_text, "context_documents": context_documents}
@staticmethod
def _raise_if_windows_size_is_negative(window_size: int) -> None:
if window_size < 1:
raise ValueError("The window_size parameter must be greater than 0.")
def _raise_if_documents_do_not_have_expected_metadata(self, retrieved_documents: list[Document]) -> None:
if (
not all(self.split_id_meta_field in doc.meta for doc in retrieved_documents)
and self.raise_on_missing_meta_fields
):
raise ValueError(f"The retrieved documents must have '{self.split_id_meta_field}' in their metadata.")
if (
not all(field in doc.meta for doc in retrieved_documents for field in self._source_id_meta_fields)
and self.raise_on_missing_meta_fields
):
raise ValueError(f"The retrieved documents must have '{self.source_id_meta_field}' in their metadata.")
def _retrieve_context_for_document(self, doc: Document, window_size: int) -> tuple[str, list[Document]]:
source_ids = [doc.meta.get(field) for field in self._source_id_meta_fields]
split_id = doc.meta.get(self.split_id_meta_field)
if any(source_id is None for source_id in source_ids) or split_id is None:
logger.warning(
"Document {doc_id} is missing required metadata fields to be used with "
"SentenceWindowRetriever: {source_id} or {split_id}. Skipping context retrieval for this document.",
doc_id=doc.id,
source_id=self._source_id_meta_fields,
split_id=self.split_id_meta_field,
)
return doc.content or "", [doc]
assert split_id is not None
filter_conditions = self._build_filter_conditions(split_id, window_size, source_ids)
context_docs = self.document_store.filter_documents(filter_conditions)
context_text = self.merge_documents_text(context_docs)
context_docs_sorted = sorted(context_docs, key=lambda doc: doc.meta[self.split_id_meta_field])
return context_text, context_docs_sorted
async def _retrieve_context_for_document_async(self, doc: Document, window_size: int) -> tuple[str, list[Document]]:
source_ids = [doc.meta.get(field) for field in self._source_id_meta_fields]
split_id = doc.meta.get(self.split_id_meta_field)
if any(source_id is None for source_id in source_ids) or split_id is None:
logger.warning(
"Document {doc_id} is missing required metadata fields to be used with "
"SentenceWindowRetriever: {source_id} or {split_id}. Skipping context retrieval for this document.",
doc_id=doc.id,
source_id=self._source_id_meta_fields,
split_id=self.split_id_meta_field,
)
return doc.content or "", [doc]
assert split_id is not None
filter_conditions = self._build_filter_conditions(split_id, window_size, source_ids)
# Ignoring type error because DocumentStore protocol doesn't define filter_documents_async
context_docs = await self.document_store.filter_documents_async(filter_conditions) # type: ignore[attr-defined]
context_text = self.merge_documents_text(context_docs)
context_docs_sorted = sorted(context_docs, key=lambda doc: doc.meta[self.split_id_meta_field])
return context_text, context_docs_sorted
def _build_filter_conditions(self, split_id: int, window_size: int, source_ids: list[Any]) -> dict[str, Any]:
min_before = split_id - window_size
max_after = split_id + window_size
source_id_filters = [
{"field": f"meta.{source_id_meta_field}", "operator": "==", "value": source_id}
for source_id_meta_field, source_id in zip(self._source_id_meta_fields, source_ids, strict=True)
]
conditions = [
{"field": f"meta.{self.split_id_meta_field}", "operator": ">=", "value": min_before},
{"field": f"meta.{self.split_id_meta_field}", "operator": "<=", "value": max_after},
*source_id_filters,
]
return {"operator": "AND", "conditions": conditions}
@@ -0,0 +1,181 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any
from haystack import Document, component, default_from_dict, default_to_dict
from haystack.components.embedders.types.protocol import TextEmbedder
from haystack.components.retrievers.types import EmbeddingRetriever
from haystack.core.serialization import component_to_dict
from haystack.utils.async_utils import _execute_component_async
@component
class TextEmbeddingRetriever:
"""
A component that retrieves documents using a query with an embedding-based retriever.
This component takes a text query, converts it to an embedding using a text embedder, and then uses an
embedding-based retriever to find relevant documents.
The results are sorted by relevance score.
### Usage example
```python
from haystack import Document
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import DuplicatePolicy
from haystack.components.embedders import OpenAITextEmbedder, OpenAIDocumentEmbedder
from haystack.components.retrievers import InMemoryEmbeddingRetriever, TextEmbeddingRetriever
from haystack.components.writers import DocumentWriter
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="Geothermal energy is heat that comes from the sub-surface of the earth."),
Document(content="Biomass energy is produced from organic materials, such as plant and animal waste."),
Document(content="Fossil fuels, such as coal, oil, and natural gas, are non-renewable energy sources."),
]
# Populate the document store
doc_store = InMemoryDocumentStore()
doc_embedder = OpenAIDocumentEmbedder()
doc_writer = DocumentWriter(document_store=doc_store, policy=DuplicatePolicy.SKIP)
documents = doc_embedder.run(documents)["documents"]
doc_writer.run(documents=documents)
# Run the retriever
in_memory_retriever = InMemoryEmbeddingRetriever(document_store=doc_store, top_k=1)
text_embedder = OpenAITextEmbedder()
retriever = TextEmbeddingRetriever(retriever=in_memory_retriever, text_embedder=text_embedder)
result = retriever.run(query="Geothermal energy")
for doc in result["documents"]:
print(f"Content: {doc.content}, Score: {doc.score}")
# >> Content: Geothermal energy is heat that comes from the sub-surface of the earth., Score: 0.8509603046266574
```
"""
def __init__(self, *, retriever: EmbeddingRetriever, text_embedder: TextEmbedder) -> None:
"""
Initialize TextEmbeddingRetriever.
:param retriever: The embedding-based retriever to use for document retrieval.
:param text_embedder: The text embedder to convert a text query to an embedding.
"""
self.retriever = retriever
self.text_embedder = text_embedder
def warm_up(self) -> None:
"""
Warm up the text embedder and the retriever.
"""
for inner in (self.text_embedder, self.retriever):
if hasattr(inner, "warm_up"):
inner.warm_up()
async def warm_up_async(self) -> None:
"""
Warm up the text embedder and the retriever on the serving event loop.
"""
for inner in (self.text_embedder, self.retriever):
if hasattr(inner, "warm_up_async"):
await inner.warm_up_async()
elif hasattr(inner, "warm_up"):
inner.warm_up()
def close(self) -> None:
"""
Release the text embedder's and the retriever's resources.
"""
for inner in (self.text_embedder, self.retriever):
if hasattr(inner, "close"):
inner.close()
async def close_async(self) -> None:
"""
Release the text embedder's and the retriever's async resources.
"""
for inner in (self.text_embedder, self.retriever):
if hasattr(inner, "close_async"):
await inner.close_async()
elif hasattr(inner, "close"):
inner.close()
@component.output_types(documents=list[Document])
def run(
self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None
) -> dict[str, list[Document]]:
"""
Retrieve documents using a single query.
:param query: The query to retrieve documents for.
:param filters: A dictionary of filters to apply when retrieving documents.
:param top_k: The maximum number of documents to return.
:returns:
A dictionary containing:
- `documents`: List of retrieved documents sorted by relevance score.
"""
self.warm_up()
embedding_result = self.text_embedder.run(text=query)
result = self.retriever.run(query_embedding=embedding_result["embedding"], filters=filters, top_k=top_k)
docs: list[Document] = result["documents"]
# sort
docs.sort(key=lambda x: x.score or 0.0, reverse=True)
return {"documents": docs}
@component.output_types(documents=list[Document])
async def run_async(
self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None
) -> dict[str, list[Document]]:
"""
Retrieve documents using a single query asynchronously.
Uses `run_async` on the text embedder and retriever if available, otherwise falls back to
running `run` in a thread executor.
:param query: The query to retrieve documents for.
:param filters: A dictionary of filters to apply when retrieving documents.
:param top_k: The maximum number of documents to return.
:returns:
A dictionary containing:
- `documents`: List of retrieved documents sorted by relevance score.
"""
await self.warm_up_async()
embedding_result = await _execute_component_async(self.text_embedder, text=query)
result = await _execute_component_async(
self.retriever, query_embedding=embedding_result["embedding"], filters=filters, top_k=top_k
)
docs: list[Document] = result["documents"]
docs.sort(key=lambda x: x.score or 0.0, reverse=True)
return {"documents": docs}
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
A dictionary representing the serialized component.
"""
return default_to_dict(
self,
retriever=component_to_dict(obj=self.retriever, name="retriever"),
text_embedder=component_to_dict(obj=self.text_embedder, name="text_embedder"),
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "TextEmbeddingRetriever":
"""
Deserializes the component from a dictionary.
:param data: The dictionary to deserialize from.
:returns:
The deserialized component.
"""
return default_from_dict(cls, data)
@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from .protocol import EmbeddingRetriever, TextRetriever
__all__ = ["TextRetriever", "EmbeddingRetriever"]
@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any, Protocol
class TextRetriever(Protocol):
"""
This protocol defines the minimal interface that all keyword-based BM25 Retrievers must implement.
Retrievers are components that process a query and, based on that query, return relevant documents from a document
store or other data source. They return a dictionary with a list of Document objects.
"""
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None) -> dict[str, Any]:
"""
Retrieve documents that are relevant to the query.
Implementing classes may accept additional optional parameters in their run method.
:param query: The input query string.
:param filters: A dictionary of filters to apply when retrieving documents.
:param top_k: The maximum number of documents to return.
:returns:
A dictionary containing:
`documents`: List of retrieved documents sorted by relevance score.
"""
...
class EmbeddingRetriever(Protocol):
"""
This protocol defines the minimal interface that all embedding-based Retrievers must implement.
Retrievers are components that process a query and, based on that query, return relevant documents from a document
store or other data source. They return a dictionary with a list of Document objects.
"""
def run(
self, query_embedding: list[float], filters: dict[str, Any] | None = None, top_k: int | None = None
) -> dict[str, Any]:
"""
Retrieve documents that are relevant to the query.
Implementing classes may accept additional optional parameters in their run method.
:param query_embedding: The input query embedding.
:param filters: A dictionary of filters to apply when retrieving documents.
:param top_k: The maximum number of documents to return.
:returns:
A dictionary containing:
`documents`: List of retrieved documents sorted by relevance score.
"""
...