pgvector-haystack 6.1.0__tar.gz → 6.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/CHANGELOG.md +7 -0
  2. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/PKG-INFO +2 -2
  3. pgvector_haystack-6.2.0/pydoc/config_docusaurus.yml +15 -0
  4. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/pyproject.toml +6 -2
  5. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/document_stores/pgvector/converters.py +5 -1
  6. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/document_stores/pgvector/filters.py +9 -0
  7. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_converters.py +17 -0
  8. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_document_store.py +18 -218
  9. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_filters.py +5 -0
  10. pgvector_haystack-6.1.0/pydoc/config_docusaurus.yml +0 -30
  11. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/.gitignore +0 -0
  12. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/LICENSE.txt +0 -0
  13. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/README.md +0 -0
  14. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/examples/embedding_retrieval.py +0 -0
  15. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/examples/hybrid_retrieval.py +0 -0
  16. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
  17. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
  18. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
  19. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  20. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
  21. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/document_stores/pgvector/document_store.py +0 -0
  22. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/src/haystack_integrations/document_stores/py.typed +0 -0
  23. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/__init__.py +0 -0
  24. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/conftest.py +0 -0
  25. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_document_store_async.py +0 -0
  26. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_retrieval.py +0 -0
  27. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_retrieval_async.py +0 -0
  28. {pgvector_haystack-6.1.0 → pgvector_haystack-6.2.0}/tests/test_retrievers.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/pgvector-v6.1.0] - 2026-01-28
