pgvector-haystack 6.5.0__tar.gz → 6.6.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.
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/CHANGELOG.md +81 -16
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/LICENSE.txt +1 -1
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/PKG-INFO +4 -2
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/README.md +2 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/document_stores/pgvector/document_store.py +57 -9
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/document_stores/pgvector/filters.py +4 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_document_store.py +57 -150
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_document_store_async.py +57 -16
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_filters.py +18 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/.gitignore +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/examples/embedding_retrieval.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/examples/hybrid_retrieval.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/pydoc/config_docusaurus.yml +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/pyproject.toml +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/components/retrievers/py.typed +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/document_stores/pgvector/converters.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/src/haystack_integrations/document_stores/py.typed +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/__init__.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/conftest.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_converters.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_retrieval.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_retrieval_async.py +0 -0
- {pgvector_haystack-6.5.0 → pgvector_haystack-6.6.1}/tests/test_retrievers.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/pgvector-v6.6.0] - 2026-08-26
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes
|
|
6
|
+
|
|
7
|
+
- PGVector adding optional parameter `filters` to `get_metadata_field_unique_values()` (#3735)
|
|
8
|
+
- Standardize license files (#3771)
|
|
9
|
+
|
|
10
|
+
### 🧪 Testing
|
|
11
|
+
|
|
12
|
+
- `PGVectorDocumentStore` removing duplicated tests already covered by haystack Mixin tests (#3783)
|
|
13
|
+
|
|
14
|
+
### 🧹 Chores
|
|
15
|
+
|
|
16
|
+
- Re-standardize READMEs (#3768)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [integrations/pgvector-v6.5.0] - 2026-08-03
|
|
20
|
+
|
|
21
|
+
### 🚀 Features
|
|
22
|
+
|
|
23
|
+
- Add array_contains and array_overlaps operators to pgvector filters (#3459)
|
|
24
|
+
|
|
25
|
+
### 🐛 Bug Fixes
|
|
26
|
+
|
|
27
|
+
- Fix new issues raised by ruff 0.16.0 (#3670)
|
|
28
|
+
- `PGVectorDocumentStore` get_metadata_field_unique_values setting list of values to return to `Any` (#3709)
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## [integrations/pgvector-v6.4.1] - 2026-07-23
|
|
4
32
|
|
|
5
33
|
### 🐛 Bug Fixes
|
|
@@ -135,9 +163,14 @@
|
|
|
135
163
|
|
|
136
164
|
### 🧹 Chores
|
|
137
165
|
|
|
138
|
-
- Remove black (#1985)
|
|
139
166
|
- Pgvector - update docker image in examples (#1995)
|
|
140
167
|
|
|
168
|
+
## [integrations/pgvector-v5.2.0] - 2025-06-26
|
|
169
|
+
|
|
170
|
+
### 🧹 Chores
|
|
171
|
+
|
|
172
|
+
- Remove black (#1985)
|
|
173
|
+
|
|
141
174
|
|
|
142
175
|
## [integrations/pgvector-v3.4.1] - 2025-06-20
|
|
143
176
|
|
|
@@ -241,48 +274,75 @@
|
|
|
241
274
|
|
|
242
275
|
### 🚀 Features
|
|
243
276
|
|
|
244
|
-
- Add filter_policy to pgvector integration (#820)
|
|
245
277
|
- Add schema support to pgvector document store. (#1095)
|
|
246
278
|
- Pgvector - recreate the connection if it is no longer valid (#1202)
|
|
247
279
|
|
|
248
|
-
### 🐛 Bug Fixes
|
|
249
|
-
|
|
250
|
-
- `PgVector` - Fallback to default filter policy when deserializing retrievers without the init parameter (#900)
|
|
251
|
-
|
|
252
280
|
### 📚 Documentation
|
|
253
281
|
|
|
254
282
|
- Explain different connection string formats in the docstring (#1132)
|
|
255
283
|
|
|
284
|
+
### ⚙️ CI
|
|
285
|
+
|
|
286
|
+
- Adopt uv as installer (#1142)
|
|
287
|
+
|
|
288
|
+
### 🧹 Chores
|
|
289
|
+
|
|
290
|
+
- Update changelog after removing legacy filters (#1083)
|
|
291
|
+
- Update ruff linting scripts and settings (#1105)
|
|
292
|
+
|
|
293
|
+
## [integrations/pgvector-v1.0.0] - 2024-09-12
|
|
294
|
+
|
|
256
295
|
### 🧪 Testing
|
|
257
296
|
|
|
258
297
|
- Do not retry tests in `hatch run test` command (#954)
|
|
259
298
|
|
|
299
|
+
### 🧹 Chores
|
|
300
|
+
|
|
301
|
+
- PgVector - remove legacy filter support (#1068)
|
|
302
|
+
|
|
303
|
+
### 🌀 Miscellaneous
|
|
304
|
+
|
|
305
|
+
- Chore: Update pgvector test for the new `apply_filter_policy` usage (#970)
|
|
306
|
+
- Chore: pgvector ruff update, don't ruff tests (#984)
|
|
307
|
+
|
|
308
|
+
## [integrations/pgvector-v0.5.1] - 2024-07-17
|
|
309
|
+
|
|
310
|
+
### 🐛 Bug Fixes
|
|
311
|
+
|
|
312
|
+
- `PgVector` - Fallback to default filter policy when deserializing retrievers without the init parameter (#900)
|
|
313
|
+
|
|
314
|
+
## [integrations/pgvector-v0.5.0] - 2024-07-15
|
|
315
|
+
|
|
316
|
+
### 🚀 Features
|
|
317
|
+
|
|
318
|
+
- Add filter_policy to pgvector integration (#820)
|
|
319
|
+
|
|
260
320
|
### ⚙️ CI
|
|
261
321
|
|
|
262
322
|
- Retry tests to reduce flakyness (#836)
|
|
263
|
-
- Adopt uv as installer (#1142)
|
|
264
323
|
|
|
265
324
|
### 🧹 Chores
|
|
266
325
|
|
|
267
326
|
- Update ruff invocation to include check parameter (#853)
|
|
268
|
-
- PgVector - remove legacy filter support (#1068)
|
|
269
|
-
- Update changelog after removing legacy filters (#1083)
|
|
270
|
-
- Update ruff linting scripts and settings (#1105)
|
|
271
327
|
|
|
272
328
|
### 🌀 Miscellaneous
|
|
273
329
|
|
|
274
330
|
- Ci: install `pytest-rerunfailures` where needed; add retry config to `test-cov` script (#845)
|
|
275
331
|
- Chore: Minor retriever pydoc fix (#884)
|
|
276
|
-
- Chore: Update pgvector test for the new `apply_filter_policy` usage (#970)
|
|
277
|
-
- Chore: pgvector ruff update, don't ruff tests (#984)
|
|
278
332
|
|
|
279
333
|
## [integrations/pgvector-v0.4.0] - 2024-06-20
|
|
280
334
|
|
|
281
335
|
### 🚀 Features
|
|
282
336
|
|
|
283
|
-
- Defer the database connection to when it's needed (#773)
|
|
284
337
|
- Add customizable index names for pgvector (#818)
|
|
285
338
|
|
|
339
|
+
|
|
340
|
+
## [integrations/pgvector-v0.3.0] - 2024-05-30
|
|
341
|
+
|
|
342
|
+
### 🚀 Features
|
|
343
|
+
|
|
344
|
+
- Defer the database connection to when it's needed (#773)
|
|
345
|
+
|
|
286
346
|
### 🌀 Miscellaneous
|
|
287
347
|
|
|
288
348
|
- Docs: add missing api references (#728)
|
|
@@ -316,6 +376,14 @@
|
|
|
316
376
|
|
|
317
377
|
## [integrations/pgvector-v0.1.0] - 2024-02-14
|
|
318
378
|
|
|
379
|
+
### 🌀 Miscellaneous
|
|
380
|
+
|
|
381
|
+
- Pgvector: generate API docs (#325)
|
|
382
|
+
- Pgvector: add an example (#334)
|
|
383
|
+
- Adopt `Secret` to pgvector (#402)
|
|
384
|
+
|
|
385
|
+
## [integrations/pgvector-v0.0.1] - 2024-02-02
|
|
386
|
+
|
|
319
387
|
### 🐛 Bug Fixes
|
|
320
388
|
|
|
321
389
|
- Pgvector: fix linting (#328)
|
|
@@ -326,8 +394,5 @@
|
|
|
326
394
|
- Pgvector - filters (#257)
|
|
327
395
|
- Pgvector - embedding retrieval (#298)
|
|
328
396
|
- Pgvector - Embedding Retriever (#320)
|
|
329
|
-
- Pgvector: generate API docs (#325)
|
|
330
|
-
- Pgvector: add an example (#334)
|
|
331
|
-
- Adopt `Secret` to pgvector (#402)
|
|
332
397
|
|
|
333
398
|
<!-- 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
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pgvector-haystack
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.6.1
|
|
4
4
|
Summary: An integration of pgvector (vector search extension for Postgres) 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/pgvector/README.md
|
|
@@ -34,6 +34,8 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
## Contributing
|
|
38
|
+
|
|
37
39
|
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
38
40
|
|
|
39
41
|
To run integration tests locally, you need to have a PostgreSQL DB running with the `pgvector` extension.
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## Contributing
|
|
12
|
+
|
|
11
13
|
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
12
14
|
|
|
13
15
|
To run integration tests locally, you need to have a PostgreSQL DB running with the `pgvector` extension.
|
|
@@ -923,7 +923,7 @@ class PgvectorDocumentStore:
|
|
|
923
923
|
)
|
|
924
924
|
raise DocumentStoreError(error_msg) from e
|
|
925
925
|
|
|
926
|
-
#
|
|
926
|
+
# executemany(returning=True) yields one result set per document, so walk them all with nextset()
|
|
927
927
|
# https://www.psycopg.org/psycopg3/docs/api/cursors.html#psycopg.Cursor.executemany
|
|
928
928
|
written_docs = 0
|
|
929
929
|
while True:
|
|
@@ -980,9 +980,14 @@ class PgvectorDocumentStore:
|
|
|
980
980
|
)
|
|
981
981
|
raise DocumentStoreError(error_msg) from e
|
|
982
982
|
|
|
983
|
+
# executemany(returning=True) yields one result set per document, so walk them all with nextset()
|
|
984
|
+
# https://www.psycopg.org/psycopg3/docs/api/cursors.html#psycopg.Cursor.executemany
|
|
983
985
|
written_docs = 0
|
|
984
|
-
|
|
985
|
-
|
|
986
|
+
while True:
|
|
987
|
+
if await self._async_cursor.fetchone():
|
|
988
|
+
written_docs += 1
|
|
989
|
+
if not self._async_cursor.nextset():
|
|
990
|
+
break
|
|
986
991
|
|
|
987
992
|
return written_docs
|
|
988
993
|
|
|
@@ -1926,7 +1931,12 @@ class PgvectorDocumentStore:
|
|
|
1926
1931
|
return {"min": min_value, "max": max_value}
|
|
1927
1932
|
|
|
1928
1933
|
def _build_unique_values_queries(
|
|
1929
|
-
self,
|
|
1934
|
+
self,
|
|
1935
|
+
normalized_field: str,
|
|
1936
|
+
search_term: str | None,
|
|
1937
|
+
from_: int,
|
|
1938
|
+
size: int,
|
|
1939
|
+
filters: dict[str, Any] | None = None,
|
|
1930
1940
|
) -> tuple[Composed, Composed, tuple]:
|
|
1931
1941
|
"""
|
|
1932
1942
|
Builds SQL queries for getting unique metadata field values.
|
|
@@ -1936,6 +1946,7 @@ class PgvectorDocumentStore:
|
|
|
1936
1946
|
against the metadata field's own value.
|
|
1937
1947
|
:param from_: The offset for pagination (0-based).
|
|
1938
1948
|
:param size: The number of unique values to return.
|
|
1949
|
+
:param filters: Optional filters to restrict the documents considered.
|
|
1939
1950
|
:returns: A tuple containing (count_query, select_query, params).
|
|
1940
1951
|
"""
|
|
1941
1952
|
field_literal = SQLLiteral(normalized_field)
|
|
@@ -1944,6 +1955,9 @@ class PgvectorDocumentStore:
|
|
|
1944
1955
|
# Use the `->` (jsonb) operator rather than `->>` (text) so DISTINCT/ORDER BY operate on the
|
|
1945
1956
|
# value's original JSON type (numbers sort numerically, not lexicographically) and psycopg
|
|
1946
1957
|
# decodes the result to its native Python type instead of a string.
|
|
1958
|
+
#
|
|
1959
|
+
# Not fixable here: JSONB equality treats a whole-number float (1.0) as identical to a
|
|
1960
|
+
# numerically equal int (1), so DISTINCT still collapses them - see this method's docstring.
|
|
1947
1961
|
sql_select = SQL("SELECT DISTINCT meta->{} AS value").format(field_literal)
|
|
1948
1962
|
sql_from = SQL(" FROM {schema_name}.{table_name}").format(
|
|
1949
1963
|
schema_name=Identifier(self.schema_name), table_name=Identifier(self.table_name)
|
|
@@ -1951,10 +1965,16 @@ class PgvectorDocumentStore:
|
|
|
1951
1965
|
sql_where = SQL(" WHERE meta->>{} IS NOT NULL").format(field_literal)
|
|
1952
1966
|
|
|
1953
1967
|
params: tuple = ()
|
|
1968
|
+
if filters:
|
|
1969
|
+
_validate_filters(filters)
|
|
1970
|
+
filters_where_clause, filters_params = _convert_filters_to_where_clause_and_params(filters, operator="AND")
|
|
1971
|
+
sql_where += filters_where_clause
|
|
1972
|
+
params += filters_params
|
|
1973
|
+
|
|
1954
1974
|
if search_term:
|
|
1955
1975
|
# Case-insensitive substring match against the metadata field's own (text) value.
|
|
1956
1976
|
sql_where += SQL(" AND meta->>{} ILIKE %s").format(field_literal)
|
|
1957
|
-
params
|
|
1977
|
+
params += (f"%{search_term}%",)
|
|
1958
1978
|
|
|
1959
1979
|
# count query
|
|
1960
1980
|
sql_count = SQL("SELECT COUNT(DISTINCT meta->{} ) AS total").format(field_literal)
|
|
@@ -1984,22 +2004,36 @@ class PgvectorDocumentStore:
|
|
|
1984
2004
|
return unique_values, total_count
|
|
1985
2005
|
|
|
1986
2006
|
def get_metadata_field_unique_values(
|
|
1987
|
-
self,
|
|
2007
|
+
self,
|
|
2008
|
+
metadata_field: str,
|
|
2009
|
+
search_term: str | None = None,
|
|
2010
|
+
from_: int = 0,
|
|
2011
|
+
size: int = 10,
|
|
2012
|
+
filters: dict[str, Any] | None = None,
|
|
1988
2013
|
) -> tuple[list[Any], int]:
|
|
1989
2014
|
"""
|
|
1990
2015
|
Returns unique values for a given metadata field, optionally filtered by a search term.
|
|
1991
2016
|
|
|
2017
|
+
**Note**: values of different JSON type categories are kept distinct - a string, a number and
|
|
2018
|
+
a boolean never collapse into each other, even when they share a textual form (e.g. the string
|
|
2019
|
+
`"1"` and the number `1`). One exception: the ``meta`` column is JSONB, whose equality treats a
|
|
2020
|
+
whole-number float (`1.0`) as identical to a numerically equal int (`1`), so those two collapse
|
|
2021
|
+
into a single value. Floats with a fractional part (e.g. `1.5`) are unaffected.
|
|
2022
|
+
|
|
1992
2023
|
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
1993
2024
|
:param search_term: Optional search term to filter unique values by a case-insensitive substring
|
|
1994
2025
|
match against the metadata field's own value. If None, all values are considered.
|
|
1995
2026
|
:param from_: The offset for pagination (0-based).
|
|
1996
2027
|
:param size: The number of unique values to return.
|
|
2028
|
+
:param filters: Optional filters to restrict the documents considered.
|
|
1997
2029
|
:returns: A tuple containing:
|
|
1998
2030
|
- A list of unique values in their original type
|
|
1999
2031
|
- The total count of unique values
|
|
2000
2032
|
"""
|
|
2001
2033
|
normalized_field = PgvectorDocumentStore._normalize_metadata_field_name(metadata_field)
|
|
2002
|
-
sql_count, sql_query, params = self._build_unique_values_queries(
|
|
2034
|
+
sql_count, sql_query, params = self._build_unique_values_queries(
|
|
2035
|
+
normalized_field, search_term, from_, size, filters
|
|
2036
|
+
)
|
|
2003
2037
|
|
|
2004
2038
|
self._ensure_db_setup()
|
|
2005
2039
|
assert self._dict_cursor is not None
|
|
@@ -2022,22 +2056,36 @@ class PgvectorDocumentStore:
|
|
|
2022
2056
|
return PgvectorDocumentStore._process_unique_values_result(count_result, records)
|
|
2023
2057
|
|
|
2024
2058
|
async def get_metadata_field_unique_values_async(
|
|
2025
|
-
self,
|
|
2059
|
+
self,
|
|
2060
|
+
metadata_field: str,
|
|
2061
|
+
search_term: str | None = None,
|
|
2062
|
+
from_: int = 0,
|
|
2063
|
+
size: int = 10,
|
|
2064
|
+
filters: dict[str, Any] | None = None,
|
|
2026
2065
|
) -> tuple[list[Any], int]:
|
|
2027
2066
|
"""
|
|
2028
2067
|
Asynchronously returns unique values for a given metadata field, optionally filtered by a search term.
|
|
2029
2068
|
|
|
2069
|
+
**Note**: values of different JSON type categories are kept distinct - a string, a number and
|
|
2070
|
+
a boolean never collapse into each other, even when they share a textual form (e.g. the string
|
|
2071
|
+
`"1"` and the number `1`). One exception: the ``meta`` column is JSONB, whose equality treats a
|
|
2072
|
+
whole-number float (`1.0`) as identical to a numerically equal int (`1`), so those two collapse
|
|
2073
|
+
into a single value. Floats with a fractional part (e.g. `1.5`) are unaffected.
|
|
2074
|
+
|
|
2030
2075
|
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
2031
2076
|
:param search_term: Optional search term to filter unique values by a case-insensitive substring
|
|
2032
2077
|
match against the metadata field's own value. If None, all values are considered.
|
|
2033
2078
|
:param from_: The offset for pagination (0-based).
|
|
2034
2079
|
:param size: The number of unique values to return.
|
|
2080
|
+
:param filters: Optional filters to restrict the documents considered.
|
|
2035
2081
|
:returns: A tuple containing:
|
|
2036
2082
|
- A list of unique values in their original type
|
|
2037
2083
|
- The total count of unique values
|
|
2038
2084
|
"""
|
|
2039
2085
|
normalized_field = PgvectorDocumentStore._normalize_metadata_field_name(metadata_field)
|
|
2040
|
-
sql_count, sql_query, params = self._build_unique_values_queries(
|
|
2086
|
+
sql_count, sql_query, params = self._build_unique_values_queries(
|
|
2087
|
+
normalized_field, search_term, from_, size, filters
|
|
2088
|
+
)
|
|
2041
2089
|
|
|
2042
2090
|
await self._ensure_db_setup_async()
|
|
2043
2091
|
assert self._async_dict_cursor is not None
|
|
@@ -71,6 +71,10 @@ def _parse_logical_condition(condition: dict[str, Any]) -> tuple[Composed, list[
|
|
|
71
71
|
msg = f"Unknown logical operator '{operator}'. Valid operators are: 'AND', 'OR'"
|
|
72
72
|
raise FilterError(msg)
|
|
73
73
|
|
|
74
|
+
if not condition["conditions"]:
|
|
75
|
+
msg = f"'conditions' must not be empty in {condition}"
|
|
76
|
+
raise FilterError(msg)
|
|
77
|
+
|
|
74
78
|
# logical conditions can be nested, so we need to parse them recursively
|
|
75
79
|
conditions = []
|
|
76
80
|
for c in condition["conditions"]:
|
|
@@ -66,6 +66,55 @@ class TestDocumentStore(
|
|
|
66
66
|
with pytest.raises(DuplicateDocumentError):
|
|
67
67
|
document_store.write_documents(docs, DuplicatePolicy.FAIL)
|
|
68
68
|
|
|
69
|
+
def test_write_documents_counts_every_document(self, document_store: PgvectorDocumentStore):
|
|
70
|
+
docs = [Document(id="1"), Document(id="2"), Document(id="3")]
|
|
71
|
+
assert document_store.write_documents(docs) == 3
|
|
72
|
+
|
|
73
|
+
def test_write_documents_skip_counts_only_new_documents(self, document_store: PgvectorDocumentStore):
|
|
74
|
+
document_store.write_documents([Document(id="1")])
|
|
75
|
+
docs = [Document(id="1"), Document(id="2"), Document(id="3")]
|
|
76
|
+
assert document_store.write_documents(docs, DuplicatePolicy.SKIP) == 2
|
|
77
|
+
|
|
78
|
+
def test_write_documents_overwrite_counts_every_document(self, document_store: PgvectorDocumentStore):
|
|
79
|
+
document_store.write_documents([Document(id="1", content="old")])
|
|
80
|
+
docs = [Document(id="1", content="new"), Document(id="2"), Document(id="3")]
|
|
81
|
+
assert document_store.write_documents(docs, DuplicatePolicy.OVERWRITE) == 3
|
|
82
|
+
|
|
83
|
+
def test_get_metadata_field_unique_values_distinct_types(self, document_store: PgvectorDocumentStore):
|
|
84
|
+
"""
|
|
85
|
+
Override: the base mixin test stores int, float, str and bool under the *same* metadata field
|
|
86
|
+
name and expects all four back as distinct values. This store's ``meta`` column is JSONB, and
|
|
87
|
+
PostgreSQL's JSONB equality treats a whole-number float (e.g. 1.0) and a numerically equal int
|
|
88
|
+
(1) as the same value, so ``SELECT DISTINCT meta->'field'`` collapses them regardless of which
|
|
89
|
+
other values share that field.
|
|
90
|
+
|
|
91
|
+
This adapts the same intent - int, float, str and bool must come back as distinct, unmangled
|
|
92
|
+
types via get_metadata_field_unique_values() - using one field per type instead of one shared
|
|
93
|
+
field, which is what this store can actually support.
|
|
94
|
+
|
|
95
|
+
The float value is a non-whole number (1.5, not 1.0): a whole-number float would still collapse
|
|
96
|
+
with an int under PostgreSQL's JSONB numeric equality even in its own field, so a fractional
|
|
97
|
+
value is used to sidestep that ambiguity entirely.
|
|
98
|
+
"""
|
|
99
|
+
docs = [
|
|
100
|
+
Document(content="Doc 1", meta={"priority_int": 1}),
|
|
101
|
+
Document(content="Doc 2", meta={"priority_str": "1"}),
|
|
102
|
+
Document(content="Doc 3", meta={"priority_float": 1.5}),
|
|
103
|
+
Document(content="Doc 4", meta={"priority_bool": True}),
|
|
104
|
+
]
|
|
105
|
+
document_store.write_documents(docs)
|
|
106
|
+
|
|
107
|
+
int_values, int_count = document_store.get_metadata_field_unique_values(metadata_field="priority_int")
|
|
108
|
+
str_values, str_count = document_store.get_metadata_field_unique_values(metadata_field="priority_str")
|
|
109
|
+
float_values, float_count = document_store.get_metadata_field_unique_values(metadata_field="priority_float")
|
|
110
|
+
bool_values, bool_count = document_store.get_metadata_field_unique_values(metadata_field="priority_bool")
|
|
111
|
+
|
|
112
|
+
assert (int_count, str_count, float_count, bool_count) == (1, 1, 1, 1)
|
|
113
|
+
assert int_values == [1] and type(int_values[0]) is int
|
|
114
|
+
assert str_values == ["1"] and type(str_values[0]) is str
|
|
115
|
+
assert float_values == [1.5] and type(float_values[0]) is float
|
|
116
|
+
assert bool_values == [True] and type(bool_values[0]) is bool
|
|
117
|
+
|
|
69
118
|
def test_write_blob(self, document_store: PgvectorDocumentStore):
|
|
70
119
|
bytestream = ByteStream(b"test", meta={"meta_key": "meta_value"}, mime_type="mime_type")
|
|
71
120
|
docs = [Document(id="1", blob=bytestream)]
|
|
@@ -596,172 +645,30 @@ def test_get_metadata_field_min_max(document_store: PgvectorDocumentStore):
|
|
|
596
645
|
|
|
597
646
|
|
|
598
647
|
@pytest.mark.integration
|
|
599
|
-
def
|
|
600
|
-
|
|
648
|
+
def test_get_metadata_field_unique_values_pagination_edge_cases(document_store: PgvectorDocumentStore):
|
|
649
|
+
"""
|
|
650
|
+
Edge cases not covered by the shared GetMetadataFieldUniqueValuesTest mixin's pagination test:
|
|
651
|
+
size larger than the total count, from_ beyond the total count, and single-item-per-page paging.
|
|
652
|
+
"""
|
|
601
653
|
docs = [
|
|
602
|
-
Document(content="Python programming", meta={"category": "A"
|
|
603
|
-
Document(content="Java programming", meta={"category": "B"
|
|
604
|
-
Document(content="
|
|
605
|
-
Document(content="JavaScript development", meta={"category": "C", "language": "JavaScript"}),
|
|
606
|
-
Document(content="Python data science", meta={"category": "A", "language": "Python"}),
|
|
607
|
-
Document(content="Java backend", meta={"category": "B", "language": "Java"}),
|
|
654
|
+
Document(content="Python programming", meta={"category": "A"}),
|
|
655
|
+
Document(content="Java programming", meta={"category": "B"}),
|
|
656
|
+
Document(content="JavaScript development", meta={"category": "C"}),
|
|
608
657
|
]
|
|
609
658
|
document_store.write_documents(docs)
|
|
610
659
|
|
|
611
|
-
# Test getting all unique values without search term
|
|
612
|
-
unique_values, total_count = document_store.get_metadata_field_unique_values("meta.category", None, 0, 10)
|
|
613
|
-
assert set(unique_values) == {"A", "B", "C"}
|
|
614
|
-
assert total_count == 3
|
|
615
|
-
|
|
616
|
-
# Test with "meta." prefix
|
|
617
|
-
unique_languages, total_languages = document_store.get_metadata_field_unique_values("meta.language", None, 0, 10)
|
|
618
|
-
assert set(unique_languages) == {"Python", "Java", "JavaScript"}
|
|
619
|
-
assert total_languages == 3
|
|
620
|
-
|
|
621
|
-
# Test pagination - first page
|
|
622
|
-
unique_values_page1, total_count_page1 = document_store.get_metadata_field_unique_values(
|
|
623
|
-
"meta.category", None, 0, 2
|
|
624
|
-
)
|
|
625
|
-
assert len(unique_values_page1) == 2
|
|
626
|
-
assert all(val in ["A", "B", "C"] for val in unique_values_page1)
|
|
627
|
-
assert total_count_page1 == 3
|
|
628
|
-
|
|
629
|
-
# Test pagination - second page
|
|
630
|
-
unique_values_page2, total_count_page2 = document_store.get_metadata_field_unique_values(
|
|
631
|
-
"meta.category", None, 2, 2
|
|
632
|
-
)
|
|
633
|
-
assert len(unique_values_page2) == 1
|
|
634
|
-
assert unique_values_page2[0] in ["A", "B", "C"]
|
|
635
|
-
assert total_count_page2 == 3
|
|
636
|
-
|
|
637
|
-
# Test pagination - verify pages don't overlap
|
|
638
|
-
assert not set(unique_values_page1).intersection(set(unique_values_page2))
|
|
639
|
-
|
|
640
|
-
# Test pagination - verify all values are covered
|
|
641
|
-
all_values = set(unique_values_page1) | set(unique_values_page2)
|
|
642
|
-
assert all_values == {"A", "B", "C"}
|
|
643
|
-
|
|
644
|
-
# Test pagination - size larger than total count
|
|
645
660
|
unique_values_large, total_large = document_store.get_metadata_field_unique_values("meta.category", None, 0, 100)
|
|
646
|
-
assert len(unique_values_large) == 3
|
|
647
661
|
assert set(unique_values_large) == {"A", "B", "C"}
|
|
648
662
|
assert total_large == 3
|
|
649
663
|
|
|
650
|
-
# Test pagination - from_ beyond total count (should return empty)
|
|
651
664
|
unique_values_beyond, total_beyond = document_store.get_metadata_field_unique_values("meta.category", None, 10, 10)
|
|
652
665
|
assert len(unique_values_beyond) == 0
|
|
653
666
|
assert total_beyond == 3
|
|
654
667
|
|
|
655
|
-
# Test pagination - single item per page
|
|
656
668
|
unique_values_single1, _ = document_store.get_metadata_field_unique_values("meta.category", None, 0, 1)
|
|
657
669
|
unique_values_single2, _ = document_store.get_metadata_field_unique_values("meta.category", None, 1, 1)
|
|
658
670
|
unique_values_single3, _ = document_store.get_metadata_field_unique_values("meta.category", None, 2, 1)
|
|
659
671
|
assert len(unique_values_single1) == 1
|
|
660
672
|
assert len(unique_values_single2) == 1
|
|
661
673
|
assert len(unique_values_single3) == 1
|
|
662
|
-
# All three pages should be different
|
|
663
674
|
assert len(set(unique_values_single1 + unique_values_single2 + unique_values_single3)) == 3
|
|
664
|
-
|
|
665
|
-
# Test with search term - case-insensitive substring match against the metadata field's OWN value
|
|
666
|
-
# (not against document content).
|
|
667
|
-
unique_values_filtered, total_filtered = document_store.get_metadata_field_unique_values(
|
|
668
|
-
"meta.language", "Python", 0, 10
|
|
669
|
-
)
|
|
670
|
-
assert set(unique_values_filtered) == {"Python"}
|
|
671
|
-
assert total_filtered == 1
|
|
672
|
-
|
|
673
|
-
# Case-insensitivity: lowercase search term should still match "Python"
|
|
674
|
-
unique_values_lower, total_lower = document_store.get_metadata_field_unique_values("meta.language", "python", 0, 10)
|
|
675
|
-
assert set(unique_values_lower) == {"Python"}
|
|
676
|
-
assert total_lower == 1
|
|
677
|
-
|
|
678
|
-
# Substring matching: "Java" is a substring of both "Java" and "JavaScript"
|
|
679
|
-
unique_values_java, total_java = document_store.get_metadata_field_unique_values("meta.language", "Java", 0, 10)
|
|
680
|
-
assert set(unique_values_java) == {"Java", "JavaScript"}
|
|
681
|
-
assert total_java == 2
|
|
682
|
-
|
|
683
|
-
# Substring matching in the middle/end of a value
|
|
684
|
-
unique_values_script, total_script = document_store.get_metadata_field_unique_values(
|
|
685
|
-
"meta.language", "Script", 0, 10
|
|
686
|
-
)
|
|
687
|
-
assert set(unique_values_script) == {"JavaScript"}
|
|
688
|
-
assert total_script == 1
|
|
689
|
-
|
|
690
|
-
# Test pagination with search term
|
|
691
|
-
unique_values_search_page1, total_search = document_store.get_metadata_field_unique_values(
|
|
692
|
-
"meta.language", "Python", 0, 1
|
|
693
|
-
)
|
|
694
|
-
assert len(unique_values_search_page1) == 1
|
|
695
|
-
assert unique_values_search_page1[0] == "Python"
|
|
696
|
-
assert total_search == 1
|
|
697
|
-
|
|
698
|
-
# Test pagination with search term - beyond results
|
|
699
|
-
unique_values_search_empty, total_search_empty = document_store.get_metadata_field_unique_values(
|
|
700
|
-
"meta.language", "Python", 10, 10
|
|
701
|
-
)
|
|
702
|
-
assert len(unique_values_search_empty) == 0
|
|
703
|
-
assert total_search_empty == 1
|
|
704
|
-
|
|
705
|
-
# Test that search_term matches metadata VALUE, not content
|
|
706
|
-
content_vs_metadata_docs = [
|
|
707
|
-
Document(content="This document mentions Python explicitly", meta={"topic": "cooking"}),
|
|
708
|
-
Document(content="Unrelated content about recipes", meta={"topic": "python-tutorial"}),
|
|
709
|
-
]
|
|
710
|
-
document_store.write_documents(content_vs_metadata_docs)
|
|
711
|
-
|
|
712
|
-
unique_topics, total_topics = document_store.get_metadata_field_unique_values("meta.topic", "python", 0, 10)
|
|
713
|
-
assert set(unique_topics) == {"python-tutorial"}
|
|
714
|
-
assert total_topics == 1
|
|
715
|
-
|
|
716
|
-
# Test with integer values
|
|
717
|
-
int_docs = [
|
|
718
|
-
Document(content="Doc 1", meta={"priority": 1}),
|
|
719
|
-
Document(content="Doc 2", meta={"priority": 2}),
|
|
720
|
-
Document(content="Doc 3", meta={"priority": 1}),
|
|
721
|
-
Document(content="Doc 4", meta={"priority": 3}),
|
|
722
|
-
]
|
|
723
|
-
document_store.write_documents(int_docs)
|
|
724
|
-
unique_priorities, total_priorities = document_store.get_metadata_field_unique_values("meta.priority", None, 0, 10)
|
|
725
|
-
assert set(unique_priorities) == {1, 2, 3}
|
|
726
|
-
assert total_priorities == 3
|
|
727
|
-
|
|
728
|
-
# Test with search term on integer field - substring match against the field's own (stringified)
|
|
729
|
-
# value, e.g. "Doc 1" (content) no longer matches; "1" (the value itself) does. The returned
|
|
730
|
-
# values themselves keep their original type (int here), only the match is textual.
|
|
731
|
-
unique_priorities_filtered, total_priorities_filtered = document_store.get_metadata_field_unique_values(
|
|
732
|
-
"meta.priority", "1", 0, 10
|
|
733
|
-
)
|
|
734
|
-
assert set(unique_priorities_filtered) == {1}
|
|
735
|
-
assert total_priorities_filtered == 1
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
@pytest.mark.integration
|
|
739
|
-
def test_get_metadata_field_unique_values_no_search_term(document_store: PgvectorDocumentStore):
|
|
740
|
-
docs = [
|
|
741
|
-
Document(content="mentions tsvector and content in the body", meta={"category": "A"}),
|
|
742
|
-
Document(content="another document", meta={"category": "B"}),
|
|
743
|
-
]
|
|
744
|
-
document_store.write_documents(docs)
|
|
745
|
-
|
|
746
|
-
values, total = document_store.get_metadata_field_unique_values("meta.category")
|
|
747
|
-
|
|
748
|
-
assert set(values) == {"A", "B"}
|
|
749
|
-
assert total == 2
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
@pytest.mark.integration
|
|
753
|
-
def test_get_metadata_field_unique_values_search_term_matches_field_value_not_content(
|
|
754
|
-
document_store: PgvectorDocumentStore,
|
|
755
|
-
):
|
|
756
|
-
docs = [
|
|
757
|
-
# "python" appears in the content but NOT in the category value -> must be EXCLUDED
|
|
758
|
-
Document(content="Python programming guide", meta={"category": "Backend"}),
|
|
759
|
-
# "python" appears in the category value but NOT in the content -> must be INCLUDED
|
|
760
|
-
Document(content="General purpose scripting language", meta={"category": "Python-based"}),
|
|
761
|
-
]
|
|
762
|
-
document_store.write_documents(docs)
|
|
763
|
-
|
|
764
|
-
values, total = document_store.get_metadata_field_unique_values("meta.category", search_term="python")
|
|
765
|
-
|
|
766
|
-
assert values == ["Python-based"]
|
|
767
|
-
assert total == 1
|
|
@@ -71,6 +71,63 @@ class TestDocumentStoreAsync(
|
|
|
71
71
|
with pytest.raises(DuplicateDocumentError):
|
|
72
72
|
await document_store.write_documents_async(docs, DuplicatePolicy.FAIL)
|
|
73
73
|
|
|
74
|
+
async def test_write_documents_async_counts_every_document(self, document_store: PgvectorDocumentStore):
|
|
75
|
+
docs = [Document(id="1"), Document(id="2"), Document(id="3")]
|
|
76
|
+
assert await document_store.write_documents_async(docs) == 3
|
|
77
|
+
|
|
78
|
+
async def test_write_documents_async_skip_counts_only_new_documents(self, document_store: PgvectorDocumentStore):
|
|
79
|
+
await document_store.write_documents_async([Document(id="1")])
|
|
80
|
+
docs = [Document(id="1"), Document(id="2"), Document(id="3")]
|
|
81
|
+
assert await document_store.write_documents_async(docs, DuplicatePolicy.SKIP) == 2
|
|
82
|
+
|
|
83
|
+
async def test_write_documents_async_overwrite_counts_every_document(self, document_store: PgvectorDocumentStore):
|
|
84
|
+
await document_store.write_documents_async([Document(id="1", content="old")])
|
|
85
|
+
docs = [Document(id="1", content="new"), Document(id="2"), Document(id="3")]
|
|
86
|
+
assert await document_store.write_documents_async(docs, DuplicatePolicy.OVERWRITE) == 3
|
|
87
|
+
|
|
88
|
+
async def test_get_metadata_field_unique_values_distinct_types_async(self, document_store: PgvectorDocumentStore):
|
|
89
|
+
"""
|
|
90
|
+
Override: the base mixin test stores int, float, str and bool under the *same* metadata field
|
|
91
|
+
name and expects all four back as distinct values. This store's ``meta`` column is JSONB, and
|
|
92
|
+
PostgreSQL's JSONB equality treats a whole-number float (e.g. 1.0) and a numerically equal int
|
|
93
|
+
(1) as the same value, so ``SELECT DISTINCT meta->'field'`` collapses them regardless of which
|
|
94
|
+
other values share that field.
|
|
95
|
+
|
|
96
|
+
This adapts the same intent - int, float, str and bool must come back as distinct, unmangled
|
|
97
|
+
types via get_metadata_field_unique_values_async() - using one field per type instead of one
|
|
98
|
+
shared field, which is what this store can actually support.
|
|
99
|
+
|
|
100
|
+
The float value is a non-whole number (1.5, not 1.0): a whole-number float would still collapse
|
|
101
|
+
with an int under PostgreSQL's JSONB numeric equality even in its own field, so a fractional
|
|
102
|
+
value is used to sidestep that ambiguity entirely.
|
|
103
|
+
"""
|
|
104
|
+
docs = [
|
|
105
|
+
Document(content="Doc 1", meta={"priority_int": 1}),
|
|
106
|
+
Document(content="Doc 2", meta={"priority_str": "1"}),
|
|
107
|
+
Document(content="Doc 3", meta={"priority_float": 1.5}),
|
|
108
|
+
Document(content="Doc 4", meta={"priority_bool": True}),
|
|
109
|
+
]
|
|
110
|
+
await document_store.write_documents_async(docs)
|
|
111
|
+
|
|
112
|
+
int_values, int_count = await document_store.get_metadata_field_unique_values_async(
|
|
113
|
+
metadata_field="priority_int"
|
|
114
|
+
)
|
|
115
|
+
str_values, str_count = await document_store.get_metadata_field_unique_values_async(
|
|
116
|
+
metadata_field="priority_str"
|
|
117
|
+
)
|
|
118
|
+
float_values, float_count = await document_store.get_metadata_field_unique_values_async(
|
|
119
|
+
metadata_field="priority_float"
|
|
120
|
+
)
|
|
121
|
+
bool_values, bool_count = await document_store.get_metadata_field_unique_values_async(
|
|
122
|
+
metadata_field="priority_bool"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
assert (int_count, str_count, float_count, bool_count) == (1, 1, 1, 1)
|
|
126
|
+
assert int_values == [1] and type(int_values[0]) is int
|
|
127
|
+
assert str_values == ["1"] and type(str_values[0]) is str
|
|
128
|
+
assert float_values == [1.5] and type(float_values[0]) is float
|
|
129
|
+
assert bool_values == [True] and type(bool_values[0]) is bool
|
|
130
|
+
|
|
74
131
|
async def test_count_not_empty_async(self, document_store: PgvectorDocumentStore):
|
|
75
132
|
"""Override: mixin method is missing 'self', causing fixture injection to fail."""
|
|
76
133
|
await document_store.write_documents_async(
|
|
@@ -124,22 +181,6 @@ class TestDocumentStoreAsync(
|
|
|
124
181
|
filters={"field": "meta.category", "operator": "==", "value": "A"}, meta={}
|
|
125
182
|
)
|
|
126
183
|
|
|
127
|
-
async def test_get_metadata_field_unique_values_async_preserves_non_string_types(
|
|
128
|
-
self, document_store: PgvectorDocumentStore
|
|
129
|
-
):
|
|
130
|
-
"""Non-string metadata values (e.g. ints) are returned in their original type, not stringified."""
|
|
131
|
-
docs = [
|
|
132
|
-
Document(content="Doc 1", meta={"priority": 1}),
|
|
133
|
-
Document(content="Doc 2", meta={"priority": 2}),
|
|
134
|
-
Document(content="Doc 3", meta={"priority": 1}),
|
|
135
|
-
]
|
|
136
|
-
await document_store.write_documents_async(docs)
|
|
137
|
-
|
|
138
|
-
values, total = await document_store.get_metadata_field_unique_values_async("meta.priority")
|
|
139
|
-
|
|
140
|
-
assert set(values) == {1, 2}
|
|
141
|
-
assert total == 2
|
|
142
|
-
|
|
143
184
|
async def test_delete_table_async_first_call(self, document_store: PgvectorDocumentStore):
|
|
144
185
|
"""
|
|
145
186
|
Test that delete_table_async can be executed as the initial operation on the Document Store
|
|
@@ -483,3 +483,21 @@ def test_validate_filters():
|
|
|
483
483
|
invalid_filters = {"field": "meta.number", "value": "100"}
|
|
484
484
|
with pytest.raises(ValueError):
|
|
485
485
|
_validate_filters(invalid_filters)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@pytest.mark.parametrize("operator", ["AND", "OR"])
|
|
489
|
+
def test_logical_condition_empty_conditions_raises(operator):
|
|
490
|
+
with pytest.raises(FilterError):
|
|
491
|
+
_parse_logical_condition({"operator": operator, "conditions": []})
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def test_logical_condition_nested_empty_conditions_raises():
|
|
495
|
+
filters = {
|
|
496
|
+
"operator": "AND",
|
|
497
|
+
"conditions": [
|
|
498
|
+
{"field": "meta.type", "operator": "==", "value": "article"},
|
|
499
|
+
{"operator": "OR", "conditions": []},
|
|
500
|
+
],
|
|
501
|
+
}
|
|
502
|
+
with pytest.raises(FilterError):
|
|
503
|
+
_convert_filters_to_where_clause_and_params(filters)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|