weaviate-haystack 7.7.0__tar.gz → 7.9.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 (31) hide show
  1. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/CHANGELOG.md +62 -14
  2. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/LICENSE.txt +1 -1
  3. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/PKG-INFO +3 -3
  4. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/docker-compose.yml +14 -2
  5. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/pyproject.toml +2 -1
  6. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/document_stores/weaviate/document_store.py +132 -46
  7. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_document_store.py +128 -46
  8. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_document_store_async.py +49 -45
  9. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/.gitignore +0 -0
  10. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/README.md +0 -0
  11. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/pydoc/config_docusaurus.yml +0 -0
  12. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  13. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/components/retrievers/weaviate/__init__.py +0 -0
  14. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/components/retrievers/weaviate/bm25_retriever.py +0 -0
  15. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/components/retrievers/weaviate/embedding_retriever.py +0 -0
  16. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/components/retrievers/weaviate/hybrid_retriever.py +0 -0
  17. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/document_stores/py.typed +0 -0
  18. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/document_stores/weaviate/__init__.py +0 -0
  19. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/document_stores/weaviate/_filters.py +0 -0
  20. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/src/haystack_integrations/document_stores/weaviate/auth.py +0 -0
  21. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/__init__.py +0 -0
  22. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/conftest.py +0 -0
  23. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_auth.py +0 -0
  24. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_bm25_retriever.py +0 -0
  25. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_bm25_retriever_async.py +0 -0
  26. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_embedding_retriever.py +0 -0
  27. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_embedding_retriever_async.py +0 -0
  28. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_files/robot1.jpg +0 -0
  29. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_filters.py +0 -0
  30. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_hybrid_retriever.py +0 -0
  31. {weaviate_haystack-7.7.0 → weaviate_haystack-7.9.0}/tests/test_hybrid_retriever_async.py +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/weaviate-v7.8.0] - 2026-09-01
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - `Weaviate` match `search_term` against metadata field value, not content + default size value set to 10 (#3643)
8
+ - Fix new issues raised by ruff 0.16.0 (#3670)
9
+ - `WeaviateDocumentStore` get_metadata_field_unique_values setting list of values to return to `Any` (#3708)
10
+ - Weaviate adding optional parameter `filters` to `get_metadata_field_unique_values()` (#3741)
11
+ - Standardize license files (#3771)
12
+
13
+ ### 🧪 Testing
14
+
15
+ - `WeaviateDocumentStore` removing duplicated tests already covered by haystack Mixin tests (#3787)
16
+
17
+
18
+ ## [integrations/weaviate-v7.7.0] - 2026-07-22
19
+
20
+ ### 🚜 Refactor
21
+
22
+ - Align Weaviate closing methods to standard (#3630)
23
+
24
+
3
25
  ## [integrations/weaviate-v7.6.0] - 2026-06-22
4
26
 
5
27
  ### 🐛 Bug Fixes
@@ -200,21 +222,20 @@
200
222
 
201
223
  ### 🧹 Chores
202
224
 
203
- - Weaviate - remove legacy filter support (#1070)
204
225
  - Update changelog after removing legacy filters (#1083)
205
226
  - Update ruff linting scripts and settings (#1105)
206
227
 
228
+ ## [integrations/weaviate-v3.0.0] - 2024-09-12
207
229
 
208
- ## [integrations/weaviate-v2.2.1] - 2024-09-07
230
+ ### 🧹 Chores
209
231
 
210
- ### 🚀 Features
232
+ - Weaviate - remove legacy filter support (#1070)
211
233
 
212
- - Add filter_policy to weaviate integration (#824)
234
+
235
+ ## [integrations/weaviate-v2.2.1] - 2024-09-07
213
236
 
214
237
  ### 🐛 Bug Fixes
215
238
 
216
- - Weaviate filter error (#811)
217
- - Fix connection to Weaviate Cloud Service (#624)
218
239
  - Pin weaviate-client (#1046)
219
240
  - Weaviate - fix connection issues with some WCS URLs (#1058)
220
241
 
@@ -222,9 +243,27 @@
222
243
 
223
244
  - Do not retry tests in `hatch run test` command (#954)
224
245
 
225
- ### ⚙️ CI
246
+ ### 🌀 Miscellaneous
226
247
 
227
- - Retry tests to reduce flakyness (#836)
248
+ - Chore: weaviate - ruff update, don't ruff tests (#986)
249
+
250
+ ## [integrations/weaviate-v2.2.0] - 2024-08-02
251
+
252
+ ### 🌀 Miscellaneous
253
+
254
+ - Fix: Weaviate: Use collections.list_all instead of collections._get_all (#921)
255
+
256
+ ## [integrations/weaviate-v2.1.3] - 2024-07-17
257
+
258
+ ### 🌀 Miscellaneous
259
+
260
+ - Fix: `weaviate` - Fallback to default filter policy when deserializing retrievers without the init parameter (#903)
261
+
262
+ ## [integrations/weaviate-v2.1.2] - 2024-07-15
263
+
264
+ ### 🚀 Features
265
+
266
+ - Add filter_policy to weaviate integration (#824)
228
267
 
229
268
  ### 🧹 Chores
230
269
 
@@ -232,15 +271,24 @@
232
271
 
233
272
  ### 🌀 Miscellaneous
234
273
 
274
+ - Chore: Minor retriever pydoc fix (#884)
275
+
276
+ ## [integrations/weaviate-v2.1.1] - 2024-06-25
277
+
278
+ ### 🐛 Bug Fixes
279
+
280
+ - Weaviate filter error (#811)
281
+ - Fix connection to Weaviate Cloud Service (#624)
282
+
283
+ ### ⚙️ CI
284
+
285
+ - Retry tests to reduce flakyness (#836)
286
+
287
+ ### 🌀 Miscellaneous
288
+
235
289
  - Handle connection to WCS and add tests
236
290
  - Revert "Handle connection to WCS and add tests"
237
-
238
- This reverts commit f48802b2ce612896fd06a13cf33dffd9f77a8859.
239
291
  - Ci: install `pytest-rerunfailures` where needed; add retry config to `test-cov` script (#845)
240
- - Chore: Minor retriever pydoc fix (#884)
241
- - Fix: `weaviate` - Fallback to default filter policy when deserializing retrievers without the init parameter (#903)
242
- - Fix: Weaviate: Use collections.list_all instead of collections._get_all (#921)
243
- - Chore: weaviate - ruff update, don't ruff tests (#986)
244
292
 
245
293
  ## [integrations/weaviate-v2.1.0] - 2024-06-10
246
294
 
@@ -58,7 +58,7 @@ APPENDIX: How to apply the Apache License to your work.
58
58
 
59
59
  To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
60
 
61
- Copyright [yyyy] [name of copyright owner]
61
+ Copyright 2023-present deepset GmbH
62
62
 
63
63
  Licensed under the Apache License, Version 2.0 (the "License");
64
64
  you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: weaviate-haystack
3
- Version: 7.7.0
3
+ Version: 7.9.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
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: haystack-ai>=2.28.0
23
23
  Requires-Dist: python-dateutil>=2.7.0
24
- Requires-Dist: weaviate-client>=4.20
24
+ Requires-Dist: weaviate-client>=4.23.0
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  # weaviate-haystack
@@ -7,7 +7,7 @@ services:
7
7
  - '8080'
8
8
  - --scheme
9
9
  - http
10
- image: semitechnologies/weaviate:1.36.2
10
+ image: cr.weaviate.io/semitechnologies/weaviate:1.39.4
11
11
  ports:
12
12
  - 8080:8080
13
13
  - 50051:50051
@@ -18,4 +18,16 @@ services:
18
18
  PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
19
19
  DEFAULT_VECTORIZER_MODULE: 'none'
20
20
  ENABLE_MODULES: ''
21
- CLUSTER_HOSTNAME: 'node1'
21
+ CLUSTER_HOSTNAME: 'node1'
22
+ healthcheck:
23
+ test:
24
+ - CMD
25
+ - busybox
26
+ - wget
27
+ - -q
28
+ - -O
29
+ - '-'
30
+ - http://127.0.0.1:8080/v1/.well-known/ready
31
+ interval: 5s
32
+ timeout: 3s
33
+ retries: 20
@@ -25,7 +25,7 @@ classifiers = [
25
25
  ]
26
26
  dependencies = [
27
27
  "haystack-ai>=2.28.0",
28
- "weaviate-client>=4.20",
28
+ "weaviate-client>=4.23.0",
29
29
  "python-dateutil>=2.7.0",
30
30
  ]
31
31
 
@@ -136,6 +136,7 @@ ignore = [
136
136
  "PLR0912",
137
137
  "PLR0913",
138
138
  "PLR0915",
139
+ "PLR0917",
139
140
  ]
140
141
 
141
142
  [tool.ruff.lint.isort]
@@ -4,6 +4,7 @@
4
4
 
5
5
  import base64
6
6
  import datetime
7
+ import importlib.metadata
7
8
  import json
8
9
  from contextlib import suppress
9
10
  from dataclasses import asdict
@@ -38,6 +39,27 @@ from .auth import AuthCredentials
38
39
 
39
40
  logger = logging.getLogger(__name__)
40
41
 
42
+ # Weaviate tracks which framework integrations talk to it through this header, so that adoption of
43
+ # weaviate-haystack shows up in Weaviate's own telemetry. See https://github.com/weaviate/weaviate/pull/10535.
44
+ _INTEGRATION_HEADER = "X-Weaviate-Client-Integration"
45
+ _INTEGRATION_NAME = "haystack-python"
46
+ _PACKAGE_NAME = "weaviate-haystack"
47
+
48
+
49
+ def _integration_header_value() -> str:
50
+ """
51
+ Builds the value of the Weaviate client integration header.
52
+
53
+ :returns:
54
+ A `<name>/<version>` string. The version falls back to `unknown` when the package metadata is
55
+ unavailable, for example when running from a source checkout that was never installed.
56
+ """
57
+ try:
58
+ version = importlib.metadata.version(_PACKAGE_NAME)
59
+ except importlib.metadata.PackageNotFoundError:
60
+ version = "unknown"
61
+ return f"{_INTEGRATION_NAME}/{version}"
62
+
41
63
 
42
64
  # This is the default collection properties for Weaviate.
43
65
  # It's a list of properties that will be created on the collection.
@@ -144,6 +166,9 @@ class WeaviateDocumentStore:
144
166
  ```
145
167
  {"X-OpenAI-Api-Key": "<THE-KEY>"}, {"X-HuggingFace-Api-Key": "<THE-KEY>"}
146
168
  ```
169
+ Every connection also carries `X-Weaviate-Client-Integration: haystack-python/<version>`, which
170
+ identifies this integration in Weaviate's telemetry. Supplying that header here, in any casing,
171
+ overrides it.
147
172
  :param embedded_options:
148
173
  If set, create an embedded Weaviate cluster inside the client. For a full list of options see
149
174
  `weaviate.embedded.EmbeddedOptions`.
@@ -183,6 +208,26 @@ class WeaviateDocumentStore:
183
208
  "properties", DOCUMENT_COLLECTION_PROPERTIES
184
209
  )
185
210
 
211
+ def _connection_headers(self) -> dict[str, Any]:
212
+ """
213
+ Builds the headers passed to the Weaviate client.
214
+
215
+ Merges the integration identification header into the user supplied `additional_headers`. A user
216
+ supplied value for the same header wins, so the header can be overridden or suppressed. The merged
217
+ headers are deliberately not stored on the instance: `to_dict()` must keep serializing only the
218
+ headers the user actually passed, so that a serialized pipeline does not bake in the version of
219
+ `weaviate-haystack` that happened to write it.
220
+
221
+ :returns:
222
+ The headers to pass to the Weaviate client.
223
+ """
224
+ headers: dict[str, Any] = dict(self._additional_headers) if self._additional_headers else {}
225
+ # The comparison is case insensitive because gRPC metadata keys are case sensitive: a user supplied
226
+ # "x-weaviate-client-integration" would otherwise end up alongside our own spelling of the header.
227
+ if not any(key.lower() == _INTEGRATION_HEADER.lower() for key in headers):
228
+ headers[_INTEGRATION_HEADER] = _integration_header_value()
229
+ return headers
230
+
186
231
  @property
187
232
  def client(self) -> weaviate.WeaviateClient:
188
233
  """Return the synchronous Weaviate client, creating and connecting it if necessary."""
@@ -199,7 +244,7 @@ class WeaviateDocumentStore:
199
244
  self._client = weaviate.connect_to_weaviate_cloud(
200
245
  self._url,
201
246
  auth_credentials=self._auth_client_secret.resolve_value(),
202
- headers=self._additional_headers,
247
+ headers=self._connection_headers(),
203
248
  additional_config=self._additional_config,
204
249
  )
205
250
  else:
@@ -216,7 +261,7 @@ class WeaviateDocumentStore:
216
261
  ),
217
262
  auth_client_secret=self._auth_client_secret.resolve_value() if self._auth_client_secret else None,
218
263
  additional_config=self._additional_config,
219
- additional_headers=self._additional_headers,
264
+ additional_headers=self._connection_headers(),
220
265
  embedded_options=self._embedded_options,
221
266
  skip_init_checks=False,
222
267
  )
@@ -244,7 +289,7 @@ class WeaviateDocumentStore:
244
289
  self._async_client = weaviate.use_async_with_weaviate_cloud(
245
290
  self._url,
246
291
  auth_credentials=self._auth_client_secret.resolve_value(),
247
- headers=self._additional_headers,
292
+ headers=self._connection_headers(),
248
293
  additional_config=self._additional_config,
249
294
  )
250
295
  else:
@@ -261,7 +306,7 @@ class WeaviateDocumentStore:
261
306
  ),
262
307
  auth_client_secret=self._auth_client_secret.resolve_value() if self._auth_client_secret else None,
263
308
  additional_config=self._additional_config,
264
- additional_headers=self._additional_headers,
309
+ additional_headers=self._connection_headers(),
265
310
  embedded_options=self._embedded_options,
266
311
  skip_init_checks=False,
267
312
  )
@@ -632,76 +677,121 @@ class WeaviateDocumentStore:
632
677
 
633
678
  return result
634
679
 
680
+ @staticmethod
681
+ def _compute_field_unique_values(
682
+ agg_result: Any, search_term: str | None, from_: int, size: int
683
+ ) -> tuple[list[Any], int]:
684
+ """
685
+ Computes paginated unique values from a Weaviate group-by aggregation result.
686
+
687
+ Weaviate's `like` filter only supports case-sensitive substring matching, so instead of
688
+ filtering server-side (which would be case-sensitive), all groups are fetched unconditionally
689
+ and `search_term` is applied here as a case-insensitive substring match in Python.
690
+
691
+ :param agg_result: The group-by aggregation result, with one group per unique field value.
692
+ :param search_term: Optional term to filter the values by, matched as a case-insensitive
693
+ substring against each value.
694
+ :param from_: The starting offset for pagination (0-indexed).
695
+ :param size: The maximum number of unique values to return.
696
+ :returns: A tuple of (paginated list of unique values in their original type, total count of
697
+ unique values). See `get_metadata_field_unique_values`'s docstring for a caveat on scalar
698
+ int fields - `agg_result`'s numeric group keys always decode as `float`.
699
+ """
700
+ all_values = [g.grouped_by.value for g in agg_result.groups] if agg_result.groups else []
701
+ if search_term:
702
+ search_term_lower = search_term.lower()
703
+ all_values = [value for value in all_values if search_term_lower in str(value).lower()]
704
+ all_values.sort(key=str) # Sort for consistent pagination
705
+ total_count = len(all_values)
706
+
707
+ paginated_values = all_values[from_ : from_ + size]
708
+
709
+ return paginated_values, total_count
710
+
635
711
  def get_metadata_field_unique_values(
636
712
  self,
637
713
  metadata_field: str,
638
714
  search_term: str | None = None,
639
715
  from_: int = 0,
640
- size: int = 10000,
641
- ) -> tuple[list[str], int]:
716
+ size: int = 10,
717
+ filters: dict[str, Any] | None = None,
718
+ ) -> tuple[list[Any], int]:
642
719
  """
643
720
  Returns unique values for a metadata field with pagination support.
644
721
 
645
722
  :param metadata_field: The metadata field name to get unique values for.
646
723
  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).
724
+ :param search_term: Optional term to filter the metadata field's values by
725
+ before returning them. If provided, only values of `metadata_field` that
726
+ contain this term will be considered.
727
+ Note: Uses case-insensitive substring matching (no stemming).
651
728
  :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.
653
- :returns: A tuple of (list of unique values, total count of unique values).
654
- :raises ValueError: If the field is not found in the collection schema.
729
+ :param size: The maximum number of unique values to return. Defaults to 10.
730
+ :param filters: Optional filters to restrict the documents considered.
731
+ :returns: A tuple of (list of unique values in their original type, total count of unique values).
732
+
733
+ **Note**: a scalar `int` metadata value comes back as `float`, not `int`. weaviate-client has no
734
+ wire-protocol field for a scalar int - non-list properties are packed into a
735
+ `google.protobuf.Struct`, whose `Value` type only has `number_value` (a double), so the int/float
736
+ distinction is lost before the value reaches Weaviate. `GroupByAggregate` decodes numeric group
737
+ keys the same way, so this holds even when the field's schema type is explicitly `DataType.INT`.
738
+ List-valued int fields (e.g. `meta={"tags": [1, 2]}`) are unaffected - those go through a
739
+ dedicated `IntArrayProperties` wire type instead.
655
740
  """
656
741
  field_name = _normalize_metadata_field_name(metadata_field)
657
742
 
658
743
  config = self.collection.config.get()
659
744
  schema_fields = {prop.name for prop in config.properties}
660
745
  if field_name not in schema_fields:
661
- msg = f"Field '{field_name}' not found in collection schema"
662
- raise ValueError(msg)
746
+ # No document has ever written this field, so Weaviate's auto-schema never created a
747
+ # property for it - there are no values to aggregate.
748
+ return [], 0
663
749
 
664
750
  weaviate_filter = None
665
- if search_term:
666
- weaviate_filter = convert_filters({"field": "content", "operator": "contains", "value": search_term})
751
+ if filters:
752
+ validate_filters(filters)
753
+ weaviate_filter = convert_filters(filters)
667
754
 
668
755
  # Weaviate's GroupByAggregate has a default limit of 100 groups.
669
756
  # We use the document count as the limit to ensure all unique values are retrieved,
670
757
  # since the number of unique values cannot exceed the number of documents.
671
- doc_count = self.collection.aggregate.over_all(total_count=True).total_count or 0
758
+ doc_count = self.collection.aggregate.over_all(filters=weaviate_filter, total_count=True).total_count or 0
672
759
 
673
760
  agg_result = self.collection.aggregate.over_all(
674
761
  filters=weaviate_filter, group_by=GroupByAggregate(prop=field_name, limit=doc_count)
675
762
  )
676
763
 
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
764
+ return self._compute_field_unique_values(agg_result, search_term, from_, size)
684
765
 
685
766
  async def get_metadata_field_unique_values_async(
686
767
  self,
687
768
  metadata_field: str,
688
769
  search_term: str | None = None,
689
770
  from_: int = 0,
690
- size: int = 10000,
691
- ) -> tuple[list[str], int]:
771
+ size: int = 10,
772
+ filters: dict[str, Any] | None = None,
773
+ ) -> tuple[list[Any], int]:
692
774
  """
693
775
  Asynchronously returns unique values for a metadata field with pagination support.
694
776
 
695
777
  :param metadata_field: The metadata field name to get unique values for.
696
778
  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).
779
+ :param search_term: Optional term to filter the metadata field's values by
780
+ before returning them. If provided, only values of `metadata_field` that
781
+ contain this term will be considered.
782
+ Note: Uses case-insensitive substring matching (no stemming).
701
783
  :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.
703
- :returns: A tuple of (list of unique values, total count of unique values).
704
- :raises ValueError: If the field is not found in the collection schema.
784
+ :param size: The maximum number of unique values to return. Defaults to 10.
785
+ :param filters: Optional filters to restrict the documents considered.
786
+ :returns: A tuple of (list of unique values in their original type, total count of unique values).
787
+
788
+ **Note**: a scalar `int` metadata value comes back as `float`, not `int`. weaviate-client has no
789
+ wire-protocol field for a scalar int - non-list properties are packed into a
790
+ `google.protobuf.Struct`, whose `Value` type only has `number_value` (a double), so the int/float
791
+ distinction is lost before the value reaches Weaviate. `GroupByAggregate` decodes numeric group
792
+ keys the same way, so this holds even when the field's schema type is explicitly `DataType.INT`.
793
+ List-valued int fields (e.g. `meta={"tags": [1, 2]}`) are unaffected - those go through a
794
+ dedicated `IntArrayProperties` wire type instead.
705
795
  """
706
796
  field_name = _normalize_metadata_field_name(metadata_field)
707
797
 
@@ -709,30 +799,26 @@ class WeaviateDocumentStore:
709
799
  config = await collection.config.get()
710
800
  schema_fields = {prop.name for prop in config.properties}
711
801
  if field_name not in schema_fields:
712
- msg = f"Field '{field_name}' not found in collection schema"
713
- raise ValueError(msg)
802
+ # No document has ever written this field, so Weaviate's auto-schema never created a
803
+ # property for it - there are no values to aggregate.
804
+ return [], 0
714
805
 
715
806
  weaviate_filter = None
716
- if search_term:
717
- weaviate_filter = convert_filters({"field": "content", "operator": "contains", "value": search_term})
807
+ if filters:
808
+ validate_filters(filters)
809
+ weaviate_filter = convert_filters(filters)
718
810
 
719
811
  # Weaviate's GroupByAggregate has a default limit of 100 groups.
720
812
  # We use the document count as the limit to ensure all unique values are retrieved,
721
813
  # since the number of unique values cannot exceed the number of documents.
722
- doc_count_result = await collection.aggregate.over_all(total_count=True)
814
+ doc_count_result = await collection.aggregate.over_all(filters=weaviate_filter, total_count=True)
723
815
  doc_count = doc_count_result.total_count or 0
724
816
 
725
817
  agg_result = await collection.aggregate.over_all(
726
818
  filters=weaviate_filter, group_by=GroupByAggregate(prop=field_name, limit=doc_count)
727
819
  )
728
820
 
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
821
+ return self._compute_field_unique_values(agg_result, search_term, from_, size)
736
822
 
737
823
  @staticmethod
738
824
  def _to_data_object(document: Document) -> dict[str, Any]:
@@ -3,6 +3,7 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import base64
6
+ import importlib.metadata
6
7
  import logging
7
8
  import os
8
9
  from collections.abc import Generator
@@ -43,6 +44,7 @@ from haystack_integrations.document_stores.weaviate.auth import AuthApiKey
43
44
  from haystack_integrations.document_stores.weaviate.document_store import (
44
45
  DOCUMENT_COLLECTION_PROPERTIES,
45
46
  WeaviateDocumentStore,
47
+ _integration_header_value,
46
48
  )
47
49
 
48
50
 
@@ -81,7 +83,10 @@ def test_client_connects_to_weaviate_cloud(mock_connect, monkeypatch):
81
83
 
82
84
  mock_connect.assert_called_once()
83
85
  _args, kwargs = mock_connect.call_args
84
- assert kwargs["headers"] == {"X-HuggingFace-Api-Key": "k"}
86
+ assert kwargs["headers"] == {
87
+ "X-HuggingFace-Api-Key": "k",
88
+ "X-Weaviate-Client-Integration": _integration_header_value(),
89
+ }
85
90
 
86
91
 
87
92
  @pytest.mark.asyncio
@@ -100,9 +105,84 @@ async def test_async_client_connects_to_weaviate_cloud(mock_connect, monkeypatch
100
105
  mock_client.collections.exists = exists
101
106
  mock_connect.return_value = mock_client
102
107
 
103
- ds = WeaviateDocumentStore(url="rAnD0m.something.weaviate.cloud", auth_client_secret=AuthApiKey())
108
+ ds = WeaviateDocumentStore(
109
+ url="rAnD0m.something.weaviate.cloud",
110
+ auth_client_secret=AuthApiKey(),
111
+ additional_headers={"X-HuggingFace-Api-Key": "k"},
112
+ )
104
113
  assert await ds.async_client is mock_client
114
+
105
115
  mock_connect.assert_called_once()
116
+ _args, kwargs = mock_connect.call_args
117
+ assert kwargs["headers"] == {
118
+ "X-HuggingFace-Api-Key": "k",
119
+ "X-Weaviate-Client-Integration": _integration_header_value(),
120
+ }
121
+
122
+
123
+ @patch("haystack_integrations.document_stores.weaviate.document_store.weaviate.WeaviateClient")
124
+ def test_client_sends_integration_header(mock_weaviate_client_class):
125
+ mock_client = MagicMock()
126
+ mock_client.collections.exists.return_value = True
127
+ mock_weaviate_client_class.return_value = mock_client
128
+
129
+ ds = WeaviateDocumentStore(url="http://localhost:8080")
130
+ ds.client # noqa: B018
131
+
132
+ headers = mock_weaviate_client_class.call_args.kwargs["additional_headers"]
133
+ assert headers["X-Weaviate-Client-Integration"].startswith("haystack-python/")
134
+
135
+
136
+ @pytest.mark.asyncio
137
+ @patch("haystack_integrations.document_stores.weaviate.document_store.weaviate.WeaviateAsyncClient")
138
+ async def test_async_client_sends_integration_header(mock_weaviate_async_client_class):
139
+ mock_client = MagicMock()
140
+
141
+ async def connect() -> None:
142
+ return None
143
+
144
+ async def exists(_name: str) -> bool:
145
+ return True
146
+
147
+ mock_client.connect = connect
148
+ mock_client.collections.exists = exists
149
+ mock_weaviate_async_client_class.return_value = mock_client
150
+
151
+ ds = WeaviateDocumentStore(url="http://localhost:8080")
152
+ await ds.async_client
153
+
154
+ headers = mock_weaviate_async_client_class.call_args.kwargs["additional_headers"]
155
+ assert headers["X-Weaviate-Client-Integration"].startswith("haystack-python/")
156
+
157
+
158
+ @pytest.mark.parametrize("header_name", ["X-Weaviate-Client-Integration", "x-weaviate-client-integration"])
159
+ @patch("haystack_integrations.document_stores.weaviate.document_store.weaviate.WeaviateClient")
160
+ def test_user_supplied_integration_header_wins(mock_weaviate_client_class, header_name):
161
+ mock_client = MagicMock()
162
+ mock_client.collections.exists.return_value = True
163
+ mock_weaviate_client_class.return_value = mock_client
164
+
165
+ ds = WeaviateDocumentStore(url="http://localhost:8080", additional_headers={header_name: "custom/1.0"})
166
+ ds.client # noqa: B018
167
+
168
+ headers = mock_weaviate_client_class.call_args.kwargs["additional_headers"]
169
+ assert headers == {header_name: "custom/1.0"}
170
+
171
+
172
+ def test_integration_header_value_falls_back_to_unknown_version(monkeypatch):
173
+ def raise_package_not_found(_package_name: str) -> str:
174
+ raise importlib.metadata.PackageNotFoundError
175
+
176
+ monkeypatch.setattr(importlib.metadata, "version", raise_package_not_found)
177
+
178
+ assert _integration_header_value() == "haystack-python/unknown"
179
+
180
+
181
+ def test_integration_header_is_not_serialized():
182
+ assert WeaviateDocumentStore().to_dict()["init_parameters"]["additional_headers"] is None
183
+
184
+ ds = WeaviateDocumentStore(additional_headers={"X-HuggingFace-Api-Key": "k"})
185
+ assert ds.to_dict()["init_parameters"]["additional_headers"] == {"X-HuggingFace-Api-Key": "k"}
106
186
 
107
187
 
108
188
  def test_to_data_object_with_sparse_embedding_logs_warning(caplog):
@@ -325,7 +405,10 @@ class TestWeaviateDocumentStore(
325
405
  mock_weaviate_client_class.assert_called_once_with(
326
406
  auth_client_secret=AuthApiKey().resolve_value(),
327
407
  connection_params=None,
328
- additional_headers={"X-HuggingFace-Api-Key": "MY_HUGGINGFACE_KEY"},
408
+ additional_headers={
409
+ "X-HuggingFace-Api-Key": "MY_HUGGINGFACE_KEY",
410
+ "X-Weaviate-Client-Integration": _integration_header_value(),
411
+ },
329
412
  embedded_options=EmbeddedOptions(
330
413
  persistence_data_path=DEFAULT_PERSISTENCE_DATA_PATH,
331
414
  binary_path=DEFAULT_BINARY_PATH,
@@ -1144,60 +1227,59 @@ class TestWeaviateDocumentStore(
1144
1227
  metadata_fields=["nonexistent_field"],
1145
1228
  )
1146
1229
 
1147
- def test_get_metadata_field_unique_values_with_meta_prefix(self, document_store):
1148
- docs = [
1149
- Document(content="Doc 1", meta={"category": "TypeA"}),
1150
- Document(content="Doc 2", meta={"category": "TypeB"}),
1151
- ]
1152
- document_store.write_documents(docs)
1153
-
1154
- values, total_count = document_store.get_metadata_field_unique_values("meta.category")
1155
- assert total_count == 2
1156
- assert set(values) == {"TypeA", "TypeB"}
1230
+ # --- Overrides of mixin tests to account for Weaviate-specific behaviour ---
1157
1231
 
1158
- def test_get_metadata_field_unique_values_with_search_term(self, document_store):
1232
+ def test_get_metadata_field_unique_values_preserves_type(self, document_store):
1233
+ """
1234
+ Override: weaviate-client has no wire-protocol field for a scalar int - google.protobuf.Struct's
1235
+ Value type only has `number_value` (a double), so a metadata int is indistinguishable from a
1236
+ float by the time it reaches Weaviate. GroupByAggregate consequently always returns numeric
1237
+ group keys as float, regardless of the field's declared schema type.
1238
+ """
1159
1239
  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"}),
1240
+ Document(content="Doc 1", meta={"priority": 1}),
1241
+ Document(content="Doc 2", meta={"priority": 2}),
1242
+ Document(content="Doc 3", meta={"priority": 1}),
1164
1243
  ]
1165
1244
  document_store.write_documents(docs)
1166
1245
 
1167
- values, total_count = document_store.get_metadata_field_unique_values("category", search_term="Python")
1246
+ values, total_count = document_store.get_metadata_field_unique_values(metadata_field="priority")
1247
+
1248
+ assert set(values) == {1, 2}
1249
+ assert all(isinstance(value, float) for value in values) # not int - see docstring
1168
1250
  assert total_count == 2
1169
- assert set(values) == {"TypeA", "TypeC"}
1170
1251
 
1171
- def test_get_metadata_field_unique_values_with_pagination(self, document_store):
1252
+ def test_get_metadata_field_unique_values_distinct_types(self, document_store):
1253
+ """
1254
+ Override: the base mixin test stores int, float, str and bool under the *same* metadata field
1255
+ name and expects all four back as distinct values. Weaviate's collection schema fields are
1256
+ strongly single-typed, so a field can't simultaneously hold int/str/float/bool values.
1257
+
1258
+ This adapts the same intent - int, float, str and bool must come back as distinct, unmangled
1259
+ types via get_metadata_field_unique_values() - using one field per type instead of one shared
1260
+ field, which is what Weaviate can actually support.
1261
+ """
1172
1262
  docs = [
1173
- Document(content="Doc 1", meta={"category": "TypeA"}),
1174
- Document(content="Doc 2", meta={"category": "TypeB"}),
1175
- Document(content="Doc 3", meta={"category": "TypeC"}),
1176
- Document(content="Doc 4", meta={"category": "TypeD"}),
1177
- Document(content="Doc 5", meta={"category": "TypeE"}),
1263
+ Document(content="Doc 1", meta={"priority_int": 1}),
1264
+ Document(content="Doc 2", meta={"priority_str": "1"}),
1265
+ Document(content="Doc 3", meta={"priority_float": 1.5}),
1266
+ Document(content="Doc 4", meta={"priority_bool": True}),
1178
1267
  ]
1179
1268
  document_store.write_documents(docs)
1180
1269
 
1181
- values, total_count = document_store.get_metadata_field_unique_values("category", from_=0, size=2)
1182
- assert total_count == 5
1183
- assert len(values) == 2
1184
-
1185
- values2, total_count2 = document_store.get_metadata_field_unique_values("category", from_=2, size=2)
1186
- assert total_count2 == 5
1187
- assert len(values2) == 2
1188
-
1189
- assert set(values).isdisjoint(set(values2))
1190
-
1191
- def test_get_metadata_field_unique_values_field_not_found(self, document_store):
1192
- with pytest.raises(ValueError, match="not found in collection schema"):
1193
- document_store.get_metadata_field_unique_values("nonexistent_field")
1194
-
1195
- def test_get_metadata_field_unique_values_empty_result(self, document_store):
1196
- values, total_count = document_store.get_metadata_field_unique_values("category")
1197
- assert total_count == 0
1198
- assert values == []
1199
-
1200
- # --- Overrides of mixin tests to account for Weaviate-specific behaviour ---
1270
+ int_values, int_count = document_store.get_metadata_field_unique_values(metadata_field="priority_int")
1271
+ str_values, str_count = document_store.get_metadata_field_unique_values(metadata_field="priority_str")
1272
+ float_values, float_count = document_store.get_metadata_field_unique_values(metadata_field="priority_float")
1273
+ bool_values, bool_count = document_store.get_metadata_field_unique_values(metadata_field="priority_bool")
1274
+
1275
+ assert (int_count, str_count, float_count, bool_count) == (1, 1, 1, 1)
1276
+ # int type fidelity isn't asserted here: GroupByAggregate returns numeric group keys as float
1277
+ # regardless of the field's declared schema type, the same known gap covered by
1278
+ # test_get_metadata_field_unique_values_preserves_type - not specific to field-sharing.
1279
+ assert int_values == [1]
1280
+ assert str_values == ["1"] and type(str_values[0]) is str
1281
+ assert float_values == [1.5] and type(float_values[0]) is float
1282
+ assert bool_values == [True] and type(bool_values[0]) is bool
1201
1283
 
1202
1284
  def test_count_documents_by_filter_simple(self, document_store):
1203
1285
  docs = [
@@ -440,65 +440,69 @@ class TestWeaviateDocumentStoreAsync(
440
440
  metadata_fields=["nonexistent_field"],
441
441
  )
442
442
 
443
- @pytest.mark.asyncio
444
- async def test_get_metadata_field_unique_values_async_with_meta_prefix(self, document_store):
445
- docs = [
446
- Document(content="Doc 1", meta={"category": "TypeA"}),
447
- Document(content="Doc 2", meta={"category": "TypeB"}),
448
- ]
449
- await document_store.write_documents_async(docs)
450
-
451
- values, total_count = await document_store.get_metadata_field_unique_values_async("meta.category")
452
- assert total_count == 2
453
- assert set(values) == {"TypeA", "TypeB"}
443
+ # --- Overrides of mixin tests to account for Weaviate-specific behaviour ---
454
444
 
455
445
  @pytest.mark.asyncio
456
- async def test_get_metadata_field_unique_values_async_with_search_term(self, document_store):
446
+ async def test_get_metadata_field_unique_values_preserves_type_async(self, document_store):
447
+ """
448
+ Override: weaviate-client has no wire-protocol field for a scalar int - google.protobuf.Struct's
449
+ Value type only has `number_value` (a double), so a metadata int is indistinguishable from a
450
+ float by the time it reaches Weaviate. GroupByAggregate consequently always returns numeric
451
+ group keys as float, regardless of the field's declared schema type.
452
+ """
457
453
  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"}),
454
+ Document(content="Doc 1", meta={"priority": 1}),
455
+ Document(content="Doc 2", meta={"priority": 2}),
456
+ Document(content="Doc 3", meta={"priority": 1}),
462
457
  ]
463
458
  await document_store.write_documents_async(docs)
464
459
 
465
- values, total_count = await document_store.get_metadata_field_unique_values_async(
466
- "category", search_term="Python"
467
- )
460
+ values, total_count = await document_store.get_metadata_field_unique_values_async(metadata_field="priority")
461
+
462
+ assert set(values) == {1, 2}
463
+ assert all(isinstance(value, float) for value in values) # not int - see docstring
468
464
  assert total_count == 2
469
- assert set(values) == {"TypeA", "TypeC"}
470
465
 
471
466
  @pytest.mark.asyncio
472
- async def test_get_metadata_field_unique_values_async_with_pagination(self, document_store):
467
+ async def test_get_metadata_field_unique_values_distinct_types_async(self, document_store):
468
+ """
469
+ Override: the base mixin test stores int, float, str and bool under the *same* metadata field
470
+ name and expects all four back as distinct values. Weaviate's collection schema fields are
471
+ strongly single-typed, so a field can't simultaneously hold int/str/float/bool values.
472
+
473
+ This adapts the same intent - int, float, str and bool must come back as distinct, unmangled
474
+ types via get_metadata_field_unique_values_async() - using one field per type instead of one
475
+ shared field, which is what Weaviate can actually support.
476
+ """
473
477
  docs = [
474
- Document(content="Doc 1", meta={"category": "TypeA"}),
475
- Document(content="Doc 2", meta={"category": "TypeB"}),
476
- Document(content="Doc 3", meta={"category": "TypeC"}),
477
- Document(content="Doc 4", meta={"category": "TypeD"}),
478
- Document(content="Doc 5", meta={"category": "TypeE"}),
478
+ Document(content="Doc 1", meta={"priority_int": 1}),
479
+ Document(content="Doc 2", meta={"priority_str": "1"}),
480
+ Document(content="Doc 3", meta={"priority_float": 1.5}),
481
+ Document(content="Doc 4", meta={"priority_bool": True}),
479
482
  ]
480
483
  await document_store.write_documents_async(docs)
481
484
 
482
- values, total_count = await document_store.get_metadata_field_unique_values_async("category", from_=0, size=2)
483
- assert total_count == 5
484
- assert len(values) == 2
485
-
486
- values2, total_count2 = await document_store.get_metadata_field_unique_values_async("category", from_=2, size=2)
487
- assert total_count2 == 5
488
- assert len(values2) == 2
489
-
490
- assert set(values).isdisjoint(set(values2))
491
-
492
- @pytest.mark.asyncio
493
- async def test_get_metadata_field_unique_values_async_field_not_found(self, document_store):
494
- with pytest.raises(ValueError, match="not found in collection schema"):
495
- await document_store.get_metadata_field_unique_values_async("nonexistent_field")
485
+ int_values, int_count = await document_store.get_metadata_field_unique_values_async(
486
+ metadata_field="priority_int"
487
+ )
488
+ str_values, str_count = await document_store.get_metadata_field_unique_values_async(
489
+ metadata_field="priority_str"
490
+ )
491
+ float_values, float_count = await document_store.get_metadata_field_unique_values_async(
492
+ metadata_field="priority_float"
493
+ )
494
+ bool_values, bool_count = await document_store.get_metadata_field_unique_values_async(
495
+ metadata_field="priority_bool"
496
+ )
496
497
 
497
- @pytest.mark.asyncio
498
- async def test_get_metadata_field_unique_values_async_empty_result(self, document_store):
499
- values, total_count = await document_store.get_metadata_field_unique_values_async("category")
500
- assert total_count == 0
501
- assert values == []
498
+ assert (int_count, str_count, float_count, bool_count) == (1, 1, 1, 1)
499
+ # int type fidelity isn't asserted here: GroupByAggregate returns numeric group keys as float
500
+ # regardless of the field's declared schema type, the same known gap covered by
501
+ # test_get_metadata_field_unique_values_preserves_type_async - not specific to field-sharing.
502
+ assert int_values == [1]
503
+ assert str_values == ["1"] and type(str_values[0]) is str
504
+ assert float_values == [1.5] and type(float_values[0]) is float
505
+ assert bool_values == [True] and type(bool_values[0]) is bool
502
506
 
503
507
  @pytest.mark.asyncio
504
508
  async def test_delete_all_documents_excessive_batch_size_async(