Skip to content

Commit 11a1d22

Browse files
fix: ChromaDocumentStore match search_term against metadata field value, not content (#3644)
1 parent 84a5564 commit 11a1d22

3 files changed

Lines changed: 104 additions & 29 deletions

File tree

integrations/chroma/src/haystack_integrations/document_stores/chroma/document_store.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,8 @@ def _compute_field_unique_values(
356356
357357
:param result: The full GetResult from a Chroma collection get operation.
358358
:param field_name: The metadata field name to collect unique values for.
359-
:param search_term: Optional search term to filter documents by content.
359+
:param search_term: Optional search term to filter by, matched as a case-insensitive
360+
substring against the value of `field_name`.
360361
:param from_: The offset to start returning values from.
361362
:param size: The maximum number of unique values to return.
362363
:returns: A tuple of the paginated unique values list and the total count.
@@ -367,13 +368,15 @@ def _compute_field_unique_values(
367368
return [], 0
368369

369370
if search_term:
370-
documents = result.get("documents")
371-
if documents is None:
372-
documents = []
373-
filtered_metadatas = [
374-
metadatas[i] for i in range(len(documents)) if documents[i] and search_term in documents[i]
371+
search_term_lower = search_term.lower()
372+
metadatas = [
373+
meta
374+
for meta in metadatas
375+
if meta
376+
and field_name in meta
377+
and meta.get(field_name) is not None
378+
and search_term_lower in str(meta.get(field_name)).lower()
375379
]
376-
metadatas = filtered_metadatas
377380

378381
if not metadatas:
379382
return [], 0
@@ -1294,12 +1297,12 @@ def get_metadata_field_unique_values(
12941297
size: int = 10,
12951298
) -> tuple[list[str], int]:
12961299
"""
1297-
Return unique metadata field values, optionally filtered by a content search term, with pagination.
1300+
Return unique metadata field values, optionally filtered by a search term, with pagination.
12981301
12991302
:param metadata_field: The metadata field to get unique values for.
13001303
Can include or omit the "meta." prefix.
1301-
:param search_term: Optional search term to filter documents by matching
1302-
in the content field.
1304+
:param search_term: Optional search term to filter values, matched as a
1305+
case-insensitive substring against the metadata field's value.
13031306
:param from_: The offset to start returning values from (for pagination).
13041307
:param size: The maximum number of unique values to return.
13051308
:returns: A tuple containing list of unique values and total count of unique values.
@@ -1309,11 +1312,7 @@ def get_metadata_field_unique_values(
13091312

13101313
field_name = _normalize_metadata_field_name(metadata_field)
13111314

1312-
kwargs: dict[str, Any] = {"include": ["metadatas"]}
1313-
if search_term:
1314-
kwargs["include"] = ["metadatas", "documents"]
1315-
1316-
result = self._collection.get(**kwargs)
1315+
result = self._collection.get(include=["metadatas"])
13171316
return self._compute_field_unique_values(result, field_name, search_term, from_, size)
13181317

13191318
async def get_metadata_field_unique_values_async(
@@ -1324,14 +1323,14 @@ async def get_metadata_field_unique_values_async(
13241323
size: int = 10,
13251324
) -> tuple[list[str], int]:
13261325
"""
1327-
Asynchronously return unique metadata field values, optionally filtered by content, with pagination.
1326+
Asynchronously return unique metadata field values, optionally filtered by a search term, with pagination.
13281327
13291328
Asynchronous methods are only supported for HTTP connections.
13301329
13311330
:param metadata_field: The metadata field to get unique values for.
13321331
Can include or omit the "meta." prefix.
1333-
:param search_term: Optional search term to filter documents by matching
1334-
in the content field.
1332+
:param search_term: Optional search term to filter values, matched as a
1333+
case-insensitive substring against the metadata field's value.
13351334
:param from_: The offset to start returning values from (for pagination).
13361335
:param size: The maximum number of unique values to return.
13371336
:returns: A tuple containing list of unique values and total count of unique values.
@@ -1341,11 +1340,7 @@ async def get_metadata_field_unique_values_async(
13411340

13421341
field_name = _normalize_metadata_field_name(metadata_field)
13431342

1344-
kwargs: dict[str, Any] = {"include": ["metadatas"]}
1345-
if search_term:
1346-
kwargs["include"] = ["metadatas", "documents"]
1347-
1348-
result = await self._async_collection.get(**kwargs)
1343+
result = await self._async_collection.get(include=["metadatas"])
13491344
return self._compute_field_unique_values(result, field_name, search_term, from_, size)
13501345

13511346
@classmethod

integrations/chroma/tests/test_document_store.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,11 @@ def test_compute_field_min_max_skips_non_scalar_values(self):
196196
@pytest.mark.parametrize(
197197
"result",
198198
[
199-
{"ids": ["1"], "documents": None, "metadatas": [{"cat": "A"}]},
200-
{"ids": ["1"], "documents": ["hello world"], "metadatas": [{"cat": "A"}]},
199+
{"ids": ["1"], "metadatas": [{"cat": "A"}]},
200+
{"ids": ["1"], "metadatas": [None]},
201+
{"ids": ["1"], "metadatas": [{"other": "A"}]},
201202
],
202-
ids=["documents_none", "no_matches"],
203+
ids=["no_match", "metadata_none", "field_missing"],
203204
)
204205
def test_compute_field_unique_values_with_search_term_edge_cases(self, result):
205206
values, total = ChromaDocumentStore._compute_field_unique_values(result, "cat", "absent", 0, 10)
@@ -741,12 +742,57 @@ def test_get_metadata_field_unique_values_pagination(self, populated_store):
741742
assert sorted(all_values) == ["A", "B", "C"]
742743

743744
def test_get_metadata_field_unique_values_with_search_term(self, populated_store):
744-
"""Test getting unique values filtered by search term"""
745-
# Search for documents containing "Doc 1"
745+
"""Test getting unique values filtered by search term.
746+
747+
The search term is matched against the metadata field's own value, not document
748+
content. "Doc 1" is content for one document (category "A"), but it is not a
749+
substring of any "category" value, so no values should match.
750+
"""
746751
values, total = populated_store.get_metadata_field_unique_values(
747752
"category", search_term="Doc 1", from_=0, size=10
748753
)
749-
assert values == ["A"] # Only Doc 1 has category A
754+
assert values == []
755+
assert total == 0
756+
757+
def test_get_metadata_field_unique_values_search_term_matches_field_value(self, populated_store):
758+
"""Search term matches when it's a case-insensitive substring of the field's value."""
759+
values, total = populated_store.get_metadata_field_unique_values("status", search_term="ACT", from_=0, size=10)
760+
# "ACT" is a substring of both "active" and "inactive" (case-insensitive)
761+
assert sorted(values) == ["active", "inactive"]
762+
assert total == 2
763+
764+
values, total = populated_store.get_metadata_field_unique_values("status", search_term="ina", from_=0, size=10)
765+
assert values == ["inactive"]
766+
assert total == 1
767+
768+
def test_get_metadata_field_unique_values_search_term_excludes_content_only_match(self, document_store):
769+
"""A search term present only in document content (not in the metadata field value)
770+
must not match, proving search_term no longer filters on content."""
771+
docs = [
772+
Document(content="unique-marker-xyz", meta={"category": "A"}),
773+
Document(content="plain content", meta={"category": "B"}),
774+
]
775+
document_store.write_documents(docs)
776+
777+
values, total = document_store.get_metadata_field_unique_values(
778+
"category", search_term="unique-marker-xyz", from_=0, size=10
779+
)
780+
assert values == []
781+
assert total == 0
782+
783+
def test_get_metadata_field_unique_values_search_term_matches_field_value_not_content(self, document_store):
784+
"""A search term present in the metadata field's value but absent from the content
785+
must match, proving search_term filters on the metadata field's value."""
786+
docs = [
787+
Document(content="Nothing special here", meta={"category": "special-value"}),
788+
Document(content="Nothing special here either", meta={"category": "other"}),
789+
]
790+
document_store.write_documents(docs)
791+
792+
values, total = document_store.get_metadata_field_unique_values(
793+
"category", search_term="SPECIAL", from_=0, size=10
794+
)
795+
assert values == ["special-value"]
750796
assert total == 1
751797

752798
def test_get_metadata_field_unique_values_field_normalization(self, populated_store):

integrations/chroma/tests/test_document_store_async.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,37 @@ async def test_get_metadata_field_unique_values_async_missing_field(self, docume
215215
)
216216
assert values == []
217217
assert total == 0
218+
219+
async def test_get_metadata_field_unique_values_async_search_term_excludes_content_only_match(
220+
self, document_store: ChromaDocumentStore
221+
):
222+
"""A search term present only in document content (not in the metadata field value)
223+
must not match, proving search_term no longer filters on content."""
224+
docs = [
225+
Document(content="unique-marker-xyz", meta={"category": "A"}),
226+
Document(content="plain content", meta={"category": "B"}),
227+
]
228+
await document_store.write_documents_async(docs)
229+
230+
values, total = await document_store.get_metadata_field_unique_values_async(
231+
"category", search_term="unique-marker-xyz", from_=0, size=10
232+
)
233+
assert values == []
234+
assert total == 0
235+
236+
async def test_get_metadata_field_unique_values_async_search_term_matches_field_value(
237+
self, document_store: ChromaDocumentStore
238+
):
239+
"""A search term present in the metadata field's value but absent from the content
240+
must match, proving search_term filters on the metadata field's value (case-insensitively)."""
241+
docs = [
242+
Document(content="Nothing special here", meta={"category": "special-value"}),
243+
Document(content="Nothing special here either", meta={"category": "other"}),
244+
]
245+
await document_store.write_documents_async(docs)
246+
247+
values, total = await document_store.get_metadata_field_unique_values_async(
248+
"category", search_term="SPECIAL", from_=0, size=10
249+
)
250+
assert values == ["special-value"]
251+
assert total == 1

0 commit comments

Comments
 (0)