Coverage for haystack/components/evaluators/document_map.py: 92%
52 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from typing import Any
7from haystack import Document, component, default_to_dict
10@component
11class DocumentMAPEvaluator:
12 """
13 A Mean Average Precision (MAP) evaluator for documents.
15 Evaluator that calculates the mean average precision of the retrieved documents, a metric
16 that measures how high retrieved documents are ranked.
17 Each question can have multiple ground truth documents and multiple retrieved documents.
19 `DocumentMAPEvaluator` doesn't normalize its inputs, the `DocumentCleaner` component
20 should be used to clean and normalize the documents before passing them to this evaluator.
22 Usage example:
23 ```python
24 from haystack import Document
25 from haystack.components.evaluators import DocumentMAPEvaluator
27 evaluator = DocumentMAPEvaluator()
28 result = evaluator.run(
29 ground_truth_documents=[
30 [Document(content="France")],
31 [Document(content="9th century"), Document(content="9th")],
32 ],
33 retrieved_documents=[
34 [Document(content="France")],
35 [Document(content="9th century"), Document(content="10th century"), Document(content="9th")],
36 ],
37 )
39 print(result["individual_scores"])
40 # [1.0, 0.8333333333333333]
41 print(result["score"])
42 # 0.9166666666666666
43 ```
44 """
46 def __init__(self, document_comparison_field: str = "content") -> None:
47 """
48 Create a DocumentMAPEvaluator component.
50 :param document_comparison_field:
51 The Document field to use for comparison. Possible options:
52 - `"content"`: uses `doc.content`
53 - `"id"`: uses `doc.id`
54 - A `meta.` prefix followed by a key name: uses `doc.meta["<key>"]`
55 (e.g. `"meta.file_id"`, `"meta.page_number"`)
56 Nested keys are supported (e.g. `"meta.source.url"`).
57 """
58 self.document_comparison_field = document_comparison_field
60 def _get_comparison_value(self, doc: Document) -> Any:
61 """
62 Extract the comparison value from a document based on the configured field.
63 """
64 if self.document_comparison_field == "content":
65 return doc.content
66 if self.document_comparison_field == "id":
67 return doc.id
68 if self.document_comparison_field.startswith("meta."):
69 parts = self.document_comparison_field[5:].split(".")
70 value = doc.meta
71 for part in parts:
72 if not isinstance(value, dict) or part not in value:
73 return None
74 value = value[part]
75 return value
76 msg = (
77 f"Unsupported document_comparison_field: '{self.document_comparison_field}'. "
78 "Use 'content', 'id', or 'meta.<key>'."
79 )
80 raise ValueError(msg)
82 def to_dict(self) -> dict[str, Any]:
83 """
84 Serializes the component to a dictionary.
86 :returns:
87 Dictionary with serialized data.
88 """
89 return default_to_dict(self, document_comparison_field=self.document_comparison_field)
91 # Refer to https://www.pinecone.io/learn/offline-evaluation/ for the algorithm.
92 @component.output_types(score=float, individual_scores=list[float])
93 def run(
94 self, ground_truth_documents: list[list[Document]], retrieved_documents: list[list[Document]]
95 ) -> dict[str, Any]:
96 """
97 Run the DocumentMAPEvaluator on the given inputs.
99 All lists must have the same length.
101 :param ground_truth_documents:
102 A list of expected documents for each question.
103 :param retrieved_documents:
104 A list of retrieved documents for each question.
105 :returns:
106 A dictionary with the following outputs:
107 - `score` - The average of calculated scores.
108 - `individual_scores` - A list of numbers from 0.0 to 1.0 that represents how high retrieved documents
109 are ranked.
110 """
111 if len(ground_truth_documents) != len(retrieved_documents):
112 msg = "The length of ground_truth_documents and retrieved_documents must be the same."
113 raise ValueError(msg)
115 individual_scores = []
117 for ground_truth, retrieved in zip(ground_truth_documents, retrieved_documents, strict=True):
118 average_precision = 0.0
119 average_precision_numerator = 0.0
120 retrieved_relevant_documents = 0
122 # A list keeps the deduplication working for unhashable comparison values, for example when
123 # document_comparison_field points to a meta key holding a list.
124 uncredited_ground_truth_values: list[Any] = []
125 for doc in ground_truth:
126 value = self._get_comparison_value(doc)
127 if value is not None and value not in uncredited_ground_truth_values:
128 uncredited_ground_truth_values.append(value)
130 total_relevant_documents = len(uncredited_ground_truth_values)
131 for rank, retrieved_document in enumerate(retrieved):
132 retrieved_value = self._get_comparison_value(retrieved_document)
133 if retrieved_value is None:
134 continue
136 if retrieved_value in uncredited_ground_truth_values:
137 uncredited_ground_truth_values.remove(retrieved_value)
138 retrieved_relevant_documents += 1
139 average_precision_numerator += retrieved_relevant_documents / (rank + 1)
140 if total_relevant_documents:
141 average_precision = average_precision_numerator / total_relevant_documents
142 individual_scores.append(average_precision)
144 score = sum(individual_scores) / len(ground_truth_documents)
145 return {"score": score, "individual_scores": individual_scores}