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.
Files changed (27) hide show
  1. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/CHANGELOG.md +15 -0
  2. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/PKG-INFO +1 -1
  3. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/document_store.py +11 -12
  4. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/filters.py +3 -3
  5. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_document_store.py +66 -9
  6. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_filters.py +28 -1
  7. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/.gitignore +0 -0
  8. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/LICENSE.txt +0 -0
  9. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/README.md +0 -0
  10. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/examples/embedding_retrieval.py +0 -0
  11. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/examples/hybrid_retrieval.py +0 -0
  12. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/pydoc/config_docusaurus.yml +0 -0
  13. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/pyproject.toml +0 -0
  14. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
  15. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
  16. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
  17. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  18. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
  19. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/pgvector/converters.py +0 -0
  20. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/src/haystack_integrations/document_stores/py.typed +0 -0
  21. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/__init__.py +0 -0
  22. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/conftest.py +0 -0
  23. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_converters.py +0 -0
  24. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_document_store_async.py +0 -0
  25. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_retrieval.py +0 -0
  26. {pgvector_haystack-6.4.0 → pgvector_haystack-6.4.1}/tests/test_retrieval_async.py +0 -0
  27. {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.0
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 documents by content.
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
- # Use full-text search with word boundaries (similar to keyword retrieval)
1952
- sql_where += SQL(" AND to_tsvector({language}, content) @@ plainto_tsquery({language}, %s)").format(
1953
- language=SQLLiteral(self.language)
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 documents by content before extracting unique values.
1992
- If None, all documents are considered.
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 documents by content before extracting unique values.
2030
- If None, all documents are considered.
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 Pinecone"
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 Pinecone"
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 - filter by content matching "Python"
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.category", "Python", 0, 10
668
+ "meta.language", "Python", 0, 10
668
669
  )
669
- assert set(unique_values_filtered) == {"A"} # Only category A has documents with "Python" in content
670
+ assert set(unique_values_filtered) == {"Python"}
670
671
  assert total_filtered == 1
671
672
 
672
- # Test with search term - filter by content matching "Java"
673
- unique_values_java, total_java = document_store.get_metadata_field_unique_values("meta.category", "Java", 0, 10)
674
- assert set(unique_values_java) == {"B"} # Only category B has documents with "Java" in content
675
- assert total_java == 1
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", "Doc 1", 0, 10
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",