qdrant-haystack 13.1.0__tar.gz → 13.1.1__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.
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/CHANGELOG.md +7 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/PKG-INFO +1 -1
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/qdrant/document_store.py +48 -48
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/qdrant/filters.py +15 -1
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_document_store.py +47 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_document_store_async.py +49 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_filters.py +76 -16
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/.gitignore +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/LICENSE.txt +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/README.md +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/examples/embedding_retrieval.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/pydoc/config_docusaurus.yml +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/pyproject.toml +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/components/retrievers/py.typed +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/components/retrievers/qdrant/__init__.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/components/retrievers/qdrant/retriever.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/py.typed +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/qdrant/__init__.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/qdrant/converters.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/qdrant/migrate_to_sparse.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/__init__.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/conftest.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_converters.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_dict_converters.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_embedding_retriever.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_hybrid_retriever.py +0 -0
- {qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/tests/test_sparse_embedding_retriever.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: qdrant-haystack
|
|
3
|
-
Version: 13.1.
|
|
3
|
+
Version: 13.1.1
|
|
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
|
|
@@ -467,7 +467,7 @@ class QdrantDocumentStore:
|
|
|
467
467
|
wait=self.wait_result_from_api,
|
|
468
468
|
)
|
|
469
469
|
|
|
470
|
-
progress_bar.update(
|
|
470
|
+
progress_bar.update(len(document_batch))
|
|
471
471
|
return len(document_objects)
|
|
472
472
|
|
|
473
473
|
async def write_documents_async(
|
|
@@ -522,7 +522,7 @@ class QdrantDocumentStore:
|
|
|
522
522
|
wait=self.wait_result_from_api,
|
|
523
523
|
)
|
|
524
524
|
|
|
525
|
-
progress_bar.update(
|
|
525
|
+
progress_bar.update(len(document_batch))
|
|
526
526
|
return len(document_objects)
|
|
527
527
|
|
|
528
528
|
def delete_documents(self, document_ids: list[str]) -> None:
|
|
@@ -694,8 +694,8 @@ class QdrantDocumentStore:
|
|
|
694
694
|
"""
|
|
695
695
|
Build field name -> {type: ...} dict from Qdrant payload_schema.
|
|
696
696
|
|
|
697
|
-
|
|
698
|
-
Returns empty dict when schema has no metadata field info.
|
|
697
|
+
Covers only the indexed metadata fields (e.g. meta.category); unindexed fields
|
|
698
|
+
are added by scrolling. Returns empty dict when schema has no metadata field info.
|
|
699
699
|
"""
|
|
700
700
|
fields_info: dict[str, dict[str, str]] = {}
|
|
701
701
|
for field_name, field_config in payload_schema.items():
|
|
@@ -1082,9 +1082,9 @@ class QdrantDocumentStore:
|
|
|
1082
1082
|
"""
|
|
1083
1083
|
Returns the information about the metadata fields in the collection.
|
|
1084
1084
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
payload["meta"].
|
|
1085
|
+
Indexed metadata fields take their type from the collection's payload schema.
|
|
1086
|
+
Qdrant has no schema for unindexed metadata, so this method also scrolls
|
|
1087
|
+
through documents to infer the type of every other field from payload["meta"].
|
|
1088
1088
|
|
|
1089
1089
|
:returns:
|
|
1090
1090
|
A dictionary mapping field names to their type information e.g.:
|
|
@@ -1101,20 +1101,21 @@ class QdrantDocumentStore:
|
|
|
1101
1101
|
payload_schema = collection_info.payload_schema or {}
|
|
1102
1102
|
fields_info = self._metadata_fields_info_from_schema(payload_schema)
|
|
1103
1103
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1104
|
+
# Indexed fields only cover part of the metadata, so scroll even when the schema
|
|
1105
|
+
# has entries. Fields already typed by the schema keep their indexed type.
|
|
1106
|
+
next_offset = None
|
|
1107
|
+
while True:
|
|
1108
|
+
records, next_offset = self._client.scroll(
|
|
1109
|
+
collection_name=self.index,
|
|
1110
|
+
scroll_filter=None,
|
|
1111
|
+
limit=self.scroll_size,
|
|
1112
|
+
offset=next_offset,
|
|
1113
|
+
with_payload=True,
|
|
1114
|
+
with_vectors=False,
|
|
1115
|
+
)
|
|
1116
|
+
self._process_records_fields_info(records, fields_info)
|
|
1117
|
+
if self._check_stop_scrolling(next_offset):
|
|
1118
|
+
break
|
|
1118
1119
|
|
|
1119
1120
|
return fields_info
|
|
1120
1121
|
except Exception as e:
|
|
@@ -1125,9 +1126,9 @@ class QdrantDocumentStore:
|
|
|
1125
1126
|
"""
|
|
1126
1127
|
Asynchronously returns the information about the metadata fields in the collection.
|
|
1127
1128
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
payload["meta"].
|
|
1129
|
+
Indexed metadata fields take their type from the collection's payload schema.
|
|
1130
|
+
Qdrant has no schema for unindexed metadata, so this method also scrolls
|
|
1131
|
+
through documents to infer the type of every other field from payload["meta"].
|
|
1131
1132
|
|
|
1132
1133
|
:returns:
|
|
1133
1134
|
A dictionary mapping field names to their type information e.g.:
|
|
@@ -1144,20 +1145,21 @@ class QdrantDocumentStore:
|
|
|
1144
1145
|
payload_schema = collection_info.payload_schema or {}
|
|
1145
1146
|
fields_info = self._metadata_fields_info_from_schema(payload_schema)
|
|
1146
1147
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1148
|
+
# Indexed fields only cover part of the metadata, so scroll even when the schema
|
|
1149
|
+
# has entries. Fields already typed by the schema keep their indexed type.
|
|
1150
|
+
next_offset = None
|
|
1151
|
+
while True:
|
|
1152
|
+
records, next_offset = await self._async_client.scroll(
|
|
1153
|
+
collection_name=self.index,
|
|
1154
|
+
scroll_filter=None,
|
|
1155
|
+
limit=self.scroll_size,
|
|
1156
|
+
offset=next_offset,
|
|
1157
|
+
with_payload=True,
|
|
1158
|
+
with_vectors=False,
|
|
1159
|
+
)
|
|
1160
|
+
self._process_records_fields_info(records, fields_info)
|
|
1161
|
+
if self._check_stop_scrolling(next_offset):
|
|
1162
|
+
break
|
|
1161
1163
|
|
|
1162
1164
|
return fields_info
|
|
1163
1165
|
except Exception as e:
|
|
@@ -1664,7 +1666,7 @@ class QdrantDocumentStore:
|
|
|
1664
1666
|
with_vectors=return_embedding,
|
|
1665
1667
|
score_threshold=score_threshold,
|
|
1666
1668
|
).groups
|
|
1667
|
-
return self._process_group_results(groups)
|
|
1669
|
+
return self._process_group_results(groups, scale_score=scale_score)
|
|
1668
1670
|
else:
|
|
1669
1671
|
points = self._client.query_points(
|
|
1670
1672
|
collection_name=self.index,
|
|
@@ -1726,7 +1728,7 @@ class QdrantDocumentStore:
|
|
|
1726
1728
|
with_vectors=return_embedding,
|
|
1727
1729
|
score_threshold=score_threshold,
|
|
1728
1730
|
).groups
|
|
1729
|
-
return self._process_group_results(groups)
|
|
1731
|
+
return self._process_group_results(groups, scale_score=scale_score)
|
|
1730
1732
|
|
|
1731
1733
|
else:
|
|
1732
1734
|
points = self._client.query_points(
|
|
@@ -1924,7 +1926,7 @@ class QdrantDocumentStore:
|
|
|
1924
1926
|
score_threshold=score_threshold,
|
|
1925
1927
|
)
|
|
1926
1928
|
groups = response.groups
|
|
1927
|
-
return self._process_group_results(groups)
|
|
1929
|
+
return self._process_group_results(groups, scale_score=scale_score)
|
|
1928
1930
|
else:
|
|
1929
1931
|
query_response = await self._async_client.query_points(
|
|
1930
1932
|
collection_name=self.index,
|
|
@@ -1988,7 +1990,7 @@ class QdrantDocumentStore:
|
|
|
1988
1990
|
score_threshold=score_threshold,
|
|
1989
1991
|
)
|
|
1990
1992
|
groups = response.groups
|
|
1991
|
-
return self._process_group_results(groups)
|
|
1993
|
+
return self._process_group_results(groups, scale_score=scale_score)
|
|
1992
1994
|
else:
|
|
1993
1995
|
query_response = await self._async_client.query_points(
|
|
1994
1996
|
collection_name=self.index,
|
|
@@ -2579,7 +2581,7 @@ class QdrantDocumentStore:
|
|
|
2579
2581
|
|
|
2580
2582
|
return documents
|
|
2581
2583
|
|
|
2582
|
-
def _process_group_results(self, groups: list[rest.PointGroup]) -> list[Document]:
|
|
2584
|
+
def _process_group_results(self, groups: list[rest.PointGroup], scale_score: bool = False) -> list[Document]:
|
|
2583
2585
|
"""
|
|
2584
2586
|
Processes grouped query results from Qdrant.
|
|
2585
2587
|
|
|
@@ -2587,11 +2589,9 @@ class QdrantDocumentStore:
|
|
|
2587
2589
|
if not groups:
|
|
2588
2590
|
return []
|
|
2589
2591
|
|
|
2590
|
-
return
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
for point in group.hits
|
|
2594
|
-
]
|
|
2592
|
+
return self._process_query_point_results(
|
|
2593
|
+
[point for group in groups for point in group.hits], scale_score=scale_score
|
|
2594
|
+
)
|
|
2595
2595
|
|
|
2596
2596
|
def _validate_collection_compatibility(
|
|
2597
2597
|
self,
|
|
@@ -79,7 +79,9 @@ def _process_logical_operator(item: dict[str, Any]) -> models.Condition | None:
|
|
|
79
79
|
elif operator == "OR":
|
|
80
80
|
return models.Filter(should=nested_conditions)
|
|
81
81
|
elif operator == "NOT":
|
|
82
|
-
|
|
82
|
+
# Haystack's NOT negates the conjunction of its conditions. `must_not` on its own excludes points
|
|
83
|
+
# that match any condition, which would negate the disjunction instead.
|
|
84
|
+
return models.Filter(must_not=[models.Filter(must=nested_conditions)])
|
|
83
85
|
|
|
84
86
|
return None
|
|
85
87
|
|
|
@@ -134,7 +136,19 @@ def _build_comparison_condition(operator: str, key: str, value: Any) -> models.C
|
|
|
134
136
|
return builder(key, value)
|
|
135
137
|
|
|
136
138
|
|
|
139
|
+
# Qdrant compares ranges as float64, so ints beyond 2**53 can't be matched exactly with a range.
|
|
140
|
+
_MAX_EXACT_FLOAT_INT = 2**53
|
|
141
|
+
|
|
142
|
+
|
|
137
143
|
def _build_eq_condition(key: str, value: models.ValueVariants) -> models.Condition:
|
|
144
|
+
# Qdrant ranges are compared as float64, so a huge int can't be matched exactly: keep MatchValue for it.
|
|
145
|
+
if (
|
|
146
|
+
isinstance(value, (int, float))
|
|
147
|
+
and not isinstance(value, bool)
|
|
148
|
+
and not (isinstance(value, int) and abs(value) > _MAX_EXACT_FLOAT_INT)
|
|
149
|
+
):
|
|
150
|
+
# MatchValue rejects floats and doesn't match float payloads for int values; a closed range does both.
|
|
151
|
+
return models.FieldCondition(key=key, range=models.Range(gte=value, lte=value))
|
|
138
152
|
return models.FieldCondition(key=key, match=models.MatchValue(value=value))
|
|
139
153
|
|
|
140
154
|
|
|
@@ -429,6 +429,23 @@ class TestQdrantDocumentStoreUnit:
|
|
|
429
429
|
getattr(document_store, method_name)(*args)
|
|
430
430
|
assert exc_info.value.__cause__ is err
|
|
431
431
|
|
|
432
|
+
def test_get_metadata_fields_info_includes_unindexed_fields_when_some_are_indexed(self):
|
|
433
|
+
document_store = QdrantDocumentStore(location=":memory:")
|
|
434
|
+
schema = {"meta.category": rest.PayloadIndexInfo(data_type=rest.PayloadSchemaType.TEXT, points=1)}
|
|
435
|
+
client = MagicMock()
|
|
436
|
+
client.get_collection.return_value = SimpleNamespace(payload_schema=schema)
|
|
437
|
+
client.scroll.return_value = (
|
|
438
|
+
[SimpleNamespace(payload={"meta": {"category": "news", "priority": 1}})],
|
|
439
|
+
None,
|
|
440
|
+
)
|
|
441
|
+
document_store._client = client
|
|
442
|
+
|
|
443
|
+
fields = document_store.get_metadata_fields_info()
|
|
444
|
+
|
|
445
|
+
# "news" would be inferred as "keyword", so "text" proves the indexed type wins.
|
|
446
|
+
assert fields == {"category": {"type": "text"}, "priority": {"type": "long"}}
|
|
447
|
+
client.scroll.assert_called_once()
|
|
448
|
+
|
|
432
449
|
def test_close(self):
|
|
433
450
|
document_store = QdrantDocumentStore(location=":memory:")
|
|
434
451
|
mock_client = MagicMock()
|
|
@@ -452,6 +469,36 @@ class TestQdrantDocumentStoreUnit:
|
|
|
452
469
|
|
|
453
470
|
assert document_store._client is None
|
|
454
471
|
|
|
472
|
+
@pytest.mark.parametrize(
|
|
473
|
+
"method_name, query_kwargs",
|
|
474
|
+
[
|
|
475
|
+
("_query_by_embedding", {"query_embedding": [0.1] * 768}),
|
|
476
|
+
("_query_by_sparse", {"query_sparse_embedding": SparseEmbedding(indices=[0], values=[0.1])}),
|
|
477
|
+
],
|
|
478
|
+
)
|
|
479
|
+
@pytest.mark.parametrize("scale_score, expected_score", [(True, 0.75), (False, 0.5)])
|
|
480
|
+
def test_query_with_group_by_scale_score(self, method_name, query_kwargs, scale_score, expected_score):
|
|
481
|
+
document_store = QdrantDocumentStore(location=":memory:", similarity="cosine", use_sparse_embeddings=True)
|
|
482
|
+
mock_client = MagicMock()
|
|
483
|
+
point = rest.ScoredPoint(id=1, version=0, score=0.5, payload={"id": "1", "content": "doc"})
|
|
484
|
+
mock_client.query_points_groups.return_value.groups = [rest.PointGroup(id="a", hits=[point])]
|
|
485
|
+
document_store._client = mock_client
|
|
486
|
+
|
|
487
|
+
result = getattr(document_store, method_name)(**query_kwargs, group_by="meta.group", scale_score=scale_score)
|
|
488
|
+
|
|
489
|
+
assert [doc.score for doc in result] == [expected_score]
|
|
490
|
+
|
|
491
|
+
def test_write_documents_progress_bar_counts_documents(self):
|
|
492
|
+
document_store = QdrantDocumentStore(location=":memory:", write_batch_size=2)
|
|
493
|
+
document_store._client = MagicMock()
|
|
494
|
+
with patch("haystack_integrations.document_stores.qdrant.document_store.tqdm") as mock_tqdm:
|
|
495
|
+
document_store.write_documents(
|
|
496
|
+
[Document(content=str(i)) for i in range(3)], policy=DuplicatePolicy.OVERWRITE
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
progress_bar = mock_tqdm.return_value.__enter__.return_value
|
|
500
|
+
assert [c.args[0] for c in progress_bar.update.call_args_list] == [2, 1]
|
|
501
|
+
|
|
455
502
|
|
|
456
503
|
@pytest.mark.integration
|
|
457
504
|
class TestQdrantDocumentStore(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
1
2
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -184,6 +185,22 @@ class TestQdrantDocumentStoreAsyncUnit:
|
|
|
184
185
|
await getattr(document_store, method_name)(*args)
|
|
185
186
|
assert exc_info.value.__cause__ is err
|
|
186
187
|
|
|
188
|
+
async def test_get_metadata_fields_info_async_includes_unindexed_fields_when_some_are_indexed(self):
|
|
189
|
+
document_store = QdrantDocumentStore(location=":memory:")
|
|
190
|
+
schema = {"meta.category": rest.PayloadIndexInfo(data_type=rest.PayloadSchemaType.TEXT, points=1)}
|
|
191
|
+
client = AsyncMock()
|
|
192
|
+
client.get_collection.return_value = SimpleNamespace(payload_schema=schema)
|
|
193
|
+
client.scroll.return_value = (
|
|
194
|
+
[SimpleNamespace(payload={"meta": {"category": "news", "priority": 1}})],
|
|
195
|
+
None,
|
|
196
|
+
)
|
|
197
|
+
document_store._async_client = client
|
|
198
|
+
|
|
199
|
+
fields = await document_store.get_metadata_fields_info_async()
|
|
200
|
+
|
|
201
|
+
assert fields == {"category": {"type": "text"}, "priority": {"type": "long"}}
|
|
202
|
+
client.scroll.assert_awaited_once()
|
|
203
|
+
|
|
187
204
|
async def test_close_async(self):
|
|
188
205
|
document_store = QdrantDocumentStore(location=":memory:")
|
|
189
206
|
mock_client = AsyncMock()
|
|
@@ -207,6 +224,38 @@ class TestQdrantDocumentStoreAsyncUnit:
|
|
|
207
224
|
|
|
208
225
|
assert document_store._async_client is None
|
|
209
226
|
|
|
227
|
+
@pytest.mark.parametrize(
|
|
228
|
+
"method_name, query_kwargs",
|
|
229
|
+
[
|
|
230
|
+
("_query_by_embedding_async", {"query_embedding": [0.1] * 768}),
|
|
231
|
+
("_query_by_sparse_async", {"query_sparse_embedding": SparseEmbedding(indices=[0], values=[0.1])}),
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
@pytest.mark.parametrize("scale_score, expected_score", [(True, 0.75), (False, 0.5)])
|
|
235
|
+
async def test_query_with_group_by_scale_score_async(self, method_name, query_kwargs, scale_score, expected_score):
|
|
236
|
+
document_store = QdrantDocumentStore(location=":memory:", similarity="cosine", use_sparse_embeddings=True)
|
|
237
|
+
mock_client = AsyncMock()
|
|
238
|
+
point = rest.ScoredPoint(id=1, version=0, score=0.5, payload={"id": "1", "content": "doc"})
|
|
239
|
+
mock_client.query_points_groups.return_value = rest.GroupsResult(groups=[rest.PointGroup(id="a", hits=[point])])
|
|
240
|
+
document_store._async_client = mock_client
|
|
241
|
+
|
|
242
|
+
result = await getattr(document_store, method_name)(
|
|
243
|
+
**query_kwargs, group_by="meta.group", scale_score=scale_score
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
assert [doc.score for doc in result] == [expected_score]
|
|
247
|
+
|
|
248
|
+
async def test_write_documents_async_progress_bar_counts_documents(self):
|
|
249
|
+
document_store = QdrantDocumentStore(location=":memory:", write_batch_size=2)
|
|
250
|
+
document_store._async_client = AsyncMock()
|
|
251
|
+
with patch("haystack_integrations.document_stores.qdrant.document_store.tqdm") as mock_tqdm:
|
|
252
|
+
await document_store.write_documents_async(
|
|
253
|
+
[Document(content=str(i)) for i in range(3)], policy=DuplicatePolicy.OVERWRITE
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
progress_bar = mock_tqdm.return_value.__enter__.return_value
|
|
257
|
+
assert [c.args[0] for c in progress_bar.update.call_args_list] == [2, 1]
|
|
258
|
+
|
|
210
259
|
|
|
211
260
|
@pytest.mark.integration
|
|
212
261
|
@pytest.mark.asyncio
|
|
@@ -73,6 +73,46 @@ class TestConvertFiltersToQdrantUnit:
|
|
|
73
73
|
assert isinstance(qdrant_filter, models.Filter)
|
|
74
74
|
assert len(qdrant_filter.must) == 2
|
|
75
75
|
|
|
76
|
+
@pytest.mark.parametrize("value", [4, 4.5, -3, 2**53])
|
|
77
|
+
def test_eq_numeric_uses_closed_range(self, value):
|
|
78
|
+
qdrant_filter = convert_filters_to_qdrant({"operator": "==", "field": "meta.x", "value": value})
|
|
79
|
+
condition = qdrant_filter.must[0]
|
|
80
|
+
assert condition.match is None
|
|
81
|
+
assert condition.range == models.Range(gte=value, lte=value)
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize("value", [True, False, "a", 2**53 + 1, -(2**60)])
|
|
84
|
+
def test_eq_non_numeric_or_unsafe_int_keeps_match_value(self, value):
|
|
85
|
+
qdrant_filter = convert_filters_to_qdrant({"operator": "==", "field": "meta.x", "value": value})
|
|
86
|
+
condition = qdrant_filter.must[0]
|
|
87
|
+
assert condition.range is None
|
|
88
|
+
assert condition.match == models.MatchValue(value=value)
|
|
89
|
+
assert type(condition.match.value) is type(value)
|
|
90
|
+
|
|
91
|
+
def test_in_and_not_in_mixed_numerics(self):
|
|
92
|
+
values = [1, 2.5, "a", True]
|
|
93
|
+
in_filter = convert_filters_to_qdrant({"operator": "in", "field": "meta.x", "value": values})
|
|
94
|
+
nin_filter = convert_filters_to_qdrant({"operator": "not in", "field": "meta.x", "value": values})
|
|
95
|
+
for conditions in (in_filter.should, nin_filter.must_not):
|
|
96
|
+
assert [(c.range, c.match) for c in conditions] == [
|
|
97
|
+
(models.Range(gte=1, lte=1), None),
|
|
98
|
+
(models.Range(gte=2.5, lte=2.5), None),
|
|
99
|
+
(None, models.MatchValue(value="a")),
|
|
100
|
+
(None, models.MatchValue(value=True)),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def test_ne_float(self):
|
|
104
|
+
qdrant_filter = convert_filters_to_qdrant({"operator": "!=", "field": "meta.x", "value": 4.5})
|
|
105
|
+
assert qdrant_filter.must_not[0].range == models.Range(gte=4.5, lte=4.5)
|
|
106
|
+
|
|
107
|
+
def test_not_negates_the_conjunction_of_its_conditions(self):
|
|
108
|
+
a = {"operator": "==", "field": "meta.a", "value": 1}
|
|
109
|
+
b = {"operator": "==", "field": "meta.b", "value": 2}
|
|
110
|
+
qdrant_filter = convert_filters_to_qdrant({"operator": "NOT", "conditions": [a, b]})
|
|
111
|
+
# `must_not` alone would exclude points matching either condition
|
|
112
|
+
assert qdrant_filter == models.Filter(
|
|
113
|
+
must_not=[models.Filter(must=[convert_filters_to_qdrant(a).must[0], convert_filters_to_qdrant(b).must[0]])]
|
|
114
|
+
)
|
|
115
|
+
|
|
76
116
|
|
|
77
117
|
def test_is_datetime_string():
|
|
78
118
|
assert is_datetime_string("2024-01-01T00:00:00") is True
|
|
@@ -165,22 +205,6 @@ class TestQdrantFilters(FilterDocumentsTest):
|
|
|
165
205
|
# Check that the sets are equal, meaning the content and IDs match regardless of order
|
|
166
206
|
assert {doc.id for doc in received} == {doc.id for doc in expected}
|
|
167
207
|
|
|
168
|
-
def test_not_operator(self, document_store, filterable_docs):
|
|
169
|
-
document_store.write_documents(filterable_docs)
|
|
170
|
-
result = document_store.filter_documents(
|
|
171
|
-
filters={
|
|
172
|
-
"operator": "NOT",
|
|
173
|
-
"conditions": [
|
|
174
|
-
{"field": "meta.number", "operator": "==", "value": 100},
|
|
175
|
-
{"field": "meta.name", "operator": "==", "value": "name_0"},
|
|
176
|
-
],
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
self.assert_documents_are_equal(
|
|
180
|
-
result,
|
|
181
|
-
[d for d in filterable_docs if (d.meta.get("number") != 100 and d.meta.get("name") != "name_0")],
|
|
182
|
-
)
|
|
183
|
-
|
|
184
208
|
def test_filter_criteria(self, document_store):
|
|
185
209
|
documents = [
|
|
186
210
|
Document(
|
|
@@ -329,3 +353,39 @@ class TestQdrantFilters(FilterDocumentsTest):
|
|
|
329
353
|
|
|
330
354
|
@pytest.mark.skip(reason="Cannot distinguish errors yet")
|
|
331
355
|
def test_missing_top_level_operator_key(self, document_store, filterable_docs): ...
|
|
356
|
+
|
|
357
|
+
@pytest.mark.parametrize(
|
|
358
|
+
("operator", "value", "expected"),
|
|
359
|
+
[
|
|
360
|
+
("==", 4, [4, 4.0]),
|
|
361
|
+
("==", 4.5, [4.5]),
|
|
362
|
+
("==", 4.0, [4, 4.0]),
|
|
363
|
+
("!=", 4.5, [3, 4, 4.0]),
|
|
364
|
+
("!=", 4, [3, 4.5]),
|
|
365
|
+
("in", [3, 4.5], [3, 4.5]),
|
|
366
|
+
("in", [4.0, 9], [4, 4.0]),
|
|
367
|
+
("not in", [3, 4.5], [4, 4.0]),
|
|
368
|
+
("not in", [4, 9.5], [3, 4.5]),
|
|
369
|
+
],
|
|
370
|
+
)
|
|
371
|
+
def test_numeric_filters_match_int_and_float_payloads(self, document_store, operator, value, expected):
|
|
372
|
+
ratings = [3, 4, 4.0, 4.5]
|
|
373
|
+
document_store.write_documents(
|
|
374
|
+
[Document(content=str(r), meta={"rating": r}, embedding=[0.1] * 768) for r in ratings]
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
result = document_store.filter_documents({"field": "meta.rating", "operator": operator, "value": value})
|
|
378
|
+
|
|
379
|
+
assert sorted(d.meta["rating"] for d in result) == sorted(expected)
|
|
380
|
+
|
|
381
|
+
def test_eq_bool_does_not_match_numeric_payload(self, document_store):
|
|
382
|
+
document_store.write_documents(
|
|
383
|
+
[
|
|
384
|
+
Document(content="int", meta={"flag": 1}, embedding=[0.1] * 768),
|
|
385
|
+
Document(content="bool", meta={"flag": True}, embedding=[0.1] * 768),
|
|
386
|
+
]
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
result = document_store.filter_documents({"field": "meta.flag", "operator": "==", "value": True})
|
|
390
|
+
|
|
391
|
+
assert [d.content for d in result] == ["bool"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qdrant_haystack-13.1.0 → qdrant_haystack-13.1.1}/src/haystack_integrations/document_stores/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|