pgvector-haystack 6.4.0__tar.gz → 6.4.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.4.0 → pgvector_haystack-6.4.1}/CHANGELOG.md +15 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/PKG-INFO +1 -1
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/document_store.py +11 -12
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/filters.py +3 -3
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_document_store.py +66 -9
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_filters.py +28 -1
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/.gitignore +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/LICENSE.txt +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/README.md +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/examples/embedding_retrieval.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/examples/hybrid_retrieval.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/pydoc/config_docusaurus.yml +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/pyproject.toml +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/py.typed +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/converters.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/py.typed +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/__init__.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/conftest.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_converters.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_document_store_async.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_retrieval.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_retrieval_async.py +0 -0
- {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_retrievers.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/pgvector-v6.4.0] - 2026-07-23
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Pgvector - add closing methods (#3650)
|
|
8
|
+
|
|
9
|
+
### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- *(pgvector)* NO_VALUE sentinel collides with the literal string 'no_value' (#3593)
|
|
12
|
+
|
|
13
|
+
### 📚 Documentation
|
|
14
|
+
|
|
15
|
+
- Replace old haystack core imports with haystack_integrations paths (#3545)
|
|
16
|
+
|
|
17
|
+
|
|
3
18
|
## [integrations/pgvector-v6.3.1] - 2026-06-01
|
|
4
19
|
|
|
5
20
|
### 🐛 Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgvector-haystack
|
|
3
|
-
Version: 6.4.
|
|
3
|
+
Version: 6.4.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
|
|
@@ -1932,7 +1932,8 @@ class PgvectorDocumentStore:
|
|
|
1932
1932
|
Builds SQL queries for getting unique metadata field values.
|
|
1933
1933
|
|
|
1934
1934
|
:param normalized_field: The normalized metadata field name.
|
|
1935
|
-
:param search_term: Optional search term to filter
|
|
1935
|
+
:param search_term: Optional search term to filter unique values by a case-insensitive substring match
|
|
1936
|
+
against the metadata field's own value.
|
|
1936
1937
|
:param from_: The offset for pagination (0-based).
|
|
1937
1938
|
:param size: The number of unique values to return.
|
|
1938
1939
|
:returns: A tuple containing (count_query, select_query, params).
|
|
@@ -1948,11 +1949,9 @@ class PgvectorDocumentStore:
|
|
|
1948
1949
|
|
|
1949
1950
|
params: tuple = ()
|
|
1950
1951
|
if search_term:
|
|
1951
|
-
#
|
|
1952
|
-
sql_where += SQL(" AND
|
|
1953
|
-
|
|
1954
|
-
)
|
|
1955
|
-
params = (search_term,)
|
|
1952
|
+
# Case-insensitive substring match against the metadata field's own value.
|
|
1953
|
+
sql_where += SQL(" AND meta->>{} ILIKE %s").format(field_literal)
|
|
1954
|
+
params = (f"%{search_term}%",)
|
|
1956
1955
|
|
|
1957
1956
|
# count query
|
|
1958
1957
|
sql_count = SQL("SELECT COUNT(DISTINCT meta->>{} ) AS total").format(field_literal)
|
|
@@ -1982,14 +1981,14 @@ class PgvectorDocumentStore:
|
|
|
1982
1981
|
return unique_values, total_count
|
|
1983
1982
|
|
|
1984
1983
|
def get_metadata_field_unique_values(
|
|
1985
|
-
self, metadata_field: str, search_term: str | None, from_: int, size: int
|
|
1984
|
+
self, metadata_field: str, search_term: str | None = None, from_: int = 0, size: int = 10
|
|
1986
1985
|
) -> tuple[list[str], int]:
|
|
1987
1986
|
"""
|
|
1988
1987
|
Returns unique values for a given metadata field, optionally filtered by a search term.
|
|
1989
1988
|
|
|
1990
1989
|
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
1991
|
-
:param search_term: Optional search term to filter
|
|
1992
|
-
If None, all
|
|
1990
|
+
:param search_term: Optional search term to filter unique values by a case-insensitive substring
|
|
1991
|
+
match against the metadata field's own value. If None, all values are considered.
|
|
1993
1992
|
:param from_: The offset for pagination (0-based).
|
|
1994
1993
|
:param size: The number of unique values to return.
|
|
1995
1994
|
:returns: A tuple containing:
|
|
@@ -2020,14 +2019,14 @@ class PgvectorDocumentStore:
|
|
|
2020
2019
|
return PgvectorDocumentStore._process_unique_values_result(count_result, records)
|
|
2021
2020
|
|
|
2022
2021
|
async def get_metadata_field_unique_values_async(
|
|
2023
|
-
self, metadata_field: str, search_term: str | None, from_: int, size: int
|
|
2022
|
+
self, metadata_field: str, search_term: str | None = None, from_: int = 0, size: int = 10
|
|
2024
2023
|
) -> tuple[list[str], int]:
|
|
2025
2024
|
"""
|
|
2026
2025
|
Asynchronously returns unique values for a given metadata field, optionally filtered by a search term.
|
|
2027
2026
|
|
|
2028
2027
|
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
2029
|
-
:param search_term: Optional search term to filter
|
|
2030
|
-
If None, all
|
|
2028
|
+
:param search_term: Optional search term to filter unique values by a case-insensitive substring
|
|
2029
|
+
match against the metadata field's own value. If None, all values are considered.
|
|
2031
2030
|
:param from_: The offset for pagination (0-based).
|
|
2032
2031
|
:param size: The number of unique values to return.
|
|
2033
2032
|
:returns: A tuple containing:
|
|
@@ -227,14 +227,14 @@ def _less_than_equal(field: Composable, value: Any) -> tuple[Composed, Any]:
|
|
|
227
227
|
|
|
228
228
|
def _not_in(field: Composable, value: Any) -> tuple[Composed, list]:
|
|
229
229
|
if not isinstance(value, list):
|
|
230
|
-
msg = f"{field}'s value must be a list when using 'not in' comparator in
|
|
230
|
+
msg = f"{field}'s value must be a list when using 'not in' comparator in PgVector"
|
|
231
231
|
raise FilterError(msg)
|
|
232
|
-
return SQL("{} IS NULL OR {} != ALL(%s)").format(field, field), [value]
|
|
232
|
+
return SQL("({} IS NULL OR {} != ALL(%s))").format(field, field), [value]
|
|
233
233
|
|
|
234
234
|
|
|
235
235
|
def _in(field: Composable, value: Any) -> tuple[Composed, list]:
|
|
236
236
|
if not isinstance(value, list):
|
|
237
|
-
msg = f"{field}'s value must be a list when using 'in' comparator in
|
|
237
|
+
msg = f"{field}'s value must be a list when using 'in' comparator in PgVector"
|
|
238
238
|
raise FilterError(msg)
|
|
239
239
|
|
|
240
240
|
# see https://www.psycopg.org/psycopg3/docs/basic/adapt.html#lists-adaptation
|
|
@@ -662,17 +662,30 @@ def test_get_metadata_field_unique_values(document_store: PgvectorDocumentStore)
|
|
|
662
662
|
# All three pages should be different
|
|
663
663
|
assert len(set(unique_values_single1 + unique_values_single2 + unique_values_single3)) == 3
|
|
664
664
|
|
|
665
|
-
# Test with search term -
|
|
665
|
+
# Test with search term - case-insensitive substring match against the metadata field's OWN value
|
|
666
|
+
# (not against document content).
|
|
666
667
|
unique_values_filtered, total_filtered = document_store.get_metadata_field_unique_values(
|
|
667
|
-
"meta.
|
|
668
|
+
"meta.language", "Python", 0, 10
|
|
668
669
|
)
|
|
669
|
-
assert set(unique_values_filtered) == {"
|
|
670
|
+
assert set(unique_values_filtered) == {"Python"}
|
|
670
671
|
assert total_filtered == 1
|
|
671
672
|
|
|
672
|
-
#
|
|
673
|
-
|
|
674
|
-
assert set(
|
|
675
|
-
assert
|
|
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
|
|
676
689
|
|
|
677
690
|
# Test pagination with search term
|
|
678
691
|
unique_values_search_page1, total_search = document_store.get_metadata_field_unique_values(
|
|
@@ -689,6 +702,17 @@ def test_get_metadata_field_unique_values(document_store: PgvectorDocumentStore)
|
|
|
689
702
|
assert len(unique_values_search_empty) == 0
|
|
690
703
|
assert total_search_empty == 1
|
|
691
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
|
+
|
|
692
716
|
# Test with integer values
|
|
693
717
|
int_docs = [
|
|
694
718
|
Document(content="Doc 1", meta={"priority": 1}),
|
|
@@ -701,9 +725,42 @@ def test_get_metadata_field_unique_values(document_store: PgvectorDocumentStore)
|
|
|
701
725
|
assert set(unique_priorities) == {"1", "2", "3"}
|
|
702
726
|
assert total_priorities == 3
|
|
703
727
|
|
|
704
|
-
# Test with search term on integer field
|
|
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.
|
|
705
730
|
unique_priorities_filtered, total_priorities_filtered = document_store.get_metadata_field_unique_values(
|
|
706
|
-
"meta.priority", "
|
|
731
|
+
"meta.priority", "1", 0, 10
|
|
707
732
|
)
|
|
708
733
|
assert set(unique_priorities_filtered) == {"1"}
|
|
709
734
|
assert total_priorities_filtered == 1
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@pytest.mark.integration
|
|
738
|
+
def test_get_metadata_field_unique_values_no_search_term(document_store: PgvectorDocumentStore):
|
|
739
|
+
docs = [
|
|
740
|
+
Document(content="mentions tsvector and content in the body", meta={"category": "A"}),
|
|
741
|
+
Document(content="another document", meta={"category": "B"}),
|
|
742
|
+
]
|
|
743
|
+
document_store.write_documents(docs)
|
|
744
|
+
|
|
745
|
+
values, total = document_store.get_metadata_field_unique_values("meta.category")
|
|
746
|
+
|
|
747
|
+
assert set(values) == {"A", "B"}
|
|
748
|
+
assert total == 2
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
@pytest.mark.integration
|
|
752
|
+
def test_get_metadata_field_unique_values_search_term_matches_field_value_not_content(
|
|
753
|
+
document_store: PgvectorDocumentStore,
|
|
754
|
+
):
|
|
755
|
+
docs = [
|
|
756
|
+
# "python" appears in the content but NOT in the category value -> must be EXCLUDED
|
|
757
|
+
Document(content="Python programming guide", meta={"category": "Backend"}),
|
|
758
|
+
# "python" appears in the category value but NOT in the content -> must be INCLUDED
|
|
759
|
+
Document(content="General purpose scripting language", meta={"category": "Python-based"}),
|
|
760
|
+
]
|
|
761
|
+
document_store.write_documents(docs)
|
|
762
|
+
|
|
763
|
+
values, total = document_store.get_metadata_field_unique_values("meta.category", search_term="python")
|
|
764
|
+
|
|
765
|
+
assert values == ["Python-based"]
|
|
766
|
+
assert total == 1
|
|
@@ -183,6 +183,14 @@ def test_comparison_condition_unknown_operator():
|
|
|
183
183
|
_parse_comparison_condition(condition)
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
@pytest.mark.parametrize("operator", ["like", "not like"])
|
|
187
|
+
def test_comparison_condition_like_operator_requires_str_value(operator):
|
|
188
|
+
# pgvector-specific: LIKE / NOT LIKE require a string operand.
|
|
189
|
+
condition = {"field": "meta.name", "operator": operator, "value": 123}
|
|
190
|
+
with pytest.raises(FilterError, match="must be a str when using 'LIKE'"):
|
|
191
|
+
_parse_comparison_condition(condition)
|
|
192
|
+
|
|
193
|
+
|
|
186
194
|
def test_logical_condition_missing_operator():
|
|
187
195
|
condition = {"conditions": []}
|
|
188
196
|
with pytest.raises(FilterError):
|
|
@@ -228,13 +236,32 @@ def test_logical_condition_nested():
|
|
|
228
236
|
"("
|
|
229
237
|
"(meta->>'domain' IS DISTINCT FROM %s OR meta->>'chapter' = ANY(%s))"
|
|
230
238
|
" AND "
|
|
231
|
-
"((meta->>'number')::integer >= %s OR meta->>'author' IS NULL OR meta->>'author' != ALL(%s))"
|
|
239
|
+
"((meta->>'number')::integer >= %s OR (meta->>'author' IS NULL OR meta->>'author' != ALL(%s)))"
|
|
232
240
|
")"
|
|
233
241
|
)
|
|
234
242
|
assert _render(query) == expected_sql
|
|
235
243
|
assert values == ["science", [["intro", "conclusion"]], 90, [["John", "Jane"]]]
|
|
236
244
|
|
|
237
245
|
|
|
246
|
+
def test_logical_condition_not_in_is_parenthesized_when_and_combined():
|
|
247
|
+
# `not in` renders an internal `OR` (`IS NULL OR != ALL`). Without surrounding
|
|
248
|
+
# parentheses that `OR` escapes an enclosing `AND` (AND binds tighter than OR in
|
|
249
|
+
# SQL), so an AND-combined `not in` would wrongly match documents.
|
|
250
|
+
condition = {
|
|
251
|
+
"operator": "AND",
|
|
252
|
+
"conditions": [
|
|
253
|
+
{"field": "meta.number", "operator": "==", "value": 5},
|
|
254
|
+
{"field": "meta.author", "operator": "not in", "value": ["John", "Jane"]},
|
|
255
|
+
],
|
|
256
|
+
}
|
|
257
|
+
query, values = _parse_logical_condition(condition)
|
|
258
|
+
assert isinstance(query, Composed)
|
|
259
|
+
|
|
260
|
+
expected_sql = "((meta->>'number')::integer = %s AND (meta->>'author' IS NULL OR meta->>'author' != ALL(%s)))"
|
|
261
|
+
assert _render(query) == expected_sql
|
|
262
|
+
assert values == [5, [["John", "Jane"]]]
|
|
263
|
+
|
|
264
|
+
|
|
238
265
|
def test_convert_filters_to_where_clause_and_params():
|
|
239
266
|
filters = {
|
|
240
267
|
"operator": "AND",
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|