Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

Commit 64efaa7

Browse files
feat: add optional BM25 retrieval confidence metadata
1 parent b281672 commit 64efaa7

3 files changed

Lines changed: 80 additions & 1 deletion

File tree

haystack/components/retrievers/in_memory/bm25_retriever.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __init__(
4444
filters: dict[str, Any] | None = None,
4545
top_k: int = 10,
4646
scale_score: bool = False,
47+
include_confidence: bool = False,
4748
filter_policy: FilterPolicy = FilterPolicy.REPLACE,
4849
) -> None:
4950
"""
@@ -58,6 +59,10 @@ def __init__(
5859
:param scale_score:
5960
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
6061
When `False`, uses raw similarity scores.
62+
:param include_confidence:
63+
When `True`, adds optional retrieval confidence metadata to returned documents when `scale_score` is also
64+
`True`. The metadata is exposed via `Document.meta["retrieval_confidence"]` and
65+
`Document.meta["retrieval_confidence_source"]`.
6166
:param filter_policy: The filter policy to apply during retrieval.
6267
Filter policy determines how filters are applied when retrieving documents. You can choose:
6368
- `REPLACE` (default): Overrides the initialization filters with the filters specified at runtime.
@@ -78,6 +83,7 @@ def __init__(
7883
self.filters = filters
7984
self.top_k = top_k
8085
self.scale_score = scale_score
86+
self.include_confidence = include_confidence
8187
self.filter_policy = filter_policy
8288

8389
def _get_telemetry_data(self) -> dict[str, Any]:
@@ -99,6 +105,7 @@ def to_dict(self) -> dict[str, Any]:
99105
filters=self.filters,
100106
top_k=self.top_k,
101107
scale_score=self.scale_score,
108+
include_confidence=self.include_confidence,
102109
filter_policy=self.filter_policy.value,
103110
)
104111

@@ -124,6 +131,7 @@ def run(
124131
filters: dict[str, Any] | None = None,
125132
top_k: int | None = None,
126133
scale_score: bool | None = None,
134+
include_confidence: bool | None = None,
127135
) -> dict[str, list[Document]]:
128136
"""
129137
Run the InMemoryBM25Retriever on the given input data.
@@ -137,6 +145,9 @@ def run(
137145
:param scale_score:
138146
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
139147
When `False`, uses raw similarity scores.
148+
:param include_confidence:
149+
When `True`, adds optional retrieval confidence metadata to returned documents when `scale_score` is also
150+
`True`. When `False`, no retrieval confidence metadata is added.
140151
:returns:
141152
The retrieved documents.
142153
@@ -151,8 +162,12 @@ def run(
151162
top_k = self.top_k
152163
if scale_score is None:
153164
scale_score = self.scale_score
165+
if include_confidence is None:
166+
include_confidence = self.include_confidence
154167

155168
docs = self.document_store.bm25_retrieval(query=query, filters=filters, top_k=top_k, scale_score=scale_score)
169+
if include_confidence and scale_score:
170+
self._add_confidence_metadata(docs)
156171
return {"documents": docs}
157172

158173
@component.output_types(documents=list[Document])
@@ -162,6 +177,7 @@ async def run_async(
162177
filters: dict[str, Any] | None = None,
163178
top_k: int | None = None,
164179
scale_score: bool | None = None,
180+
include_confidence: bool | None = None,
165181
) -> dict[str, list[Document]]:
166182
"""
167183
Run the InMemoryBM25Retriever on the given input data.
@@ -175,6 +191,9 @@ async def run_async(
175191
:param scale_score:
176192
When `True`, scales the score of retrieved documents to a range of 0 to 1, where 1 means extremely relevant.
177193
When `False`, uses raw similarity scores.
194+
:param include_confidence:
195+
When `True`, adds optional retrieval confidence metadata to returned documents when `scale_score` is also
196+
`True`. When `False`, no retrieval confidence metadata is added.
178197
:returns:
179198
The retrieved documents.
180199
@@ -189,8 +208,20 @@ async def run_async(
189208
top_k = self.top_k
190209
if scale_score is None:
191210
scale_score = self.scale_score
211+
if include_confidence is None:
212+
include_confidence = self.include_confidence
192213

193214
docs = await self.document_store.bm25_retrieval_async(
194215
query=query, filters=filters, top_k=top_k, scale_score=scale_score
195216
)
217+
if include_confidence and scale_score:
218+
self._add_confidence_metadata(docs)
196219
return {"documents": docs}
220+
221+
@staticmethod
222+
def _add_confidence_metadata(documents: list[Document]) -> None:
223+
for document in documents:
224+
if document.score is None:
225+
continue
226+
document.meta["retrieval_confidence"] = document.score
227+
document.meta["retrieval_confidence_source"] = "bm25_scaled_score"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
enhancements:
3+
- |
4+
Adds an optional `include_confidence` parameter to `InMemoryBM25Retriever`.
5+
When enabled together with `scale_score=True`, returned documents now expose
6+
BM25 retrieval confidence metadata via `Document.meta["retrieval_confidence"]`
7+
and `Document.meta["retrieval_confidence_source"]` without changing
8+
`Document.score` semantics.

test/components/retrievers/test_in_memory_bm25_retriever.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
import asyncio
56
from typing import Any
67

78
import pytest
@@ -31,12 +32,16 @@ def test_init_default(self, in_memory_doc_store):
3132
assert retriever.filters is None
3233
assert retriever.top_k == 10
3334
assert retriever.scale_score is False
35+
assert retriever.include_confidence is False
3436

3537
def test_init_with_parameters(self, in_memory_doc_store):
36-
retriever = InMemoryBM25Retriever(in_memory_doc_store, filters={"name": "test.txt"}, top_k=5, scale_score=True)
38+
retriever = InMemoryBM25Retriever(
39+
in_memory_doc_store, filters={"name": "test.txt"}, top_k=5, scale_score=True, include_confidence=True
40+
)
3741
assert retriever.filters == {"name": "test.txt"}
3842
assert retriever.top_k == 5
3943
assert retriever.scale_score
44+
assert retriever.include_confidence is True
4045

4146
def test_init_with_invalid_top_k_parameter(self, in_memory_doc_store):
4247
with pytest.raises(ValueError):
@@ -56,6 +61,7 @@ def test_to_dict(self):
5661
"filters": None,
5762
"top_k": 10,
5863
"scale_score": False,
64+
"include_confidence": False,
5965
"filter_policy": "replace",
6066
},
6167
}
@@ -78,6 +84,7 @@ def test_to_dict_with_custom_init_parameters(self):
7884
"filters": {"name": "test.txt"},
7985
"top_k": 5,
8086
"scale_score": True,
87+
"include_confidence": False,
8188
"filter_policy": "replace",
8289
},
8390
}
@@ -99,6 +106,7 @@ def test_from_dict(self):
99106
assert component.filters == {"name": "test.txt"}
100107
assert component.top_k == 5
101108
assert component.scale_score is False
109+
assert component.include_confidence is False
102110
assert component.filter_policy == FilterPolicy.REPLACE
103111

104112
def test_from_dict_without_docstore(self):
@@ -140,6 +148,38 @@ def test_invalid_run_wrong_store_type(self):
140148
with pytest.raises(TypeError, match="document_store must be an instance of InMemoryDocumentStore"):
141149
InMemoryBM25Retriever(SomeOtherDocumentStore())
142150

151+
def test_run_with_include_confidence_adds_metadata_when_scaled(self, in_memory_doc_store, mock_docs):
152+
in_memory_doc_store.write_documents(mock_docs)
153+
154+
retriever = InMemoryBM25Retriever(in_memory_doc_store, top_k=3, scale_score=True, include_confidence=True)
155+
result = retriever.run(query="PHP")
156+
157+
first_document = result["documents"][0]
158+
assert first_document.score is not None
159+
assert first_document.meta["retrieval_confidence"] == first_document.score
160+
assert first_document.meta["retrieval_confidence_source"] == "bm25_scaled_score"
161+
162+
def test_run_with_include_confidence_does_not_add_metadata_when_not_scaled(self, in_memory_doc_store, mock_docs):
163+
in_memory_doc_store.write_documents(mock_docs)
164+
165+
retriever = InMemoryBM25Retriever(in_memory_doc_store, top_k=3, include_confidence=True)
166+
result = retriever.run(query="PHP")
167+
168+
first_document = result["documents"][0]
169+
assert "retrieval_confidence" not in first_document.meta
170+
assert "retrieval_confidence_source" not in first_document.meta
171+
172+
def test_run_async_with_include_confidence_matches_sync_behavior(self, in_memory_doc_store, mock_docs):
173+
in_memory_doc_store.write_documents(mock_docs)
174+
175+
retriever = InMemoryBM25Retriever(in_memory_doc_store, top_k=3, scale_score=True, include_confidence=True)
176+
result = asyncio.run(retriever.run_async(query="PHP"))
177+
178+
first_document = result["documents"][0]
179+
assert first_document.score is not None
180+
assert first_document.meta["retrieval_confidence"] == first_document.score
181+
assert first_document.meta["retrieval_confidence_source"] == "bm25_scaled_score"
182+
143183
@pytest.mark.integration
144184
@pytest.mark.parametrize(
145185
"query, query_result",

0 commit comments

Comments
 (0)