qdrant-haystack 10.5.0__tar.gz → 11.0.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 (27) hide show
  1. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/CHANGELOG.md +52 -19
  2. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/LICENSE.txt +1 -1
  3. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/PKG-INFO +2 -2
  4. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/document_stores/qdrant/document_store.py +119 -101
  5. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_document_store.py +27 -73
  6. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_document_store_async.py +14 -40
  7. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/.gitignore +0 -0
  8. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/README.md +0 -0
  9. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/examples/embedding_retrieval.py +0 -0
  10. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/pydoc/config_docusaurus.yml +0 -0
  11. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/pyproject.toml +0 -0
  12. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  13. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/components/retrievers/qdrant/__init__.py +0 -0
  14. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/components/retrievers/qdrant/retriever.py +0 -0
  15. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/document_stores/py.typed +0 -0
  16. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/document_stores/qdrant/__init__.py +0 -0
  17. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/document_stores/qdrant/converters.py +0 -0
  18. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/document_stores/qdrant/filters.py +0 -0
  19. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/src/haystack_integrations/document_stores/qdrant/migrate_to_sparse.py +0 -0
  20. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/__init__.py +0 -0
  21. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/conftest.py +0 -0
  22. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_converters.py +0 -0
  23. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_dict_converters.py +0 -0
  24. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_embedding_retriever.py +0 -0
  25. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_filters.py +0 -0
  26. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_hybrid_retriever.py +0 -0
  27. {qdrant_haystack-10.5.0 → qdrant_haystack-11.0.0}/tests/test_sparse_embedding_retriever.py +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/qdrant-v10.6.0] - 2026-09-01
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - Standardize license files (#3771)
8
+ - `QdrantDocumentStore` removing duplicated tests already covered by haystack Mixin tests + fixing dedup + adding missing Async test (#3785)
9
+
10
+
11
+ ## [integrations/qdrant-v10.5.0] - 2026-08-03
12
+
13
+ ### 🚀 Features
14
+
15
+ - *(qdrant)* Expose RRF k and weights parameters in QdrantHybridRetriever (#3702)
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - `Qdrant` normalisation of .meta usage in `get_metadata_field_unique_values()` (#3642)
20
+ - `Qdrant` adding offset based pagination to `get_metadata_field_unique_values()` (#3697)
21
+
22
+
3
23
  ## [integrations/qdrant-v10.4.0] - 2026-07-27
4
24
 
5
25
  ### 🚀 Features
@@ -67,7 +87,7 @@
67
87
 
68
88
  - Feat: `QdrantDocumentStore` return number deleted docs on `delete_by_filter` (#2807)
69
89
 
70
- ## [integrations/qdrant-v9.6.0] - 2026-02-02
90
+ ## [integrations/qdrant-v10.1.0] - 2026-02-02
71
91
 
72
92
  ### 🚀 Features
73
93
 
@@ -75,11 +95,15 @@
75
95
 
76
96
  ### 🧹 Chores
77
97
 
78
- - Make fmt command more forgiving (#2671)
79
- - [**breaking**] Qdrant - drop Python 3.9 and use X|Y typing (#2726)
80
98
  - Disabling progress bar in `QdrantDocumentStore` tests (#2797)
81
99
 
82
- ### 🌀 Miscellaneous
100
+
101
+ ## [integrations/qdrant-v10.0.0] - 2026-01-13
102
+
103
+ ### 🧹 Chores
104
+
105
+ - Make fmt command more forgiving (#2671)
106
+ - [**breaking**] Qdrant - drop Python 3.9 and use X|Y typing (#2726)
83
107
 
84
108
 
85
109
  ## [integrations/qdrant-v9.5.0] - 2026-01-07
@@ -139,6 +163,8 @@
139
163
  - Align core-integrations Hatch scripts (#1898)
140
164
  - Update md files for new hatch scripts (#1911)
141
165
 
166
+ ## [integrations/qdrant-v9.1.3] - 2025-05-28
167
+
142
168
  ### 🌀 Miscellaneous
143
169
 
144
170
  - Add pins for Qdrant (#1853)
@@ -362,6 +388,18 @@
362
388
  - *(Qdrant)* Allow payload indexing + on disk vectors (#553)
363
389
  - Qdrant datetime filtering support (#570)
364
390
 
391
+ ### 📚 Documentation
392
+
393
+ - Disable-class-def (#556)
394
+
395
+ ### 🌀 Miscellaneous
396
+
397
+ - Make tests show coverage (#566)
398
+ - Remove references to Python 3.7 (#601)
399
+ - Make Document Stores initially skip `SparseEmbedding` (#606)
400
+
401
+ ## [integrations/qdrant-v3.1.0] - 2024-03-06
402
+
365
403
  ### 🐛 Bug Fixes
366
404
 
367
405
  - Fix: fix linter errors (#282)
@@ -376,18 +414,11 @@
376
414
 
377
415
  - Update category slug (#442)
378
416
  - Small consistency improvements (#536)
379
- - Disable-class-def (#556)
380
417
 
381
418
  ### ⚙️ CI
382
419
 
383
420
  - Generate API docs for Qdrant (#361)
384
421
 
385
- ### 🌀 Miscellaneous
386
-
387
- - Make tests show coverage (#566)
388
- - Remove references to Python 3.7 (#601)
389
- - Make Document Stores initially skip `SparseEmbedding` (#606)
390
-
391
422
  ## [integrations/qdrant-v3.0.0] - 2024-01-22
392
423
 
393
424
  ### 🌀 Miscellaneous
@@ -396,20 +427,22 @@
396
427
 
397
428
  ## [integrations/qdrant-v2.0.1] - 2024-01-18
398
429
 
399
- ### 🚀 Features
400
-
401
- - Add Qdrant integration (#98)
402
-
403
430
  ### 🐛 Bug Fixes
404
431
 
405
432
  - Fix import paths for beta5 (#237)
406
433
 
407
- ### 🚜 Refactor
408
-
409
- - Use `hatch_vcs` to manage integrations versioning (#103)
410
-
411
434
  ### 🌀 Miscellaneous
412
435
 
413
436
  - Renamed QdrntRetriever to QdrntEmbeddingRetriever (#174)
414
437
 
438
+ ## [integrations/qdrant-v2.0.0] - 2023-12-18
439
+
440
+ ### 🚀 Features
441
+
442
+ - Add Qdrant integration (#98)
443
+
444
+ ### 🚜 Refactor
445
+
446
+ - Use `hatch_vcs` to manage integrations versioning (#103)
447
+
415
448
  <!-- generated by git-cliff -->
@@ -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: qdrant-haystack
3
- Version: 10.5.0
3
+ Version: 11.0.0
4
4
  Summary: An integration of Qdrant ANN vector database backend 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/qdrant/README.md
@@ -15,7 +15,6 @@ from haystack.utils import Secret, deserialize_secrets_inplace
15
15
  from haystack.utils.misc import _normalize_metadata_field_name
16
16
  from numpy import exp
17
17
  from qdrant_client.http import models as rest
18
- from qdrant_client.http.exceptions import UnexpectedResponse
19
18
  from tqdm import tqdm
20
19
 
21
20
  from .converters import (
@@ -338,6 +337,9 @@ class QdrantDocumentStore:
338
337
  def count_documents(self) -> int:
339
338
  """
340
339
  Returns the number of documents present in the Document Store.
340
+
341
+ :returns: The number of documents in the collection.
342
+ :raises QdrantStoreError: Counting documents fails.
341
343
  """
342
344
  self._initialize_client()
343
345
  assert self._client is not None
@@ -346,15 +348,16 @@ class QdrantDocumentStore:
346
348
  collection_name=self.index,
347
349
  )
348
350
  return response.count
349
- except (UnexpectedResponse, ValueError):
350
- # Qdrant local raises ValueError if the collection is not found, but
351
- # with the remote server UnexpectedResponse is raised. Until that's unified,
352
- # we need to catch both.
353
- return 0
351
+ except Exception as e:
352
+ msg = f"Error when calling QdrantDocumentStore.count_documents(): {e}"
353
+ raise QdrantStoreError(msg) from e
354
354
 
355
355
  async def count_documents_async(self) -> int:
356
356
  """
357
- Asynchronously returns the number of documents present in the document dtore.
357
+ Asynchronously returns the number of documents present in the document store.
358
+
359
+ :returns: The number of documents in the collection.
360
+ :raises QdrantStoreError: Counting documents fails.
358
361
  """
359
362
  await self._initialize_async_client()
360
363
  assert self._async_client is not None
@@ -363,11 +366,9 @@ class QdrantDocumentStore:
363
366
  collection_name=self.index,
364
367
  )
365
368
  return response.count
366
- except (UnexpectedResponse, ValueError):
367
- # Qdrant local raises ValueError if the collection is not found, but
368
- # with the remote server UnexpectedResponse is raised. Until that's unified,
369
- # we need to catch both.
370
- return 0
369
+ except Exception as e:
370
+ msg = f"Error when calling QdrantDocumentStore.count_documents_async(): {e}"
371
+ raise QdrantStoreError(msg) from e
371
372
 
372
373
  def filter_documents(
373
374
  self,
@@ -716,7 +717,7 @@ class QdrantDocumentStore:
716
717
 
717
718
  @staticmethod
718
719
  def _process_records_count_unique(
719
- records: list[Any], metadata_fields: list[str], unique_values_by_field: dict[str, set[Any]]
720
+ records: list[Any], metadata_fields: list[str], unique_values_by_field: dict[str, set[tuple[type, Any]]]
720
721
  ) -> None:
721
722
  """
722
723
  Update unique_values_by_field from a batch of Qdrant records.
@@ -730,10 +731,11 @@ class QdrantDocumentStore:
730
731
  if field in meta:
731
732
  value = meta[field]
732
733
  if value is not None:
733
- if isinstance(value, (list, dict)):
734
- unique_values_by_field[field].add(str(value))
735
- else:
736
- unique_values_by_field[field].add(value)
734
+ # Values of different types can be equal in Python (`1 == True`, `1 == 1.0`), so
735
+ # a plain set would silently merge them. Dedupe by (type, value) instead - lists
736
+ # and dicts aren't hashable, so they're keyed by their str() form.
737
+ hashable_value = str(value) if isinstance(value, (list, dict)) else value
738
+ unique_values_by_field[field].add((type(value), hashable_value))
737
739
 
738
740
  @staticmethod
739
741
  def _process_records_unique_values(
@@ -752,9 +754,13 @@ class QdrantDocumentStore:
752
754
  if value is not None:
753
755
  if search_term is not None and search_term.lower() not in str(value).lower():
754
756
  continue
757
+ # Values of different types can be equal in Python (`1 == True`, `1 == 1.0`), so
758
+ # a plain set would silently merge them. Dedupe by (type, value) instead - lists
759
+ # and dicts aren't hashable, so they're keyed by their str() form.
755
760
  hashable_value = str(value) if isinstance(value, (list, dict)) else value
756
- if hashable_value not in unique_values_set:
757
- unique_values_set.add(hashable_value)
761
+ dedup_key = (type(value), hashable_value)
762
+ if dedup_key not in unique_values_set:
763
+ unique_values_set.add(dedup_key)
758
764
  unique_values.append(value)
759
765
 
760
766
  @staticmethod
@@ -916,38 +922,39 @@ class QdrantDocumentStore:
916
922
  Deletes all documents from the document store.
917
923
 
918
924
  :param recreate_index: Whether to recreate the index after deleting all documents.
925
+ :raises QdrantStoreError: Deleting documents or recreating the collection fails.
919
926
  """
920
927
 
921
928
  self._initialize_client()
922
929
  assert self._client is not None
923
930
 
924
- if recreate_index:
925
- # get current collection config as json
926
- collection_info = self._client.get_collection(collection_name=self.index)
927
- info_json = collection_info.model_dump()
931
+ try:
932
+ if recreate_index:
933
+ # get current collection config as json
934
+ collection_info = self._client.get_collection(collection_name=self.index)
935
+ info_json = collection_info.model_dump()
928
936
 
929
- # deal with the Optional use_sparse_embeddings
930
- sparse_vectors = info_json["config"]["params"]["sparse_vectors"]
931
- use_sparse_embeddings = True if sparse_vectors else False
937
+ # deal with the Optional use_sparse_embeddings
938
+ sparse_vectors = info_json["config"]["params"]["sparse_vectors"]
939
+ use_sparse_embeddings = True if sparse_vectors else False
932
940
 
933
- # deal with the Optional sparse_idf
934
- hnsw_config = info_json["config"]["params"]["vectors"].get("config", {}).get("hnsw_config", None)
935
- sparse_idf = True if use_sparse_embeddings and hnsw_config else False
941
+ # deal with the Optional sparse_idf
942
+ hnsw_config = info_json["config"]["params"]["vectors"].get("config", {}).get("hnsw_config", None)
943
+ sparse_idf = True if use_sparse_embeddings and hnsw_config else False
936
944
 
937
- # recreate collection
938
- self._set_up_collection(
939
- collection_name=self.index,
940
- embedding_dim=info_json["config"]["params"]["vectors"]["size"],
941
- recreate_collection=True,
942
- similarity=info_json["config"]["params"]["vectors"]["distance"].lower(),
943
- use_sparse_embeddings=use_sparse_embeddings,
944
- sparse_idf=sparse_idf,
945
- on_disk=info_json["config"]["hnsw_config"]["on_disk"],
946
- payload_fields_to_index=info_json["payload_schema"],
947
- )
945
+ # recreate collection
946
+ self._set_up_collection(
947
+ collection_name=self.index,
948
+ embedding_dim=info_json["config"]["params"]["vectors"]["size"],
949
+ recreate_collection=True,
950
+ similarity=info_json["config"]["params"]["vectors"]["distance"].lower(),
951
+ use_sparse_embeddings=use_sparse_embeddings,
952
+ sparse_idf=sparse_idf,
953
+ on_disk=info_json["config"]["hnsw_config"]["on_disk"],
954
+ payload_fields_to_index=info_json["payload_schema"],
955
+ )
948
956
 
949
- else:
950
- try:
957
+ else:
951
958
  self._client.delete(
952
959
  collection_name=self.index,
953
960
  points_selector=rest.FilterSelector(
@@ -957,48 +964,48 @@ class QdrantDocumentStore:
957
964
  ),
958
965
  wait=self.wait_result_from_api,
959
966
  )
960
- except Exception as e:
961
- logger.warning(
962
- f"Error {e} when calling QdrantDocumentStore.delete_all_documents()",
963
- )
967
+ except Exception as e:
968
+ msg = f"Error when calling QdrantDocumentStore.delete_all_documents(): {e}"
969
+ raise QdrantStoreError(msg) from e
964
970
 
965
971
  async def delete_all_documents_async(self, recreate_index: bool = False) -> None:
966
972
  """
967
973
  Asynchronously deletes all documents from the document store.
968
974
 
969
975
  :param recreate_index: Whether to recreate the index after deleting all documents.
976
+ :raises QdrantStoreError: Deleting documents or recreating the collection fails.
970
977
  """
971
978
 
972
979
  await self._initialize_async_client()
973
980
  assert self._async_client is not None
974
981
 
975
- if recreate_index:
976
- # get current collection config as json
977
- collection_info = await self._async_client.get_collection(collection_name=self.index)
978
- info_json = collection_info.model_dump()
982
+ try:
983
+ if recreate_index:
984
+ # get current collection config as json
985
+ collection_info = await self._async_client.get_collection(collection_name=self.index)
986
+ info_json = collection_info.model_dump()
979
987
 
980
- # deal with the Optional use_sparse_embeddings
981
- sparse_vectors = info_json["config"]["params"]["sparse_vectors"]
982
- use_sparse_embeddings = True if sparse_vectors else False
988
+ # deal with the Optional use_sparse_embeddings
989
+ sparse_vectors = info_json["config"]["params"]["sparse_vectors"]
990
+ use_sparse_embeddings = True if sparse_vectors else False
983
991
 
984
- # deal with the Optional sparse_idf
985
- hnsw_config = info_json["config"]["params"]["vectors"].get("config", {}).get("hnsw_config", None)
986
- sparse_idf = True if use_sparse_embeddings and hnsw_config else False
992
+ # deal with the Optional sparse_idf
993
+ hnsw_config = info_json["config"]["params"]["vectors"].get("config", {}).get("hnsw_config", None)
994
+ sparse_idf = True if use_sparse_embeddings and hnsw_config else False
987
995
 
988
- # recreate collection
989
- await self._set_up_collection_async(
990
- collection_name=self.index,
991
- embedding_dim=info_json["config"]["params"]["vectors"]["size"],
992
- recreate_collection=True,
993
- similarity=info_json["config"]["params"]["vectors"]["distance"].lower(),
994
- use_sparse_embeddings=use_sparse_embeddings,
995
- sparse_idf=sparse_idf,
996
- on_disk=info_json["config"]["hnsw_config"]["on_disk"],
997
- payload_fields_to_index=info_json["payload_schema"],
998
- )
996
+ # recreate collection
997
+ await self._set_up_collection_async(
998
+ collection_name=self.index,
999
+ embedding_dim=info_json["config"]["params"]["vectors"]["size"],
1000
+ recreate_collection=True,
1001
+ similarity=info_json["config"]["params"]["vectors"]["distance"].lower(),
1002
+ use_sparse_embeddings=use_sparse_embeddings,
1003
+ sparse_idf=sparse_idf,
1004
+ on_disk=info_json["config"]["hnsw_config"]["on_disk"],
1005
+ payload_fields_to_index=info_json["payload_schema"],
1006
+ )
999
1007
 
1000
- else:
1001
- try:
1008
+ else:
1002
1009
  await self._async_client.delete(
1003
1010
  collection_name=self.index,
1004
1011
  points_selector=rest.FilterSelector(
@@ -1008,10 +1015,9 @@ class QdrantDocumentStore:
1008
1015
  ),
1009
1016
  wait=self.wait_result_from_api,
1010
1017
  )
1011
- except Exception as e:
1012
- logger.warning(
1013
- f"Error {e} when calling QdrantDocumentStore.delete_all_documents_async()",
1014
- )
1018
+ except Exception as e:
1019
+ msg = f"Error when calling QdrantDocumentStore.delete_all_documents_async(): {e}"
1020
+ raise QdrantStoreError(msg) from e
1015
1021
 
1016
1022
  def count_documents_by_filter(self, filters: dict[str, Any]) -> int:
1017
1023
  """
@@ -1021,6 +1027,7 @@ class QdrantDocumentStore:
1021
1027
  For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
1022
1028
 
1023
1029
  :returns: The number of documents that match the filters.
1030
+ :raises QdrantStoreError: Counting documents matching the filters fails.
1024
1031
  """
1025
1032
  self._initialize_client()
1026
1033
  assert self._client is not None
@@ -1032,9 +1039,9 @@ class QdrantDocumentStore:
1032
1039
  count_filter=qdrant_filter,
1033
1040
  )
1034
1041
  return response.count
1035
- except (UnexpectedResponse, ValueError) as e:
1036
- logger.warning(f"Error {e} when calling QdrantDocumentStore.count_documents_by_filter()")
1037
- return 0
1042
+ except Exception as e:
1043
+ msg = f"Error when calling QdrantDocumentStore.count_documents_by_filter(): {e}"
1044
+ raise QdrantStoreError(msg) from e
1038
1045
 
1039
1046
  async def count_documents_by_filter_async(self, filters: dict[str, Any]) -> int:
1040
1047
  """
@@ -1045,6 +1052,7 @@ class QdrantDocumentStore:
1045
1052
 
1046
1053
  :returns:
1047
1054
  The number of documents that match the filters.
1055
+ :raises QdrantStoreError: Counting documents matching the filters fails.
1048
1056
  """
1049
1057
  await self._initialize_async_client()
1050
1058
  assert self._async_client is not None
@@ -1056,9 +1064,9 @@ class QdrantDocumentStore:
1056
1064
  count_filter=qdrant_filter,
1057
1065
  )
1058
1066
  return response.count
1059
- except (UnexpectedResponse, ValueError) as e:
1060
- logger.warning(f"Error {e} when calling QdrantDocumentStore.count_documents_by_filter_async()")
1061
- return 0
1067
+ except Exception as e:
1068
+ msg = f"Error when calling QdrantDocumentStore.count_documents_by_filter_async(): {e}"
1069
+ raise QdrantStoreError(msg) from e
1062
1070
 
1063
1071
  def get_metadata_fields_info(self) -> dict[str, dict[str, str]]:
1064
1072
  """
@@ -1073,6 +1081,7 @@ class QdrantDocumentStore:
1073
1081
  ```python
1074
1082
  {"category": {"type": "keyword"}, "priority": {"type": "long"}}
1075
1083
  ```
1084
+ :raises QdrantStoreError: Retrieving or processing metadata field information fails.
1076
1085
  """
1077
1086
  self._initialize_client()
1078
1087
  assert self._client is not None
@@ -1098,9 +1107,9 @@ class QdrantDocumentStore:
1098
1107
  break
1099
1108
 
1100
1109
  return fields_info
1101
- except (UnexpectedResponse, ValueError) as e:
1102
- logger.warning(f"Error {e} when calling QdrantDocumentStore.get_metadata_fields_info()")
1103
- return {}
1110
+ except Exception as e:
1111
+ msg = f"Error when calling QdrantDocumentStore.get_metadata_fields_info(): {e}"
1112
+ raise QdrantStoreError(msg) from e
1104
1113
 
1105
1114
  async def get_metadata_fields_info_async(self) -> dict[str, dict[str, str]]:
1106
1115
  """
@@ -1115,6 +1124,7 @@ class QdrantDocumentStore:
1115
1124
  ```python
1116
1125
  {"category": {"type": "keyword"}, "priority": {"type": "long"}}
1117
1126
  ```
1127
+ :raises QdrantStoreError: Retrieving or processing metadata field information fails.
1118
1128
  """
1119
1129
  await self._initialize_async_client()
1120
1130
  assert self._async_client is not None
@@ -1140,9 +1150,9 @@ class QdrantDocumentStore:
1140
1150
  break
1141
1151
 
1142
1152
  return fields_info
1143
- except (UnexpectedResponse, ValueError) as e:
1144
- logger.warning(f"Error {e} when calling QdrantDocumentStore.get_metadata_fields_info_async()")
1145
- return {}
1153
+ except Exception as e:
1154
+ msg = f"Error when calling QdrantDocumentStore.get_metadata_fields_info_async(): {e}"
1155
+ raise QdrantStoreError(msg) from e
1146
1156
 
1147
1157
  def get_metadata_field_min_max(self, metadata_field: str) -> dict[str, Any]:
1148
1158
  """
@@ -1153,6 +1163,7 @@ class QdrantDocumentStore:
1153
1163
  :returns: A dictionary with the keys "min" and "max", where each value is the minimum or maximum value of the
1154
1164
  metadata field across all documents. Returns ``{"min": None, "max": None}`` if no documents have
1155
1165
  the field.
1166
+ :raises QdrantStoreError: Retrieving or computing the metadata bounds fails.
1156
1167
  """
1157
1168
  self._initialize_client()
1158
1169
  assert self._client is not None
@@ -1178,8 +1189,8 @@ class QdrantDocumentStore:
1178
1189
 
1179
1190
  return {"min": min_value, "max": max_value}
1180
1191
  except Exception as e:
1181
- logger.warning(f"Error {e} when calling QdrantDocumentStore.get_metadata_field_min_max()")
1182
- return {}
1192
+ msg = f"Error when calling QdrantDocumentStore.get_metadata_field_min_max(): {e}"
1193
+ raise QdrantStoreError(msg) from e
1183
1194
 
1184
1195
  async def get_metadata_field_min_max_async(self, metadata_field: str) -> dict[str, Any]:
1185
1196
  """
@@ -1190,6 +1201,7 @@ class QdrantDocumentStore:
1190
1201
  :returns: A dictionary with the keys "min" and "max", where each value is the minimum or maximum value of the
1191
1202
  metadata field across all documents. Returns ``{"min": None, "max": None}`` if no documents have
1192
1203
  the field.
1204
+ :raises QdrantStoreError: Retrieving or computing the metadata bounds fails.
1193
1205
  """
1194
1206
  await self._initialize_async_client()
1195
1207
  assert self._async_client is not None
@@ -1215,8 +1227,8 @@ class QdrantDocumentStore:
1215
1227
 
1216
1228
  return {"min": min_value, "max": max_value}
1217
1229
  except Exception as e:
1218
- logger.warning(f"Error {e} when calling QdrantDocumentStore.get_metadata_field_min_max_async()")
1219
- return {}
1230
+ msg = f"Error when calling QdrantDocumentStore.get_metadata_field_min_max_async(): {e}"
1231
+ raise QdrantStoreError(msg) from e
1220
1232
 
1221
1233
  def count_unique_metadata_by_filter(self, filters: dict[str, Any], metadata_fields: list[str]) -> dict[str, int]:
1222
1234
  """
@@ -1228,12 +1240,13 @@ class QdrantDocumentStore:
1228
1240
 
1229
1241
  :returns: A dictionary mapping each metadata field name to the count of its unique values among the filtered
1230
1242
  documents.
1243
+ :raises QdrantStoreError: Retrieving or counting unique metadata values fails.
1231
1244
  """
1232
1245
  self._initialize_client()
1233
1246
  assert self._client is not None
1234
1247
 
1235
1248
  qdrant_filter = convert_filters_to_qdrant(filters) if filters else None
1236
- unique_values_by_field: dict[str, set[Any]] = {field: set() for field in metadata_fields}
1249
+ unique_values_by_field: dict[str, set[tuple[type, Any]]] = {field: set() for field in metadata_fields}
1237
1250
 
1238
1251
  try:
1239
1252
  next_offset = None
@@ -1252,8 +1265,8 @@ class QdrantDocumentStore:
1252
1265
 
1253
1266
  return {field: len(unique_values_by_field[field]) for field in metadata_fields}
1254
1267
  except Exception as e:
1255
- logger.warning(f"Error {e} when calling QdrantDocumentStore.count_unique_metadata_by_filter()")
1256
- return dict.fromkeys(metadata_fields, 0)
1268
+ msg = f"Error when calling QdrantDocumentStore.count_unique_metadata_by_filter(): {e}"
1269
+ raise QdrantStoreError(msg) from e
1257
1270
 
1258
1271
  async def count_unique_metadata_by_filter_async(
1259
1272
  self, filters: dict[str, Any], metadata_fields: list[str]
@@ -1269,12 +1282,13 @@ class QdrantDocumentStore:
1269
1282
 
1270
1283
  :returns: A dictionary mapping each metadata field name to the count of its unique values among the filtered
1271
1284
  documents.
1285
+ :raises QdrantStoreError: Retrieving or counting unique metadata values fails.
1272
1286
  """
1273
1287
  await self._initialize_async_client()
1274
1288
  assert self._async_client is not None
1275
1289
 
1276
1290
  qdrant_filter = convert_filters_to_qdrant(filters) if filters else None
1277
- unique_values_by_field: dict[str, set[Any]] = {field: set() for field in metadata_fields}
1291
+ unique_values_by_field: dict[str, set[tuple[type, Any]]] = {field: set() for field in metadata_fields}
1278
1292
 
1279
1293
  try:
1280
1294
  next_offset = None
@@ -1293,8 +1307,8 @@ class QdrantDocumentStore:
1293
1307
 
1294
1308
  return {field: len(unique_values_by_field[field]) for field in metadata_fields}
1295
1309
  except Exception as e:
1296
- logger.warning(f"Error {e} when calling QdrantDocumentStore.count_unique_metadata_by_filter_async()")
1297
- return dict.fromkeys(metadata_fields, 0)
1310
+ msg = f"Error when calling QdrantDocumentStore.count_unique_metadata_by_filter_async(): {e}"
1311
+ raise QdrantStoreError(msg) from e
1298
1312
 
1299
1313
  def get_metadata_field_unique_values(
1300
1314
  self,
@@ -1307,7 +1321,7 @@ class QdrantDocumentStore:
1307
1321
  """
1308
1322
  Returns unique values for a metadata field, with optional filters, search term and pagination.
1309
1323
 
1310
- Unique values are ordered by first occurrence during scroll.
1324
+ Unique values are sorted by string representation, then by type name, before pagination is applied.
1311
1325
 
1312
1326
  **Note**: This operation can be expensive for metadata fields with many unique values, since all
1313
1327
  matching documents must be scrolled through to compute the total count.
@@ -1320,6 +1334,7 @@ class QdrantDocumentStore:
1320
1334
  For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
1321
1335
 
1322
1336
  :returns: A tuple containing (list of unique values, total count of unique matching values).
1337
+ :raises QdrantStoreError: Retrieving or processing unique metadata values fails.
1323
1338
  """
1324
1339
  self._initialize_client()
1325
1340
  assert self._client is not None
@@ -1344,11 +1359,12 @@ class QdrantDocumentStore:
1344
1359
  if self._check_stop_scrolling(next_offset):
1345
1360
  break
1346
1361
 
1362
+ unique_values.sort(key=lambda v: (str(v), type(v).__name__))
1347
1363
  total_count = len(unique_values)
1348
1364
  return unique_values[from_ : from_ + size], total_count
1349
1365
  except Exception as e:
1350
- logger.warning(f"Error {e} when calling QdrantDocumentStore.get_metadata_field_unique_values()")
1351
- return [], 0
1366
+ msg = f"Error when calling QdrantDocumentStore.get_metadata_field_unique_values(): {e}"
1367
+ raise QdrantStoreError(msg) from e
1352
1368
 
1353
1369
  async def get_metadata_field_unique_values_async(
1354
1370
  self,
@@ -1361,7 +1377,7 @@ class QdrantDocumentStore:
1361
1377
  """
1362
1378
  Asynchronously returns unique values for a metadata field, with optional filters, search term and pagination.
1363
1379
 
1364
- Unique values are ordered by first occurrence during scroll.
1380
+ Unique values are sorted by string representation, then by type name, before pagination is applied.
1365
1381
 
1366
1382
  **Note**: This operation can be expensive for metadata fields with many unique values, since all
1367
1383
  matching documents must be scrolled through to compute the total count.
@@ -1374,6 +1390,7 @@ class QdrantDocumentStore:
1374
1390
  For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
1375
1391
 
1376
1392
  :returns: A tuple containing (list of unique values, total count of unique matching values).
1393
+ :raises QdrantStoreError: Retrieving or processing unique metadata values fails.
1377
1394
  """
1378
1395
  await self._initialize_async_client()
1379
1396
  assert self._async_client is not None
@@ -1398,11 +1415,12 @@ class QdrantDocumentStore:
1398
1415
  if self._check_stop_scrolling(next_offset):
1399
1416
  break
1400
1417
 
1418
+ unique_values.sort(key=lambda v: (str(v), type(v).__name__))
1401
1419
  total_count = len(unique_values)
1402
1420
  return unique_values[from_ : from_ + size], total_count
1403
1421
  except Exception as e:
1404
- logger.warning(f"Error {e} when calling QdrantDocumentStore.get_metadata_field_unique_values_async()")
1405
- return [], 0
1422
+ msg = f"Error when calling QdrantDocumentStore.get_metadata_field_unique_values_async(): {e}"
1423
+ raise QdrantStoreError(msg) from e
1406
1424
 
1407
1425
  @classmethod
1408
1426
  def from_dict(cls, data: dict[str, Any]) -> "QdrantDocumentStore":
@@ -267,8 +267,19 @@ class TestQdrantDocumentStoreUnit:
267
267
  ]
268
268
  unique: dict = {"category": set(), "tags": set()}
269
269
  QdrantDocumentStore._process_records_count_unique(records, ["category", "tags"], unique)
270
- assert unique["category"] == {"A", "B"}
271
- assert unique["tags"] == {"['x']", "['y']"}
270
+ assert len(unique["category"]) == 2
271
+ assert len(unique["tags"]) == 2
272
+
273
+ def test_process_records_count_unique_does_not_merge_different_types(self):
274
+ """Values that are `==` across types (`1 == True`, `1 == 1.0`) must still count as distinct."""
275
+ records = [
276
+ SimpleNamespace(payload={"meta": {"value": 1}}),
277
+ SimpleNamespace(payload={"meta": {"value": True}}),
278
+ SimpleNamespace(payload={"meta": {"value": 1.0}}),
279
+ ]
280
+ unique: dict = {"value": set()}
281
+ QdrantDocumentStore._process_records_count_unique(records, ["value"], unique)
282
+ assert len(unique["value"]) == 3
272
283
 
273
284
  def test_process_records_unique_values_collects_all(self):
274
285
  records = [SimpleNamespace(payload={"meta": {"v": i}}) for i in range(10)]
@@ -366,17 +377,19 @@ class TestQdrantDocumentStoreUnit:
366
377
  document_store._query_by_sparse(query_sparse_embedding=sparse_embedding)
367
378
 
368
379
  @pytest.mark.parametrize(
369
- ("method_name", "args", "expected"),
380
+ ("method_name", "args"),
370
381
  [
371
- ("count_documents", (), 0),
372
- ("count_documents_by_filter", ({},), 0),
373
- ("get_metadata_fields_info", (), {}),
374
- ("get_metadata_field_min_max", ("score",), {}),
375
- ("count_unique_metadata_by_filter", ({}, ["category"]), {"category": 0}),
376
- ("get_metadata_field_unique_values", ("category",), ([], 0)),
382
+ ("count_documents", ()),
383
+ ("delete_all_documents", (False,)),
384
+ ("delete_all_documents", (True,)),
385
+ ("count_documents_by_filter", ({},)),
386
+ ("get_metadata_fields_info", ()),
387
+ ("get_metadata_field_min_max", ("score",)),
388
+ ("count_unique_metadata_by_filter", ({}, ["category"])),
389
+ ("get_metadata_field_unique_values", ("category",)),
377
390
  ],
378
391
  )
379
- def test_metadata_methods_swallow_client_errors(self, method_name, args, expected):
392
+ def test_operations_raise_on_client_errors(self, method_name, args):
380
393
  document_store = QdrantDocumentStore(location=":memory:")
381
394
  document_store._initialize_client()
382
395
  err = ValueError("boom")
@@ -384,8 +397,11 @@ class TestQdrantDocumentStoreUnit:
384
397
  patch.object(document_store._client, "count", side_effect=err),
385
398
  patch.object(document_store._client, "scroll", side_effect=err),
386
399
  patch.object(document_store._client, "get_collection", side_effect=err),
400
+ patch.object(document_store._client, "delete", side_effect=err),
387
401
  ):
388
- assert getattr(document_store, method_name)(*args) == expected
402
+ with pytest.raises(QdrantStoreError) as exc_info:
403
+ getattr(document_store, method_name)(*args)
404
+ assert exc_info.value.__cause__ is err
389
405
 
390
406
  def test_close(self):
391
407
  document_store = QdrantDocumentStore(location=":memory:")
@@ -611,65 +627,3 @@ class TestQdrantDocumentStore(
611
627
  assert len(updated_docs) == 1
612
628
  assert updated_docs[0].embedding is not None
613
629
  assert len(updated_docs[0].embedding) == 768
614
-
615
- def test_get_metadata_field_unique_values_pagination(self, document_store: QdrantDocumentStore):
616
- """Test getting unique metadata field values with pagination, total reflects the full unpaginated count."""
617
- docs = [Document(content=f"Doc {i}", meta={"value": i % 5}) for i in range(10)]
618
- document_store.write_documents(docs)
619
-
620
- # Get first 2 unique values
621
- values_page_1, total_1 = document_store.get_metadata_field_unique_values("value", from_=0, size=2)
622
- assert len(values_page_1) == 2
623
- assert total_1 == 5
624
-
625
- # Get next 2 unique values
626
- values_page_2, total_2 = document_store.get_metadata_field_unique_values("value", from_=2, size=2)
627
- assert len(values_page_2) == 2
628
- assert total_2 == 5
629
-
630
- # Values should not overlap
631
- assert set(values_page_1) != set(values_page_2)
632
-
633
- def test_get_metadata_field_unique_values_with_filter(self, document_store: QdrantDocumentStore):
634
- """Test getting unique metadata field values with filtering."""
635
- docs = [
636
- Document(content="Doc 1", meta={"category": "A", "status": "active"}),
637
- Document(content="Doc 2", meta={"category": "B", "status": "active"}),
638
- Document(content="Doc 3", meta={"category": "A", "status": "inactive"}),
639
- ]
640
- document_store.write_documents(docs)
641
-
642
- values, total = document_store.get_metadata_field_unique_values(
643
- "category", filters={"field": "meta.status", "operator": "==", "value": "active"}
644
- )
645
- assert set(values) == {"A", "B"}
646
- assert total == 2
647
-
648
- def test_get_metadata_field_unique_values_with_meta_prefix(self, document_store: QdrantDocumentStore):
649
- """Test that a 'meta.'-prefixed field name is normalized before lookup."""
650
- docs = [
651
- Document(content="Doc 1", meta={"category": "A"}),
652
- Document(content="Doc 2", meta={"category": "B"}),
653
- ]
654
- document_store.write_documents(docs)
655
-
656
- values, total = document_store.get_metadata_field_unique_values("meta.category")
657
- assert set(values) == {"A", "B"}
658
- assert total == 2
659
-
660
- def test_get_metadata_field_unique_values_with_search_term(self, document_store: QdrantDocumentStore):
661
- """Test that search_term filters unique values by a case-insensitive substring match on the field value."""
662
- docs = [
663
- Document(content="Doc 1", meta={"category": "Apple"}),
664
- Document(content="Doc 2", meta={"category": "Banana"}),
665
- Document(content="Doc 3", meta={"category": "Apricot"}),
666
- ]
667
- document_store.write_documents(docs)
668
-
669
- values, total = document_store.get_metadata_field_unique_values("category", search_term="ap")
670
- assert set(values) == {"Apple", "Apricot"}
671
- assert total == 2
672
-
673
- values, total = document_store.get_metadata_field_unique_values("category", search_term="nonexistent")
674
- assert values == []
675
- assert total == 0
@@ -141,17 +141,19 @@ class TestQdrantDocumentStoreAsyncUnit:
141
141
  await document_store.delete_documents_async(["doc-1"])
142
142
 
143
143
  @pytest.mark.parametrize(
144
- ("method_name", "args", "expected"),
144
+ ("method_name", "args"),
145
145
  [
146
- ("count_documents_async", (), 0),
147
- ("count_documents_by_filter_async", ({},), 0),
148
- ("get_metadata_fields_info_async", (), {}),
149
- ("get_metadata_field_min_max_async", ("score",), {}),
150
- ("count_unique_metadata_by_filter_async", ({}, ["category"]), {"category": 0}),
151
- ("get_metadata_field_unique_values_async", ("category",), ([], 0)),
146
+ ("count_documents_async", ()),
147
+ ("delete_all_documents_async", (False,)),
148
+ ("delete_all_documents_async", (True,)),
149
+ ("count_documents_by_filter_async", ({},)),
150
+ ("get_metadata_fields_info_async", ()),
151
+ ("get_metadata_field_min_max_async", ("score",)),
152
+ ("count_unique_metadata_by_filter_async", ({}, ["category"])),
153
+ ("get_metadata_field_unique_values_async", ("category",)),
152
154
  ],
153
155
  )
154
- async def test_metadata_methods_async_absorb_client_errors(self, method_name, args, expected):
156
+ async def test_operations_async_raise_on_client_errors(self, method_name, args):
155
157
  document_store = QdrantDocumentStore(location=":memory:")
156
158
  await document_store._initialize_async_client()
157
159
  err = ValueError("boom")
@@ -159,8 +161,11 @@ class TestQdrantDocumentStoreAsyncUnit:
159
161
  patch.object(document_store._async_client, "count", side_effect=err),
160
162
  patch.object(document_store._async_client, "scroll", side_effect=err),
161
163
  patch.object(document_store._async_client, "get_collection", side_effect=err),
164
+ patch.object(document_store._async_client, "delete", side_effect=err),
162
165
  ):
163
- assert await getattr(document_store, method_name)(*args) == expected
166
+ with pytest.raises(QdrantStoreError) as exc_info:
167
+ await getattr(document_store, method_name)(*args)
168
+ assert exc_info.value.__cause__ is err
164
169
 
165
170
  async def test_close_async(self):
166
171
  document_store = QdrantDocumentStore(location=":memory:")
@@ -342,34 +347,3 @@ class TestQdrantDocumentStoreAsync(
342
347
  assert len(updated_docs) == 1
343
348
  assert updated_docs[0].embedding is not None
344
349
  assert len(updated_docs[0].embedding) == 768
345
-
346
- async def test_get_metadata_field_unique_values_with_meta_prefix_async(self, document_store: QdrantDocumentStore):
347
- """Test that a 'meta.'-prefixed field name is normalized before lookup."""
348
- docs = [
349
- Document(content="Doc 1", meta={"category": "A"}),
350
- Document(content="Doc 2", meta={"category": "B"}),
351
- ]
352
- await document_store.write_documents_async(docs)
353
-
354
- values, total = await document_store.get_metadata_field_unique_values_async("meta.category")
355
- assert set(values) == {"A", "B"}
356
- assert total == 2
357
-
358
- async def test_get_metadata_field_unique_values_with_search_term_async(self, document_store: QdrantDocumentStore):
359
- """Test that search_term filters unique values by a case-insensitive substring match on the field value."""
360
- docs = [
361
- Document(content="Doc 1", meta={"category": "Apple"}),
362
- Document(content="Doc 2", meta={"category": "Banana"}),
363
- Document(content="Doc 3", meta={"category": "Apricot"}),
364
- ]
365
- await document_store.write_documents_async(docs)
366
-
367
- values, total = await document_store.get_metadata_field_unique_values_async("category", search_term="ap")
368
- assert set(values) == {"Apple", "Apricot"}
369
- assert total == 2
370
-
371
- values, total = await document_store.get_metadata_field_unique_values_async(
372
- "category", search_term="nonexistent"
373
- )
374
- assert values == []
375
- assert total == 0