Files
deepset-ai--haystack/haystack/components/evaluators/document_mrr.py
T
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

131 lines
5.0 KiB
Python

# 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_to_dict
@component
class DocumentMRREvaluator:
"""
Evaluator that calculates the mean reciprocal rank of the retrieved documents.
MRR measures how high the first retrieved document is ranked.
Each question can have multiple ground truth documents and multiple retrieved documents.
`DocumentMRREvaluator` doesn't normalize its inputs, the `DocumentCleaner` component
should be used to clean and normalize the documents before passing them to this evaluator.
Usage example:
```python
from haystack import Document
from haystack.components.evaluators import DocumentMRREvaluator
evaluator = DocumentMRREvaluator()
result = evaluator.run(
ground_truth_documents=[
[Document(content="France")],
[Document(content="9th century"), Document(content="9th")],
],
retrieved_documents=[
[Document(content="France")],
[Document(content="9th century"), Document(content="10th century"), Document(content="9th")],
],
)
print(result["individual_scores"])
# [1.0, 1.0]
print(result["score"])
# 1.0
```
"""
def __init__(self, document_comparison_field: str = "content") -> None:
"""
Create a DocumentMRREvaluator component.
:param document_comparison_field:
The Document field to use for comparison. Possible options:
- `"content"`: uses `doc.content`
- `"id"`: uses `doc.id`
- A `meta.` prefix followed by a key name: uses `doc.meta["<key>"]`
(e.g. `"meta.file_id"`, `"meta.page_number"`)
Nested keys are supported (e.g. `"meta.source.url"`).
"""
self.document_comparison_field = document_comparison_field
def _get_comparison_value(self, doc: Document) -> Any:
"""
Extract the comparison value from a document based on the configured field.
"""
if self.document_comparison_field == "content":
return doc.content
if self.document_comparison_field == "id":
return doc.id
if self.document_comparison_field.startswith("meta."):
parts = self.document_comparison_field[5:].split(".")
value = doc.meta
for part in parts:
if not isinstance(value, dict) or part not in value:
return None
value = value[part]
return value
msg = (
f"Unsupported document_comparison_field: '{self.document_comparison_field}'. "
"Use 'content', 'id', or 'meta.<key>'."
)
raise ValueError(msg)
def to_dict(self) -> dict[str, Any]:
"""
Serializes the component to a dictionary.
:returns:
Dictionary with serialized data.
"""
return default_to_dict(self, document_comparison_field=self.document_comparison_field)
# Refer to https://www.pinecone.io/learn/offline-evaluation/ for the algorithm.
@component.output_types(score=float, individual_scores=list[float])
def run(
self, ground_truth_documents: list[list[Document]], retrieved_documents: list[list[Document]]
) -> dict[str, Any]:
"""
Run the DocumentMRREvaluator on the given inputs.
`ground_truth_documents` and `retrieved_documents` must have the same length.
:param ground_truth_documents:
A list of expected documents for each question.
:param retrieved_documents:
A list of retrieved documents for each question.
:returns:
A dictionary with the following outputs:
- `score` - The average of calculated scores.
- `individual_scores` - A list of numbers from 0.0 to 1.0 that represents how high the first retrieved
document is ranked.
"""
if len(ground_truth_documents) != len(retrieved_documents):
msg = "The length of ground_truth_documents and retrieved_documents must be the same."
raise ValueError(msg)
individual_scores = []
for ground_truth, retrieved in zip(ground_truth_documents, retrieved_documents, strict=True):
reciprocal_rank = 0.0
ground_truth_values = [val for doc in ground_truth if (val := self._get_comparison_value(doc)) is not None]
for rank, retrieved_document in enumerate(retrieved):
retrieved_value = self._get_comparison_value(retrieved_document)
if retrieved_value is None:
continue
if retrieved_value in ground_truth_values:
reciprocal_rank = 1 / (rank + 1)
break
individual_scores.append(reciprocal_rank)
score = sum(individual_scores) / len(ground_truth_documents)
return {"score": score, "individual_scores": individual_scores}