4
+
5
+ ### 🚀 Features
6
+
7
+ - Adding count, filtering and metadata related operations to `PGVectorDocumentStore` (#2768)
8
+
9
+
3
10
  ## [integrations/pgvector-v6.0.0] - 2026-01-12
4
11
 
5
12
  ### 🧹 Chores
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgvector-haystack
3
- Version: 6.1.0
3
+ Version: 6.2.0
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
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
19
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
20
  Requires-Python: >=3.10
21
- Requires-Dist: haystack-ai>=2.22.0
21
+ Requires-Dist: haystack-ai>=2.24.0
22
22
  Requires-Dist: pgvector>=0.3.0
23
23
  Requires-Dist: psycopg[binary]
24
24
  Description-Content-Type: text/markdown
@@ -0,0 +1,15 @@
1
+ loaders:
2
+ - modules:
3
+ - haystack_integrations.components.retrievers.pgvector.embedding_retriever
4
+ - haystack_integrations.components.retrievers.pgvector.keyword_retriever
5
+ - haystack_integrations.document_stores.pgvector.document_store
6
+ search_path: [../src]
7
+ processors:
8
+ - type: filter
9
+ documented_only: true
10
+ skip_empty_modules: true
11
+ renderer:
12
+ description: Pgvector integration for Haystack
13
+ id: integrations-pgvector
14
+ filename: pgvector.md
15
+ title: Pgvector
@@ -22,7 +22,11 @@ classifiers = [
22
22
  "Programming Language :: Python :: Implementation :: CPython",
23
23
  "Programming Language :: Python :: Implementation :: PyPy",
24
24
  ]
25
- dependencies = ["haystack-ai>=2.22.0", "pgvector>=0.3.0", "psycopg[binary]"]
25
+ dependencies = [
26
+ "haystack-ai>=2.24.0",
27
+ "pgvector>=0.3.0",
28
+ "psycopg[binary]"
29
+ ]
26
30
 
27
31
  [project.urls]
28
32
  Source = "https://github.com/deepset-ai/haystack-core-integrations"
@@ -45,7 +49,7 @@ installer = "uv"
45
49
  dependencies = ["haystack-pydoc-tools", "ruff"]
46
50
 
47
51
  [tool.hatch.envs.default.scripts]
48
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
52
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
49
53
  fmt = "ruff check --fix {args}; ruff format {args}"
50
54
  fmt-check = "ruff check {args} && ruff format --check {args}"
51
55
 
@@ -1,3 +1,4 @@
1
+ from dataclasses import replace
1
2
  from typing import Any
2
3
 
3
4
  from haystack import logging
@@ -22,6 +23,9 @@ def _from_haystack_to_pg_documents(documents: list[Document]) -> list[dict[str,
22
23
  db_document["blob_meta"] = Jsonb(blob.meta) if blob and blob.meta else None
23
24
  db_document["blob_mime_type"] = blob.mime_type if blob and blob.mime_type else None
24
25
  db_document["meta"] = Jsonb(db_document["meta"])
26
+ # PostgreSQL text fields cannot contain NUL (0x00) bytes, removing NUL bytes
27
+ if content := db_document["content"]:
28
+ db_document["content"] = content.replace("\x00", "")
25
29
 
26
30
  if "sparse_embedding" in db_document:
27
31
  sparse_embedding = db_document.pop("sparse_embedding", None)
@@ -65,7 +69,7 @@ def _from_pg_to_haystack_documents(documents: list[dict[str, Any]]) -> list[Docu
65
69
 
66
70
  if blob_data:
67
71
  blob = ByteStream(data=blob_data, meta=blob_meta, mime_type=blob_mime_type)
68
- haystack_document.blob = blob
72
+ haystack_document = replace(haystack_document, blob=blob)
69
73
 
70
74
  haystack_documents.append(haystack_document)
71
75
 
@@ -1,6 +1,8 @@
1
1
  # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import re
4
6
  from datetime import datetime
5
7
  from itertools import chain
6
8
  from typing import Any, Literal
@@ -19,6 +21,7 @@ PYTHON_TYPES_TO_PG_TYPES = {
19
21
  }
20
22
 
21
23
  NO_VALUE = "no_value"
24
+ SAFE_META_FIELD_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
22
25
 
23
26
 
24
27
  def _validate_filters(filters: dict[str, Any] | None = None) -> None:
@@ -128,6 +131,12 @@ def _treat_meta_field(field: str, value: Any) -> str:
128
131
 
129
132
  # use the ->> operator to access keys in the meta JSONB field
130
133
  field_name = field.split(".", 1)[-1]
134
+ if not SAFE_META_FIELD_RE.match(field_name):
135
+ msg = (
136
+ f"Invalid metadata field name '{field_name}'. "
137
+ "Only alphanumeric characters, dashes, and underscores are allowed."
138
+ )
139
+ raise FilterError(msg)
131
140
  field = f"meta->>'{field_name}'"
132
141
 
133
142
  # meta fields are stored as strings in the JSONB field,
@@ -30,6 +30,13 @@ def test_from_haystack_to_pg_documents():
30
30
  embedding=[0.7, 0.8, 0.9],
31
31
  score=0.7,
32
32
  ),
33
+ Document(
34
+ id="4",
35
+ content="This is another text\x00",
36
+ meta={"meta_key": "meta_value"},
37
+ embedding=[0.7, 0.8, 0.9],
38
+ score=0.8,
39
+ ),
33
40
  ]
34
41
 
35
42
  pg_docs = _from_haystack_to_pg_documents(haystack_docs)
@@ -64,6 +71,16 @@ def test_from_haystack_to_pg_documents():
64
71
  assert pg_docs[2]["embedding"] == [0.7, 0.8, 0.9]
65
72
  assert "score" not in pg_docs[2]
66
73
 
74
+ assert pg_docs[3]["id"] == "4"
75
+ assert pg_docs[3]["content"] == "This is another text"
76
+ assert pg_docs[3]["blob_data"] is None
77
+ assert pg_docs[3]["blob_meta"] is None
78
+ assert pg_docs[3]["blob_mime_type"] is None
79
+ assert "dataframe" not in pg_docs[3]
80
+ assert pg_docs[3]["meta"].obj == {"meta_key": "meta_value"}
81
+ assert pg_docs[3]["embedding"] == [0.7, 0.8, 0.9]
82
+ assert "score" not in pg_docs[3]
83
+
67
84
 
68
85
  def test_from_pg_to_haystack_documents():
69
86
  pg_docs = [
@@ -9,14 +9,30 @@ import pytest
9
9
  from haystack.dataclasses.document import ByteStream, Document
10
10
  from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
11
11
  from haystack.document_stores.types import DuplicatePolicy
12
- from haystack.testing.document_store import CountDocumentsTest, DeleteDocumentsTest, WriteDocumentsTest
12
+ from haystack.testing.document_store import (
13
+ CountDocumentsTest,
14
+ DeleteAllTest,
15
+ DeleteByFilterTest,
16
+ DeleteDocumentsTest,
17
+ FilterableDocsFixtureMixin,
18
+ UpdateByFilterTest,
19
+ WriteDocumentsTest,
20
+ )
13
21
  from haystack.utils import Secret
14
22
 
15
23
  from haystack_integrations.document_stores.pgvector import PgvectorDocumentStore
16
24
 
17
25
 
18
26
  @pytest.mark.integration
19
- class TestDocumentStore(CountDocumentsTest, WriteDocumentsTest, DeleteDocumentsTest):
27
+ class TestDocumentStore(
28
+ CountDocumentsTest,
29
+ DeleteAllTest,
30
+ DeleteByFilterTest,
31
+ DeleteDocumentsTest,
32
+ FilterableDocsFixtureMixin,
33
+ UpdateByFilterTest,
34
+ WriteDocumentsTest,
35
+ ):
20
36
  def test_write_documents(self, document_store: PgvectorDocumentStore):
21
37
  docs = [Document(id="1")]
22
38
  assert document_store.write_documents(docs) == 1
@@ -53,13 +69,6 @@ class TestDocumentStore(CountDocumentsTest, WriteDocumentsTest, DeleteDocumentsT
53
69
  same_connection = document_store._connection
54
70
  assert same_connection is document_store._connection
55
71
 
56
- def test_delete_all_documents(self, document_store: PgvectorDocumentStore) -> None:
57
- document_store.write_documents([Document(id=str(i)) for i in range(10)])
58
- document_store.delete_all_documents()
59
- assert document_store.count_documents() == 0
60
- document_store.write_documents([Document(id="1")])
61
- assert document_store.count_documents() == 1
62
-
63
72
  def test_invalid_connection_string(self, monkeypatch):
64
73
  monkeypatch.setenv("PG_CONN_STR", "invalid_connection_string")
65
74
  document_store = PgvectorDocumentStore()
@@ -262,215 +271,6 @@ def test_delete_table_first_call(document_store):
262
271
  document_store.delete_table() # if throw error, test fails
263
272
 
264
273
 
265
- @pytest.mark.integration
266
- def test_delete_by_filter(document_store: PgvectorDocumentStore):
267
- docs = [
268
- Document(content="Doc 1", meta={"category": "A", "year": 2023}),
269
- Document(content="Doc 2", meta={"category": "B", "year": 2023}),
270
- Document(content="Doc 3", meta={"category": "A", "year": 2024}),
271
- ]
272
- document_store.write_documents(docs)
273
- assert document_store.count_documents() == 3
274
-
275
- deleted_count = document_store.delete_by_filter(filters={"field": "meta.category", "operator": "==", "value": "A"})
276
- assert deleted_count == 2
277
- assert document_store.count_documents() == 1
278
-
279
- remaining_docs = document_store.filter_documents()
280
- assert len(remaining_docs) == 1
281
- assert remaining_docs[0].meta["category"] == "B"
282
-
283
- deleted_count = document_store.delete_by_filter(filters={"field": "meta.year", "operator": "==", "value": 2023})
284
- assert deleted_count == 1
285
- assert document_store.count_documents() == 0
286
-
287
-
288
- @pytest.mark.integration
289
- def test_delete_by_filter_no_matches(document_store: PgvectorDocumentStore):
290
- docs = [
291
- Document(content="Doc 1", meta={"category": "A"}),
292
- Document(content="Doc 2", meta={"category": "B"}),
293
- ]
294
- document_store.write_documents(docs)
295
- assert document_store.count_documents() == 2
296
-
297
- deleted_count = document_store.delete_by_filter(filters={"field": "meta.category", "operator": "==", "value": "C"})
298
- assert deleted_count == 0
299
- assert document_store.count_documents() == 2
300
-
301
-
302
- @pytest.mark.integration
303
- def test_delete_by_filter_advanced_filters(document_store: PgvectorDocumentStore):
304
- docs = [
305
- Document(content="Doc 1", meta={"category": "A", "year": 2023, "status": "draft"}),
306
- Document(content="Doc 2", meta={"category": "A", "year": 2024, "status": "published"}),
307
- Document(content="Doc 3", meta={"category": "B", "year": 2023, "status": "draft"}),
308
- ]
309
- document_store.write_documents(docs)
310
- assert document_store.count_documents() == 3
311
-
312
- # AND condition
313
- deleted_count = document_store.delete_by_filter(
314
- filters={
315
- "operator": "AND",
316
- "conditions": [
317
- {"field": "meta.category", "operator": "==", "value": "A"},
318
- {"field": "meta.year", "operator": "==", "value": 2023},
319
- ],
320
- }
321
- )
322
- assert deleted_count == 1
323
- assert document_store.count_documents() == 2
324
-
325
- # OR condition
326
- deleted_count = document_store.delete_by_filter(
327
- filters={
328
- "operator": "OR",
329
- "conditions": [
330
- {"field": "meta.category", "operator": "==", "value": "B"},
331
- {"field": "meta.status", "operator": "==", "value": "published"},
332
- ],
333
- }
334
- )
335
- assert deleted_count == 2
336
- assert document_store.count_documents() == 0
337
-
338
-
339
- @pytest.mark.integration
340
- def test_update_by_filter(document_store: PgvectorDocumentStore):
341
- docs = [
342
- Document(content="Doc 1", meta={"category": "A", "status": "draft"}),
343
- Document(content="Doc 2", meta={"category": "B", "status": "draft"}),
344
- Document(content="Doc 3", meta={"category": "A", "status": "draft"}),
345
- ]
346
- document_store.write_documents(docs)
347
- assert document_store.count_documents() == 3
348
-
349
- # update status for category="A" documents
350
- updated_count = document_store.update_by_filter(
351
- filters={"field": "meta.category", "operator": "==", "value": "A"}, meta={"status": "published"}
352
- )
353
- assert updated_count == 2
354
-
355
- # verify
356
- published_docs = document_store.filter_documents(
357
- filters={"field": "meta.status", "operator": "==", "value": "published"}
358
- )
359
- assert len(published_docs) == 2
360
- for doc in published_docs:
361
- assert doc.meta["category"] == "A"
362
- assert doc.meta["status"] == "published"
363
-
364
- # Verify category B still has draft status
365
- draft_docs = document_store.filter_documents(filters={"field": "meta.status", "operator": "==", "value": "draft"})
366
- assert len(draft_docs) == 1
367
- assert draft_docs[0].meta["category"] == "B"
368
-
369
-
370
- @pytest.mark.integration
371
- def test_update_by_filter_multiple_fields(document_store: PgvectorDocumentStore):
372
- docs = [
373
- Document(content="Doc 1", meta={"category": "A", "year": 2023}),
374
- Document(content="Doc 2", meta={"category": "A", "year": 2023}),
375
- Document(content="Doc 3", meta={"category": "B", "year": 2024}),
376
- ]
377
- document_store.write_documents(docs)
378
- assert document_store.count_documents() == 3
379
-
380
- # update multiple fields for category="A" documents
381
- updated_count = document_store.update_by_filter(
382
- filters={"field": "meta.category", "operator": "==", "value": "A"},
383
- meta={"status": "published", "priority": "high", "reviewed": True},
384
- )
385
- assert updated_count == 2
386
-
387
- # verify
388
- published_docs = document_store.filter_documents(filters={"field": "meta.category", "operator": "==", "value": "A"})
389
- assert len(published_docs) == 2
390
- for doc in published_docs:
391
- assert doc.meta["status"] == "published"
392
- assert doc.meta["priority"] == "high"
393
- assert doc.meta["reviewed"] is True
394
- assert doc.meta["year"] == 2023 # Original field should still be present
395
-
396
- # verify category B was not updated
397
- b_docs = document_store.filter_documents(filters={"field": "meta.category", "operator": "==", "value": "B"})
398
- assert len(b_docs) == 1
399
- assert "status" not in b_docs[0].meta
400
- assert "priority" not in b_docs[0].meta
401
-
402
-
403
- @pytest.mark.integration
404
- def test_update_by_filter_no_matches(document_store: PgvectorDocumentStore):
405
- docs = [
406
- Document(content="Doc 1", meta={"category": "A"}),
407
- Document(content="Doc 2", meta={"category": "B"}),
408
- ]
409
- document_store.write_documents(docs)
410
- assert document_store.count_documents() == 2
411
-
412
- # update documents with category="C" (no matches)
413
- updated_count = document_store.update_by_filter(
414
- filters={"field": "meta.category", "operator": "==", "value": "C"}, meta={"status": "published"}
415
- )
416
- assert updated_count == 0
417
- assert document_store.count_documents() == 2
418
-
419
- # verify no documents were updated
420
- published_docs = document_store.filter_documents(
421
- filters={"field": "meta.status", "operator": "==", "value": "published"}
422
- )
423
- assert len(published_docs) == 0
424
-
425
-
426
- @pytest.mark.integration
427
- def test_update_by_filter_advanced_filters(document_store: PgvectorDocumentStore):
428
- docs = [
429
- Document(content="Doc 1", meta={"category": "A", "year": 2023, "status": "draft"}),
430
- Document(content="Doc 2", meta={"category": "A", "year": 2024, "status": "draft"}),
431
- Document(content="Doc 3", meta={"category": "B", "year": 2023, "status": "draft"}),
432
- ]
433
- document_store.write_documents(docs)
434
- assert document_store.count_documents() == 3
435
-
436
- # AND condition
437
- updated_count = document_store.update_by_filter(
438
- filters={
439
- "operator": "AND",
440
- "conditions": [
441
- {"field": "meta.category", "operator": "==", "value": "A"},
442
- {"field": "meta.year", "operator": "==", "value": 2023},
443
- ],
444
- },
445
- meta={"status": "published"},
446
- )
447
- assert updated_count == 1
448
-
449
- # verify only one document was updated
450
- published_docs = document_store.filter_documents(
451
- filters={"field": "meta.status", "operator": "==", "value": "published"}
452
- )
453
- assert len(published_docs) == 1
454
- assert published_docs[0].meta["category"] == "A"
455
- assert published_docs[0].meta["year"] == 2023
456
-
457
- # OR condition
458
- updated_count = document_store.update_by_filter(
459
- filters={
460
- "operator": "OR",
461
- "conditions": [
462
- {"field": "meta.category", "operator": "==", "value": "B"},
463
- {"field": "meta.year", "operator": "==", "value": 2024},
464
- ],
465
- },
466
- meta={"featured": True},
467
- )
468
- assert updated_count == 2
469
-
470
- featured_docs = document_store.filter_documents(filters={"field": "meta.featured", "operator": "==", "value": True})
471
- assert len(featured_docs) == 2
472
-
473
-
474
274
  @pytest.mark.integration
475
275
  def test_update_by_filter_empty_meta_raises_error(document_store: PgvectorDocumentStore):
476
276
  docs = [Document(content="Doc 1", meta={"category": "A"})]
@@ -144,6 +144,11 @@ def test_treat_meta_field():
144
144
  assert _treat_meta_field(field="meta.name", value=None) == "meta->>'name'"
145
145
 
146
146
 
147
+ def test_treat_meta_field_rejects_unsafe_metadata_key():
148
+ with pytest.raises(FilterError, match="Invalid metadata field name"):
149
+ _treat_meta_field(field="meta.name' OR 1=1 --", value="x")
150
+
151
+
147
152
  def test_comparison_condition_missing_operator():
148
153
  condition = {"field": "meta.type", "value": "article"}
149
154
  with pytest.raises(FilterError):
@@ -1,30 +0,0 @@
1
- loaders:
2
- - ignore_when_discovered:
3
- - __init__
4
- modules:
5
- - haystack_integrations.components.retrievers.pgvector.embedding_retriever
6
- - haystack_integrations.components.retrievers.pgvector.keyword_retriever
7
- - haystack_integrations.document_stores.pgvector.document_store
8
- search_path:
9
- - ../src
10
- type: haystack_pydoc_tools.loaders.CustomPythonLoader
11
- processors:
12
- - do_not_filter_modules: false
13
- documented_only: true
14
- expression: null
15
- skip_empty_modules: true
16
- type: filter
17
- - type: smart
18
- - type: crossref
19
- renderer:
20
- description: Pgvector integration for Haystack
21
- id: integrations-pgvector
22
- markdown:
23
- add_member_class_prefix: false
24
- add_method_class_prefix: true
25
- classdef_code_block: false
26
- descriptive_class_title: false
27
- descriptive_module_title: true
28
- filename: pgvector.md
29
- title: Pgvector
30
- type: haystack_pydoc_tools.renderers.DocusaurusRenderer