Skip to content

Commit 84a5564

Browse files
fix: Weaviate match search_term against metadata field value, not content + default size value set to 10 (#3643)
1 parent c883c9f commit 84a5564

3 files changed

Lines changed: 124 additions & 50 deletions

File tree

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

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -632,24 +632,53 @@ async def count_unique_metadata_by_filter_async(
632632

633633
return result
634634

635+
@staticmethod
636+
def _compute_field_unique_values(
637+
agg_result: Any, search_term: str | None, from_: int, size: int
638+
) -> tuple[list[str], int]:
639+
"""
640+
Computes paginated unique values from a Weaviate group-by aggregation result.
641+
642+
Weaviate's `like` filter only supports case-sensitive substring matching, so instead of
643+
filtering server-side (which would be case-sensitive), all groups are fetched unconditionally
644+
and `search_term` is applied here as a case-insensitive substring match in Python.
645+
646+
:param agg_result: The group-by aggregation result, with one group per unique field value.
647+
:param search_term: Optional term to filter the values by, matched as a case-insensitive
648+
substring against each value.
649+
:param from_: The starting offset for pagination (0-indexed).
650+
:param size: The maximum number of unique values to return.
651+
:returns: A tuple of (paginated list of unique values, total count of unique values).
652+
"""
653+
all_values = [str(g.grouped_by.value) for g in agg_result.groups] if agg_result.groups else []
654+
if search_term:
655+
search_term_lower = search_term.lower()
656+
all_values = [value for value in all_values if search_term_lower in value.lower()]
657+
all_values.sort() # Sort for consistent pagination
658+
total_count = len(all_values)
659+
660+
paginated_values = all_values[from_ : from_ + size]
661+
662+
return paginated_values, total_count
663+
635664
def get_metadata_field_unique_values(
636665
self,
637666
metadata_field: str,
638667
search_term: str | None = None,
639668
from_: int = 0,
640-
size: int = 10000,
669+
size: int = 10,
641670
) -> tuple[list[str], int]:
642671
"""
643672
Returns unique values for a metadata field with pagination support.
644673
645674
:param metadata_field: The metadata field name to get unique values for.
646675
Can be prefixed with 'meta.' (e.g., 'meta.category' or 'category').
647-
:param search_term: Optional term to filter documents by content before
648-
extracting unique values. If provided, only documents whose content
649-
contains this term will be considered.
650-
Note: Uses substring matching (case-sensitive, no stemming).
676+
:param search_term: Optional term to filter the metadata field's values by
677+
before returning them. If provided, only values of `metadata_field` that
678+
contain this term will be considered.
679+
Note: Uses case-insensitive substring matching (no stemming).
651680
:param from_: The starting offset for pagination (0-indexed). Defaults to 0.
652-
:param size: The maximum number of unique values to return. Defaults to 10000.
681+
:param size: The maximum number of unique values to return. Defaults to 10.
653682
:returns: A tuple of (list of unique values, total count of unique values).
654683
:raises ValueError: If the field is not found in the collection schema.
655684
"""
@@ -661,45 +690,33 @@ def get_metadata_field_unique_values(
661690
msg = f"Field '{field_name}' not found in collection schema"
662691
raise ValueError(msg)
663692

664-
weaviate_filter = None
665-
if search_term:
666-
weaviate_filter = convert_filters({"field": "content", "operator": "contains", "value": search_term})
667-
668693
# Weaviate's GroupByAggregate has a default limit of 100 groups.
669694
# We use the document count as the limit to ensure all unique values are retrieved,
670695
# since the number of unique values cannot exceed the number of documents.
671696
doc_count = self.collection.aggregate.over_all(total_count=True).total_count or 0
672697

673-
agg_result = self.collection.aggregate.over_all(
674-
filters=weaviate_filter, group_by=GroupByAggregate(prop=field_name, limit=doc_count)
675-
)
698+
agg_result = self.collection.aggregate.over_all(group_by=GroupByAggregate(prop=field_name, limit=doc_count))
676699

677-
all_values = [str(g.grouped_by.value) for g in agg_result.groups] if agg_result.groups else []
678-
all_values.sort() # Sort for consistent pagination
679-
total_count = len(all_values)
680-
681-
paginated_values = all_values[from_ : from_ + size]
682-
683-
return paginated_values, total_count
700+
return self._compute_field_unique_values(agg_result, search_term, from_, size)
684701

685702
async def get_metadata_field_unique_values_async(
686703
self,
687704
metadata_field: str,
688705
search_term: str | None = None,
689706
from_: int = 0,
690-
size: int = 10000,
707+
size: int = 10,
691708
) -> tuple[list[str], int]:
692709
"""
693710
Asynchronously returns unique values for a metadata field with pagination support.
694711
695712
:param metadata_field: The metadata field name to get unique values for.
696713
Can be prefixed with 'meta.' (e.g., 'meta.category' or 'category').
697-
:param search_term: Optional term to filter documents by content before
698-
extracting unique values. If provided, only documents whose content
699-
contains this term will be considered.
700-
Note: Uses substring matching (case-sensitive, no stemming).
714+
:param search_term: Optional term to filter the metadata field's values by
715+
before returning them. If provided, only values of `metadata_field` that
716+
contain this term will be considered.
717+
Note: Uses case-insensitive substring matching (no stemming).
701718
:param from_: The starting offset for pagination (0-indexed). Defaults to 0.
702-
:param size: The maximum number of unique values to return. Defaults to 10000.
719+
:param size: The maximum number of unique values to return. Defaults to 10.
703720
:returns: A tuple of (list of unique values, total count of unique values).
704721
:raises ValueError: If the field is not found in the collection schema.
705722
"""
@@ -712,27 +729,15 @@ async def get_metadata_field_unique_values_async(
712729
msg = f"Field '{field_name}' not found in collection schema"
713730
raise ValueError(msg)
714731

715-
weaviate_filter = None
716-
if search_term:
717-
weaviate_filter = convert_filters({"field": "content", "operator": "contains", "value": search_term})
718-
719732
# Weaviate's GroupByAggregate has a default limit of 100 groups.
720733
# We use the document count as the limit to ensure all unique values are retrieved,
721734
# since the number of unique values cannot exceed the number of documents.
722735
doc_count_result = await collection.aggregate.over_all(total_count=True)
723736
doc_count = doc_count_result.total_count or 0
724737

725-
agg_result = await collection.aggregate.over_all(
726-
filters=weaviate_filter, group_by=GroupByAggregate(prop=field_name, limit=doc_count)
727-
)
738+
agg_result = await collection.aggregate.over_all(group_by=GroupByAggregate(prop=field_name, limit=doc_count))
728739

729-
all_values = [str(g.grouped_by.value) for g in agg_result.groups] if agg_result.groups else []
730-
all_values.sort() # Sort for consistent pagination
731-
total_count = len(all_values)
732-
733-
paginated_values = all_values[from_ : from_ + size]
734-
735-
return paginated_values, total_count
740+
return self._compute_field_unique_values(agg_result, search_term, from_, size)
736741

737742
@staticmethod
738743
def _to_data_object(document: Document) -> dict[str, Any]:

integrations/weaviate/tests/test_document_store.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,17 +1156,54 @@ def test_get_metadata_field_unique_values_with_meta_prefix(self, document_store)
11561156
assert set(values) == {"TypeA", "TypeB"}
11571157

11581158
def test_get_metadata_field_unique_values_with_search_term(self, document_store):
1159+
# search_term must match against the VALUE of the target metadata field,
1160+
# not the document content.
11591161
docs = [
1160-
Document(content="Python programming language", meta={"category": "TypeA"}),
1161-
Document(content="Java programming language", meta={"category": "TypeB"}),
1162-
Document(content="Python is great", meta={"category": "TypeC"}),
1163-
Document(content="JavaScript tutorial", meta={"category": "TypeD"}),
1162+
Document(content="Some article", meta={"category": "Python Programming"}),
1163+
Document(content="Some article", meta={"category": "Java Programming"}),
1164+
Document(content="Some article", meta={"category": "Python Basics"}),
1165+
Document(content="Some article", meta={"category": "JavaScript Tutorial"}),
11641166
]
11651167
document_store.write_documents(docs)
11661168

11671169
values, total_count = document_store.get_metadata_field_unique_values("category", search_term="Python")
11681170
assert total_count == 2
1169-
assert set(values) == {"TypeA", "TypeC"}
1171+
assert set(values) == {"Python Programming", "Python Basics"}
1172+
1173+
def test_get_metadata_field_unique_values_search_term_excludes_content_only_match(self, document_store):
1174+
# A document whose content contains the search term but whose target metadata
1175+
# field value does NOT must be excluded from the results.
1176+
docs = [
1177+
Document(content="Python programming language", meta={"category": "TypeA"}),
1178+
]
1179+
document_store.write_documents(docs)
1180+
1181+
values, total_count = document_store.get_metadata_field_unique_values("category", search_term="Python")
1182+
assert total_count == 0
1183+
assert values == []
1184+
1185+
def test_get_metadata_field_unique_values_search_term_matches_metadata_value_only(self, document_store):
1186+
# A document whose metadata field value contains the search term but whose
1187+
# content does NOT must be included in the results.
1188+
docs = [
1189+
Document(content="Unrelated text about cooking", meta={"category": "Python Basics"}),
1190+
]
1191+
document_store.write_documents(docs)
1192+
1193+
values, total_count = document_store.get_metadata_field_unique_values("category", search_term="Python")
1194+
assert total_count == 1
1195+
assert values == ["Python Basics"]
1196+
1197+
def test_get_metadata_field_unique_values_search_term_case_insensitive(self, document_store):
1198+
docs = [
1199+
Document(content="n/a", meta={"category": "Python Basics"}),
1200+
Document(content="n/a", meta={"category": "Java Basics"}),
1201+
]
1202+
document_store.write_documents(docs)
1203+
1204+
values, total_count = document_store.get_metadata_field_unique_values("category", search_term="PYTHON")
1205+
assert total_count == 1
1206+
assert values == ["Python Basics"]
11701207

11711208
def test_get_metadata_field_unique_values_with_pagination(self, document_store):
11721209
docs = [

integrations/weaviate/tests/test_document_store_async.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,19 +454,51 @@ async def test_get_metadata_field_unique_values_async_with_meta_prefix(self, doc
454454

455455
@pytest.mark.asyncio
456456
async def test_get_metadata_field_unique_values_async_with_search_term(self, document_store):
457+
# search_term must match against the VALUE of the target metadata field,
458+
# not the document content.
457459
docs = [
458-
Document(content="Python programming language", meta={"category": "TypeA"}),
459-
Document(content="Java programming language", meta={"category": "TypeB"}),
460-
Document(content="Python is great", meta={"category": "TypeC"}),
461-
Document(content="JavaScript tutorial", meta={"category": "TypeD"}),
460+
Document(content="Some article", meta={"category": "Python Programming"}),
461+
Document(content="Some article", meta={"category": "Java Programming"}),
462+
Document(content="Some article", meta={"category": "Python Basics"}),
463+
Document(content="Some article", meta={"category": "JavaScript Tutorial"}),
462464
]
463465
await document_store.write_documents_async(docs)
464466

465467
values, total_count = await document_store.get_metadata_field_unique_values_async(
466468
"category", search_term="Python"
467469
)
468470
assert total_count == 2
469-
assert set(values) == {"TypeA", "TypeC"}
471+
assert set(values) == {"Python Programming", "Python Basics"}
472+
473+
@pytest.mark.asyncio
474+
async def test_get_metadata_field_unique_values_async_search_term_excludes_content_only_match(self, document_store):
475+
# A document whose content contains the search term but whose target metadata
476+
# field value does NOT must be excluded from the results.
477+
docs = [
478+
Document(content="Python programming language", meta={"category": "TypeA"}),
479+
]
480+
await document_store.write_documents_async(docs)
481+
482+
values, total_count = await document_store.get_metadata_field_unique_values_async(
483+
"category", search_term="Python"
484+
)
485+
assert total_count == 0
486+
assert values == []
487+
488+
@pytest.mark.asyncio
489+
async def test_get_metadata_field_unique_values_async_search_term_matches_metadata_value_only(self, document_store):
490+
# A document whose metadata field value contains the search term but whose
491+
# content does NOT must be included in the results.
492+
docs = [
493+
Document(content="Unrelated text about cooking", meta={"category": "Python Basics"}),
494+
]
495+
await document_store.write_documents_async(docs)
496+
497+
values, total_count = await document_store.get_metadata_field_unique_values_async(
498+
"category", search_term="Python"
499+
)
500+
assert total_count == 1
501+
assert values == ["Python Basics"]
470502

471503
@pytest.mark.asyncio
472504
async def test_get_metadata_field_unique_values_async_with_pagination(self, document_store):

0 commit comments

Comments
 (0)