weaviate-haystack 7.0.0__tar.gz → 7.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/CHANGELOG.md +19 -0
  2. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/PKG-INFO +2 -2
  3. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/docker-compose.yml +0 -1
  4. weaviate_haystack-7.2.0/pydoc/config_docusaurus.yml +17 -0
  5. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/pyproject.toml +2 -2
  6. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/components/retrievers/weaviate/bm25_retriever.py +6 -2
  7. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/document_stores/weaviate/_filters.py +21 -0
  8. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/document_stores/weaviate/document_store.py +398 -6
  9. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_document_store.py +225 -53
  10. weaviate_haystack-7.2.0/tests/test_document_store_async.py +493 -0
  11. weaviate_haystack-7.0.0/pydoc/config_docusaurus.yml +0 -32
  12. weaviate_haystack-7.0.0/tests/test_document_store_async.py +0 -245
  13. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/.gitignore +0 -0
  14. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/LICENSE.txt +0 -0
  15. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/README.md +0 -0
  16. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  17. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/components/retrievers/weaviate/__init__.py +0 -0
  18. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/components/retrievers/weaviate/embedding_retriever.py +0 -0
  19. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/components/retrievers/weaviate/hybrid_retriever.py +0 -0
  20. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/document_stores/py.typed +0 -0
  21. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/document_stores/weaviate/__init__.py +0 -0
  22. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/src/haystack_integrations/document_stores/weaviate/auth.py +0 -0
  23. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/__init__.py +0 -0
  24. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/conftest.py +0 -0
  25. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_auth.py +0 -0
  26. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_bm25_retriever.py +0 -0
  27. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_bm25_retriever_async.py +0 -0
  28. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_embedding_retriever.py +0 -0
  29. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_embedding_retriever_async.py +0 -0
  30. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_files/robot1.jpg +0 -0
  31. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_filters.py +0 -0
  32. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_hybrid_retriever.py +0 -0
  33. {weaviate_haystack-7.0.0 → weaviate_haystack-7.2.0}/tests/test_hybrid_retriever_async.py +0 -0
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/weaviate-v7.1.0] - 2026-01-29
4
+
5
+ ### 🚀 Features
6
+
7
+ - Adding count with filtering operations to `WeaviateDocumentStore` (#2767)
8
+
9
+
10
+ ## [integrations/weaviate-v7.0.0] - 2026-01-13
11
+
12
+ ### 🐛 Bug Fixes
13
+
14
+ - `WeaviateDocumentStore `_to_document()` and `to_data_object()` should be static methods (#2669)
15
+
16
+ ### 🧹 Chores
17
+
18
+ - Make fmt command more forgiving (#2671)
19
+ - [**breaking**] Weaviate - drop Python 3.9 and use X|Y typing (#2733)
20
+
21
+
3
22
  ## [integrations/weaviate-v6.5.0] - 2026-01-08
4
23
 
5
24
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weaviate-haystack
3
- Version: 7.0.0
3
+ Version: 7.2.0
4
4
  Summary: An integration of Weaviate vector database with Haystack
5
5
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations
6
6
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/weaviate/README.md
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
19
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
20
  Requires-Python: >=3.10
21
- Requires-Dist: haystack-ai>=2.22.0
21
+ Requires-Dist: haystack-ai>=2.24.0
22
22
  Requires-Dist: python-dateutil
23
23
  Requires-Dist: weaviate-client>=4.9
24
24
  Description-Content-Type: text/markdown
@@ -1,4 +1,3 @@
1
- version: '3.4'
2
1
  services:
3
2
  weaviate:
4
3
  command:
@@ -0,0 +1,17 @@
1
+ loaders:
2
+ - modules:
3
+ - haystack_integrations.document_stores.weaviate.auth
4
+ - haystack_integrations.document_stores.weaviate.document_store
5
+ - haystack_integrations.components.retrievers.weaviate.bm25_retriever
6
+ - haystack_integrations.components.retrievers.weaviate.embedding_retriever
7
+ - haystack_integrations.components.retrievers.weaviate.hybrid_retriever
8
+ search_path: [../src]
9
+ processors:
10
+ - type: filter
11
+ documented_only: true
12
+ skip_empty_modules: true
13
+ renderer:
14
+ description: Weaviate integration for Haystack
15
+ id: integrations-weaviate
16
+ filename: weaviate.md
17
+ title: Weaviate
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: Implementation :: PyPy",
24
24
  ]
25
25
  dependencies = [
26
- "haystack-ai>=2.22.0",
26
+ "haystack-ai>=2.24.0",
27
27
  "weaviate-client>=4.9",
28
28
  "python-dateutil",
29
29
  ]
@@ -49,7 +49,7 @@ installer = "uv"
49
49
  dependencies = ["haystack-pydoc-tools", "ruff"]
50
50
 
51
51
  [tool.hatch.envs.default.scripts]
52
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
52
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
53
53
  fmt = "ruff check --fix {args}; ruff format {args}"
54
54
  fmt-check = "ruff check {args} && ruff format --check {args}"
55
55
 
@@ -18,8 +18,12 @@ class WeaviateBM25Retriever:
18
18
 
19
19
  Example usage:
20
20
  ```python
21
- from haystack_integrations.document_stores.weaviate.document_store import WeaviateDocumentStore
22
- from haystack_integrations.components.retrievers.weaviate.bm25_retriever import WeaviateBM25Retriever
21
+ from haystack_integrations.document_stores.weaviate.document_store import (
22
+ WeaviateDocumentStore,
23
+ )
24
+ from haystack_integrations.components.retrievers.weaviate.bm25_retriever import (
25
+ WeaviateBM25Retriever,
26
+ )
23
27
 
24
28
  document_store = WeaviateDocumentStore(url="http://localhost:8080")
25
29
  retriever = WeaviateBM25Retriever(document_store=document_store)
@@ -26,6 +26,13 @@ def validate_filters(filters: dict[str, Any] | None) -> None:
26
26
  def convert_filters(filters: dict[str, Any]) -> FilterReturn:
27
27
  """
28
28
  Convert filters from Haystack format to Weaviate format.
29
+
30
+ Supported comparison operators: ``==``, ``!=``, ``>``, ``>=``, ``<``, ``<=``,
31
+ ``in``, ``not in``, ``contains``.
32
+
33
+ Note: The ``contains`` operator performs substring matching and is
34
+ **case-sensitive**. For case-insensitive matching, normalize the value
35
+ (e.g., lowercase) before building the filter.
29
36
  """
30
37
  if not isinstance(filters, dict):
31
38
  msg = "Filters must be a dictionary"
@@ -228,6 +235,19 @@ def _not_in(field: str, value: Any) -> FilterReturn:
228
235
  return Filter.all_of(operands)
229
236
 
230
237
 
238
+ def _contains(field: str, value: Any) -> FilterReturn:
239
+ """
240
+ Creates a filter for substring matching using Weaviate's 'like' operator.
241
+
242
+ The matching is case-sensitive. For case-insensitive matching, consider
243
+ normalizing the value before passing it to this function.
244
+ """
245
+ if not isinstance(value, str):
246
+ msg = "Filter value must be a string when using 'contains' comparator"
247
+ raise FilterError(msg)
248
+ return weaviate.classes.query.Filter.by_property(field).like(f"*{value}*")
249
+
250
+
231
251
  COMPARISON_OPERATORS = {
232
252
  "==": _equal,
233
253
  "!=": _not_equal,
@@ -237,6 +257,7 @@ COMPARISON_OPERATORS = {
237
257
  "<=": _less_than_equal,
238
258
  "in": _in,
239
259
  "not in": _not_in,
260
+ "contains": _contains,
240
261
  }
241
262
 
242
263
 
@@ -15,6 +15,13 @@ from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumen
15
15
  from haystack.document_stores.types.policy import DuplicatePolicy
16
16
 
17
17
  import weaviate
18
+ from weaviate.collections.classes.aggregate import (
19
+ GroupByAggregate,
20
+ Metrics,
21
+ _MetricsDate,
22
+ _MetricsInteger,
23
+ _MetricsNumber,
24
+ )
18
25
  from weaviate.collections.classes.data import DataObject
19
26
  from weaviate.config import AdditionalConfig
20
27
  from weaviate.embedded import EmbeddedOptions
@@ -65,7 +72,9 @@ class WeaviateDocumentStore:
65
72
  ```python
66
73
  import os
67
74
  from haystack_integrations.document_stores.weaviate.auth import AuthApiKey
68
- from haystack_integrations.document_stores.weaviate.document_store import WeaviateDocumentStore
75
+ from haystack_integrations.document_stores.weaviate.document_store import (
76
+ WeaviateDocumentStore,
77
+ )
69
78
 
70
79
  os.environ["WEAVIATE_API_KEY"] = "MY_API_KEY"
71
80
 
@@ -77,7 +86,9 @@ class WeaviateDocumentStore:
77
86
 
78
87
  Usage example with self-hosted Weaviate:
79
88
  ```python
80
- from haystack_integrations.document_stores.weaviate.document_store import WeaviateDocumentStore
89
+ from haystack_integrations.document_stores.weaviate.document_store import (
90
+ WeaviateDocumentStore,
91
+ )
81
92
 
82
93
  document_store = WeaviateDocumentStore(url="http://localhost:8080")
83
94
  ```
@@ -206,8 +217,6 @@ class WeaviateDocumentStore:
206
217
 
207
218
  self._client.connect()
208
219
 
209
- # Test connection, it will raise an exception if it fails.
210
- self._client.collections.list_all(simple=True)
211
220
  if not self._client.collections.exists(self._collection_settings["class"]):
212
221
  self._client.collections.create_from_dict(self._collection_settings)
213
222
 
@@ -251,8 +260,7 @@ class WeaviateDocumentStore:
251
260
  )
252
261
 
253
262
  await self._async_client.connect()
254
- # Test connection, it will raise an exception if it fails.
255
- await self._async_client.collections.list_all(simple=True)
263
+
256
264
  if not await self._async_client.collections.exists(self._collection_settings["class"]):
257
265
  await self._async_client.collections.create_from_dict(self._collection_settings)
258
266
 
@@ -276,6 +284,24 @@ class WeaviateDocumentStore:
276
284
  self._async_collection = async_client.collections.get(self._collection_settings["class"])
277
285
  return self._async_collection
278
286
 
287
+ def close(self) -> None:
288
+ """
289
+ Close the synchronous Weaviate client connection.
290
+ """
291
+ if self._client:
292
+ self._client.close()
293
+ self._client = None
294
+ self._collection = None
295
+
296
+ async def close_async(self) -> None:
297
+ """
298
+ Close the asynchronous Weaviate client connection.
299
+ """
300
+ if self._async_client:
301
+ await self._async_client.close()
302
+ self._async_client = None
303
+ self._async_collection = None
304
+
279
305
  def to_dict(self) -> dict[str, Any]:
280
306
  """
281
307
  Serializes the component to a dictionary.
@@ -326,6 +352,368 @@ class WeaviateDocumentStore:
326
352
  total = self.collection.aggregate.over_all(total_count=True).total_count
327
353
  return total if total else 0
328
354
 
355
+ def count_documents_by_filter(self, filters: dict[str, Any]) -> int:
356
+ """
357
+ Returns the number of documents that match the provided filters.
358
+
359
+ :param filters: The filters to apply to count documents.
360
+ For filter syntax, see
361
+ [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering).
362
+ :returns: The number of documents that match the filters.
363
+ """
364
+ validate_filters(filters)
365
+ weaviate_filter = convert_filters(filters)
366
+ total = self.collection.aggregate.over_all(filters=weaviate_filter, total_count=True).total_count
367
+ return total if total else 0
368
+
369
+ async def count_documents_by_filter_async(self, filters: dict[str, Any]) -> int:
370
+ """
371
+ Asynchronously returns the number of documents that match the provided filters.
372
+
373
+ :param filters: The filters to apply to count documents.
374
+ For filter syntax, see
375
+ [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering).
376
+ :returns: The number of documents that match the filters.
377
+ """
378
+ validate_filters(filters)
379
+ collection = await self.async_collection
380
+ weaviate_filter = convert_filters(filters)
381
+ result = await collection.aggregate.over_all(filters=weaviate_filter, total_count=True)
382
+ return result.total_count if result.total_count else 0
383
+
384
+ def get_metadata_fields_info(self) -> dict[str, dict[str, str]]:
385
+ """
386
+ Returns metadata field names and their types, excluding special fields.
387
+
388
+ Special fields (content, blob_data, blob_mime_type, _original_id, score) are excluded
389
+ as they are not user metadata fields.
390
+
391
+ :returns: A dictionary where keys are field names and values are dictionaries
392
+ containing type information, e.g.:
393
+ ```python
394
+ {
395
+ 'number': {'type': 'int'},
396
+ 'date': {'type': 'date'},
397
+ 'category': {'type': 'text'},
398
+ 'status': {'type': 'text'}
399
+ }
400
+ ```
401
+ """
402
+ config = self.collection.config.get()
403
+ special_fields = {prop["name"] for prop in DOCUMENT_COLLECTION_PROPERTIES}
404
+ fields_info = {}
405
+ for prop in config.properties:
406
+ if prop.name not in special_fields:
407
+ data_type = str(prop.data_type.value) if hasattr(prop.data_type, "value") else str(prop.data_type)
408
+ fields_info[prop.name] = {"type": data_type}
409
+ return fields_info
410
+
411
+ async def get_metadata_fields_info_async(self) -> dict[str, dict[str, str]]:
412
+ """
413
+ Asynchronously returns metadata field names and their types, excluding special fields.
414
+
415
+ Special fields (content, blob_data, blob_mime_type, _original_id, score) are excluded
416
+ as they are not user metadata fields.
417
+
418
+ :returns: A dictionary where keys are field names and values are dictionaries
419
+ containing type information, e.g.:
420
+ ```python
421
+ {
422
+ 'number': {'type': 'int'},
423
+ 'date': {'type': 'date'},
424
+ 'category': {'type': 'text'},
425
+ 'status': {'type': 'text'}
426
+ }
427
+ ```
428
+ """
429
+ collection = await self.async_collection
430
+ config = await collection.config.get()
431
+ special_fields = {prop["name"] for prop in DOCUMENT_COLLECTION_PROPERTIES}
432
+ fields_info = {}
433
+ for prop in config.properties:
434
+ if prop.name not in special_fields:
435
+ data_type = str(prop.data_type.value) if hasattr(prop.data_type, "value") else str(prop.data_type)
436
+ fields_info[prop.name] = {"type": data_type}
437
+ return fields_info
438
+
439
+ @staticmethod
440
+ def _normalize_metadata_field_name(metadata_field: str) -> str:
441
+ """
442
+ Removes 'meta.' prefix from field name if present.
443
+
444
+ :param metadata_field: The field name, possibly prefixed with 'meta.'.
445
+ :returns: The field name without the 'meta.' prefix.
446
+ """
447
+ return metadata_field[5:] if metadata_field.startswith("meta.") else metadata_field
448
+
449
+ def get_metadata_field_min_max(self, metadata_field: str) -> dict[str, Any]:
450
+ """
451
+ Returns the minimum and maximum values for a numeric or date metadata field.
452
+
453
+ :param metadata_field: The metadata field name to get min/max for.
454
+ Can be prefixed with 'meta.' (e.g., 'meta.year' or 'year').
455
+ :returns: A dictionary with 'min' and 'max' keys containing the respective values.
456
+ :raises ValueError: If the field is not found or doesn't support min/max operations.
457
+ """
458
+ field_name = self._normalize_metadata_field_name(metadata_field)
459
+
460
+ # Get field type from schema
461
+ config = self.collection.config.get()
462
+ field_type = None
463
+ for prop in config.properties:
464
+ if prop.name == field_name:
465
+ field_type = prop.data_type
466
+ break
467
+
468
+ if field_type is None:
469
+ msg = f"Field '{field_name}' not found in collection schema"
470
+ raise ValueError(msg)
471
+
472
+ data_type_str = str(field_type.value) if hasattr(field_type, "value") else str(field_type)
473
+
474
+ # Build metrics based on type.
475
+ # Explicit type annotation needed for mypy compatibility with Python 3.10,
476
+ # which infers the type from the first assignment and fails on reassignment.
477
+ metrics: _MetricsInteger | _MetricsNumber | _MetricsDate
478
+ if data_type_str.lower() == "int":
479
+ metrics = Metrics(field_name).integer(minimum=True, maximum=True)
480
+ elif data_type_str.lower() == "number":
481
+ metrics = Metrics(field_name).number(minimum=True, maximum=True)
482
+ elif data_type_str.lower() == "date":
483
+ metrics = Metrics(field_name).date_(minimum=True, maximum=True)
484
+ else:
485
+ msg = f"Field type '{data_type_str}' doesn't support min/max aggregation"
486
+ raise ValueError(msg)
487
+
488
+ result = self.collection.aggregate.over_all(return_metrics=metrics)
489
+ field_metrics = result.properties.get(field_name)
490
+
491
+ return {
492
+ "min": getattr(field_metrics, "minimum", None) if field_metrics else None,
493
+ "max": getattr(field_metrics, "maximum", None) if field_metrics else None,
494
+ }
495
+
496
+ async def get_metadata_field_min_max_async(self, metadata_field: str) -> dict[str, Any]:
497
+ """
498
+ Asynchronously returns the minimum and maximum values for a numeric or date metadata field.
499
+
500
+ :param metadata_field: The metadata field name to get min/max for.
501
+ Can be prefixed with 'meta.' (e.g., 'meta.year' or 'year').
502
+ :returns: A dictionary with 'min' and 'max' keys containing the respective values.
503
+ :raises ValueError: If the field is not found or doesn't support min/max operations.
504
+ """
505
+ field_name = self._normalize_metadata_field_name(metadata_field)
506
+
507
+ # Get field type from schema
508
+ collection = await self.async_collection
509
+ config = await collection.config.get()
510
+ field_type = None
511
+ for prop in config.properties:
512
+ if prop.name == field_name:
513
+ field_type = prop.data_type
514
+ break
515
+
516
+ if field_type is None:
517
+ msg = f"Field '{field_name}' not found in collection schema"
518
+ raise ValueError(msg)
519
+
520
+ data_type_str = str(field_type.value) if hasattr(field_type, "value") else str(field_type)
521
+
522
+ # Build metrics based on type.
523
+ # Explicit type annotation needed for mypy compatibility with Python 3.10,
524
+ # which infers the type from the first assignment and fails on reassignment.
525
+ metrics: _MetricsInteger | _MetricsNumber | _MetricsDate
526
+ if data_type_str.lower() == "int":
527
+ metrics = Metrics(field_name).integer(minimum=True, maximum=True)
528
+ elif data_type_str.lower() == "number":
529
+ metrics = Metrics(field_name).number(minimum=True, maximum=True)
530
+ elif data_type_str.lower() == "date":
531
+ metrics = Metrics(field_name).date_(minimum=True, maximum=True)
532
+ else:
533
+ msg = f"Field type '{data_type_str}' doesn't support min/max aggregation"
534
+ raise ValueError(msg)
535
+
536
+ result = await collection.aggregate.over_all(return_metrics=metrics)
537
+ field_metrics = result.properties.get(field_name)
538
+
539
+ return {
540
+ "min": getattr(field_metrics, "minimum", None) if field_metrics else None,
541
+ "max": getattr(field_metrics, "maximum", None) if field_metrics else None,
542
+ }
543
+
544
+ def count_unique_metadata_by_filter(self, filters: dict[str, Any], metadata_fields: list[str]) -> dict[str, int]:
545
+ """
546
+ Returns the count of unique values for each specified metadata field.
547
+
548
+ :param filters: The filters to apply when counting unique values.
549
+ For filter syntax, see
550
+ [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering).
551
+ :param metadata_fields: List of metadata field names to count unique values for.
552
+ Field names can be prefixed with 'meta.' (e.g., 'meta.category' or 'category').
553
+ :returns: A dictionary mapping field names to counts of unique values.
554
+ :raises ValueError: If any of the requested fields don't exist in the collection schema.
555
+ """
556
+ validate_filters(filters)
557
+ weaviate_filter = convert_filters(filters)
558
+
559
+ normalized_fields = [self._normalize_metadata_field_name(f) for f in metadata_fields]
560
+
561
+ # Validate that all requested fields exist in the schema
562
+ config = self.collection.config.get()
563
+ schema_fields = {prop.name for prop in config.properties}
564
+ missing_fields = [f for f in normalized_fields if f not in schema_fields]
565
+ if missing_fields:
566
+ msg = f"Fields not found in collection schema: {missing_fields}"
567
+ raise ValueError(msg)
568
+
569
+ result = {}
570
+ for field in normalized_fields:
571
+ agg_result = self.collection.aggregate.over_all(
572
+ filters=weaviate_filter, group_by=GroupByAggregate(prop=field)
573
+ )
574
+ result[field] = len(agg_result.groups) if agg_result.groups else 0
575
+
576
+ return result
577
+
578
+ async def count_unique_metadata_by_filter_async(
579
+ self, filters: dict[str, Any], metadata_fields: list[str]
580
+ ) -> dict[str, int]:
581
+ """
582
+ Asynchronously returns the count of unique values for each specified metadata field.
583
+
584
+ :param filters: The filters to apply when counting unique values.
585
+ For filter syntax, see
586
+ [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering).
587
+ :param metadata_fields: List of metadata field names to count unique values for.
588
+ Field names can be prefixed with 'meta.' (e.g., 'meta.category' or 'category').
589
+ :returns: A dictionary mapping field names to counts of unique values.
590
+ :raises ValueError: If any of the requested fields don't exist in the collection schema.
591
+ """
592
+ validate_filters(filters)
593
+ collection = await self.async_collection
594
+ weaviate_filter = convert_filters(filters)
595
+
596
+ normalized_fields = [self._normalize_metadata_field_name(f) for f in metadata_fields]
597
+
598
+ # Validate that all requested fields exist in the schema
599
+ config = await collection.config.get()
600
+ schema_fields = {prop.name for prop in config.properties}
601
+ missing_fields = [f for f in normalized_fields if f not in schema_fields]
602
+ if missing_fields:
603
+ msg = f"Fields not found in collection schema: {missing_fields}"
604
+ raise ValueError(msg)
605
+
606
+ result = {}
607
+ for field in normalized_fields:
608
+ agg_result = await collection.aggregate.over_all(
609
+ filters=weaviate_filter, group_by=GroupByAggregate(prop=field)
610
+ )
611
+ result[field] = len(agg_result.groups) if agg_result.groups else 0
612
+
613
+ return result
614
+
615
+ def get_metadata_field_unique_values(
616
+ self,
617
+ metadata_field: str,
618
+ search_term: str | None = None,
619
+ from_: int = 0,
620
+ size: int = 10000,
621
+ ) -> tuple[list[str], int]:
622
+ """
623
+ Returns unique values for a metadata field with pagination support.
624
+
625
+ :param metadata_field: The metadata field name to get unique values for.
626
+ Can be prefixed with 'meta.' (e.g., 'meta.category' or 'category').
627
+ :param search_term: Optional term to filter documents by content before
628
+ extracting unique values. If provided, only documents whose content
629
+ contains this term will be considered.
630
+ Note: Uses substring matching (case-sensitive, no stemming).
631
+ :param from_: The starting offset for pagination (0-indexed). Defaults to 0.
632
+ :param size: The maximum number of unique values to return. Defaults to 10000.
633
+ :returns: A tuple of (list of unique values, total count of unique values).
634
+ :raises ValueError: If the field is not found in the collection schema.
635
+ """
636
+ field_name = self._normalize_metadata_field_name(metadata_field)
637
+
638
+ config = self.collection.config.get()
639
+ schema_fields = {prop.name for prop in config.properties}
640
+ if field_name not in schema_fields:
641
+ msg = f"Field '{field_name}' not found in collection schema"
642
+ raise ValueError(msg)
643
+
644
+ weaviate_filter = None
645
+ if search_term:
646
+ weaviate_filter = convert_filters({"field": "content", "operator": "contains", "value": search_term})
647
+
648
+ # Weaviate's GroupByAggregate has a default limit of 100 groups.
649
+ # We use the document count as the limit to ensure all unique values are retrieved,
650
+ # since the number of unique values cannot exceed the number of documents.
651
+ doc_count = self.collection.aggregate.over_all(total_count=True).total_count or 0
652
+
653
+ agg_result = self.collection.aggregate.over_all(
654
+ filters=weaviate_filter, group_by=GroupByAggregate(prop=field_name, limit=doc_count)
655
+ )
656
+
657
+ all_values = [str(g.grouped_by.value) for g in agg_result.groups] if agg_result.groups else []
658
+ all_values.sort() # Sort for consistent pagination
659
+ total_count = len(all_values)
660
+
661
+ paginated_values = all_values[from_ : from_ + size]
662
+
663
+ return paginated_values, total_count
664
+
665
+ async def get_metadata_field_unique_values_async(
666
+ self,
667
+ metadata_field: str,
668
+ search_term: str | None = None,
669
+ from_: int = 0,
670
+ size: int = 10000,
671
+ ) -> tuple[list[str], int]:
672
+ """
673
+ Asynchronously returns unique values for a metadata field with pagination support.
674
+
675
+ :param metadata_field: The metadata field name to get unique values for.
676
+ Can be prefixed with 'meta.' (e.g., 'meta.category' or 'category').
677
+ :param search_term: Optional term to filter documents by content before
678
+ extracting unique values. If provided, only documents whose content
679
+ contains this term will be considered.
680
+ Note: Uses substring matching (case-sensitive, no stemming).
681
+ :param from_: The starting offset for pagination (0-indexed). Defaults to 0.
682
+ :param size: The maximum number of unique values to return. Defaults to 10000.
683
+ :returns: A tuple of (list of unique values, total count of unique values).
684
+ :raises ValueError: If the field is not found in the collection schema.
685
+ """
686
+ field_name = self._normalize_metadata_field_name(metadata_field)
687
+
688
+ collection = await self.async_collection
689
+ config = await collection.config.get()
690
+ schema_fields = {prop.name for prop in config.properties}
691
+ if field_name not in schema_fields:
692
+ msg = f"Field '{field_name}' not found in collection schema"
693
+ raise ValueError(msg)
694
+
695
+ weaviate_filter = None
696
+ if search_term:
697
+ weaviate_filter = convert_filters({"field": "content", "operator": "contains", "value": search_term})
698
+
699
+ # Weaviate's GroupByAggregate has a default limit of 100 groups.
700
+ # We use the document count as the limit to ensure all unique values are retrieved,
701
+ # since the number of unique values cannot exceed the number of documents.
702
+ doc_count_result = await collection.aggregate.over_all(total_count=True)
703
+ doc_count = doc_count_result.total_count or 0
704
+
705
+ agg_result = await collection.aggregate.over_all(
706
+ filters=weaviate_filter, group_by=GroupByAggregate(prop=field_name, limit=doc_count)
707
+ )
708
+
709
+ all_values = [str(g.grouped_by.value) for g in agg_result.groups] if agg_result.groups else []
710
+ all_values.sort() # Sort for consistent pagination
711
+ total_count = len(all_values)
712
+
713
+ paginated_values = all_values[from_ : from_ + size]
714
+
715
+ return paginated_values, total_count
716
+
329
717
  @staticmethod
330
718
  def _to_data_object(document: Document) -> dict[str, Any]:
331
719
  """
@@ -452,6 +840,10 @@ class WeaviateDocumentStore:
452
840
  For a detailed specification of the filters, refer to the
453
841
  DocumentStore.filter_documents() protocol documentation.
454
842
 
843
+ Note: The ``contains`` filter operator is case-sensitive (substring
844
+ matching). For case-insensitive matching, normalize the value before
845
+ building the filter.
846
+
455
847
  :param filters: The filters to apply to the document list.
456
848
  :returns: A list of Documents that match the given filters.
457
849
  """