pgvector-haystack 0.2.0__tar.gz → 0.3.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.
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/PKG-INFO +1 -1
- pgvector_haystack-0.3.0/examples/hybrid_retrieval.py +69 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/pydoc/config.yml +1 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/src/haystack_integrations/document_stores/pgvector/document_store.py +50 -23
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/conftest.py +0 -1
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/test_document_store.py +6 -9
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/.gitignore +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/LICENSE.txt +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/README.md +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/examples/embedding_retrieval.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/pyproject.toml +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/src/haystack_integrations/document_stores/pgvector/filters.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/__init__.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/test_embedding_retrieval.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/test_filters.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/test_keyword_retrieval.py +0 -0
- {pgvector_haystack-0.2.0 → pgvector_haystack-0.3.0}/tests/test_retrievers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pgvector-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Before running this example, ensure you have PostgreSQL installed with the pgvector extension.
|
|
2
|
+
# For a quick setup using Docker:
|
|
3
|
+
# docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres
|
|
4
|
+
# -e POSTGRES_DB=postgres ankane/pgvector
|
|
5
|
+
|
|
6
|
+
# Install required packages for this example, including pgvector-haystack and other libraries needed
|
|
7
|
+
# for Markdown conversion and embeddings generation. Use the following command:
|
|
8
|
+
# pip install pgvector-haystack markdown-it-py mdit_plain "sentence-transformers>=2.2.0"
|
|
9
|
+
|
|
10
|
+
# Download some Markdown files to index.
|
|
11
|
+
# git clone https://github.com/anakin87/neural-search-pills
|
|
12
|
+
|
|
13
|
+
import glob
|
|
14
|
+
|
|
15
|
+
from haystack import Pipeline
|
|
16
|
+
from haystack.components.converters import MarkdownToDocument
|
|
17
|
+
from haystack.components.embedders import SentenceTransformersDocumentEmbedder, SentenceTransformersTextEmbedder
|
|
18
|
+
from haystack.components.joiners import DocumentJoiner
|
|
19
|
+
from haystack.components.preprocessors import DocumentSplitter
|
|
20
|
+
from haystack.components.writers import DocumentWriter
|
|
21
|
+
from haystack_integrations.components.retrievers.pgvector import PgvectorEmbeddingRetriever, PgvectorKeywordRetriever
|
|
22
|
+
from haystack_integrations.document_stores.pgvector import PgvectorDocumentStore
|
|
23
|
+
|
|
24
|
+
# Set an environment variable `PG_CONN_STR` with the connection string to your PostgreSQL database.
|
|
25
|
+
# e.g., "postgresql://USER:PASSWORD@HOST:PORT/DB_NAME"
|
|
26
|
+
|
|
27
|
+
# Initialize PgvectorDocumentStore
|
|
28
|
+
document_store = PgvectorDocumentStore(
|
|
29
|
+
table_name="haystack_test",
|
|
30
|
+
embedding_dimension=768,
|
|
31
|
+
vector_function="cosine_similarity",
|
|
32
|
+
recreate_table=True,
|
|
33
|
+
search_strategy="hnsw",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Create the indexing Pipeline and index some documents
|
|
37
|
+
file_paths = glob.glob("neural-search-pills/pills/*.md")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
indexing = Pipeline()
|
|
41
|
+
indexing.add_component("converter", MarkdownToDocument())
|
|
42
|
+
indexing.add_component("splitter", DocumentSplitter(split_by="sentence", split_length=2))
|
|
43
|
+
indexing.add_component("document_embedder", SentenceTransformersDocumentEmbedder())
|
|
44
|
+
indexing.add_component("writer", DocumentWriter(document_store))
|
|
45
|
+
indexing.connect("converter", "splitter")
|
|
46
|
+
indexing.connect("splitter", "document_embedder")
|
|
47
|
+
indexing.connect("document_embedder", "writer")
|
|
48
|
+
|
|
49
|
+
indexing.run({"converter": {"sources": file_paths}})
|
|
50
|
+
|
|
51
|
+
# Create the querying Pipeline and try a query
|
|
52
|
+
querying = Pipeline()
|
|
53
|
+
querying.add_component("text_embedder", SentenceTransformersTextEmbedder())
|
|
54
|
+
querying.add_component("retriever", PgvectorEmbeddingRetriever(document_store=document_store, top_k=3))
|
|
55
|
+
querying.add_component("keyword_retriever", PgvectorKeywordRetriever(document_store=document_store, top_k=3))
|
|
56
|
+
querying.add_component(
|
|
57
|
+
"joiner",
|
|
58
|
+
DocumentJoiner(join_mode="reciprocal_rank_fusion", top_k=3),
|
|
59
|
+
)
|
|
60
|
+
querying.connect("text_embedder", "retriever")
|
|
61
|
+
querying.connect("keyword_retriever", "joiner")
|
|
62
|
+
querying.connect("retriever", "joiner")
|
|
63
|
+
|
|
64
|
+
query = "cross-encoder"
|
|
65
|
+
results = querying.run({"text_embedder": {"text": query}, "keyword_retriever": {"query": query}})
|
|
66
|
+
|
|
67
|
+
for doc in results["joiner"]["documents"]:
|
|
68
|
+
print(doc)
|
|
69
|
+
print("-" * 10)
|
|
@@ -3,6 +3,7 @@ loaders:
|
|
|
3
3
|
search_path: [../src]
|
|
4
4
|
modules: [
|
|
5
5
|
"haystack_integrations.components.retrievers.pgvector.embedding_retriever",
|
|
6
|
+
"haystack_integrations.components.retrievers.pgvector.keyword_retriever",
|
|
6
7
|
"haystack_integrations.document_stores.pgvector.document_store",
|
|
7
8
|
]
|
|
8
9
|
ignore_when_discovered: ["__init__"]
|
|
@@ -145,26 +145,53 @@ class PgvectorDocumentStore:
|
|
|
145
145
|
self.hnsw_index_creation_kwargs = hnsw_index_creation_kwargs or {}
|
|
146
146
|
self.hnsw_ef_search = hnsw_ef_search
|
|
147
147
|
self.language = language
|
|
148
|
+
self._connection = None
|
|
149
|
+
self._cursor = None
|
|
150
|
+
self._dict_cursor = None
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
self.
|
|
152
|
+
@property
|
|
153
|
+
def cursor(self):
|
|
154
|
+
if self._cursor is None:
|
|
155
|
+
self._create_connection()
|
|
156
|
+
|
|
157
|
+
return self._cursor
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def dict_cursor(self):
|
|
161
|
+
if self._dict_cursor is None:
|
|
162
|
+
self._create_connection()
|
|
163
|
+
|
|
164
|
+
return self._dict_cursor
|
|
152
165
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
self.
|
|
166
|
+
@property
|
|
167
|
+
def connection(self):
|
|
168
|
+
if self._connection is None:
|
|
169
|
+
self._create_connection()
|
|
156
170
|
|
|
171
|
+
return self._connection
|
|
172
|
+
|
|
173
|
+
def _create_connection(self):
|
|
174
|
+
conn_str = self.connection_string.resolve_value() or ""
|
|
175
|
+
connection = connect(conn_str)
|
|
176
|
+
connection.autocommit = True
|
|
157
177
|
connection.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
|
158
|
-
register_vector(connection)
|
|
178
|
+
register_vector(connection) # Note: this must be called before creating the cursors.
|
|
159
179
|
|
|
160
|
-
|
|
180
|
+
self._connection = connection
|
|
181
|
+
self._cursor = self._connection.cursor()
|
|
182
|
+
self._dict_cursor = self._connection.cursor(row_factory=dict_row)
|
|
183
|
+
|
|
184
|
+
# Init schema
|
|
185
|
+
if self.recreate_table:
|
|
161
186
|
self.delete_table()
|
|
162
187
|
self._create_table_if_not_exists()
|
|
163
188
|
self._create_keyword_index_if_not_exists()
|
|
164
189
|
|
|
165
|
-
if search_strategy == "hnsw":
|
|
190
|
+
if self.search_strategy == "hnsw":
|
|
166
191
|
self._handle_hnsw()
|
|
167
192
|
|
|
193
|
+
return self._connection
|
|
194
|
+
|
|
168
195
|
def to_dict(self) -> Dict[str, Any]:
|
|
169
196
|
"""
|
|
170
197
|
Serializes the component to a dictionary.
|
|
@@ -208,11 +235,11 @@ class PgvectorDocumentStore:
|
|
|
208
235
|
:param sql_query: The SQL query to execute.
|
|
209
236
|
:param params: The parameters to pass to the SQL query.
|
|
210
237
|
:param error_msg: The error message to use if an exception is raised.
|
|
211
|
-
:param cursor: The cursor to use to execute the SQL query. Defaults to self.
|
|
238
|
+
:param cursor: The cursor to use to execute the SQL query. Defaults to self.cursor.
|
|
212
239
|
"""
|
|
213
240
|
|
|
214
241
|
params = params or ()
|
|
215
|
-
cursor = cursor or self.
|
|
242
|
+
cursor = cursor or self.cursor
|
|
216
243
|
|
|
217
244
|
sql_query_str = sql_query.as_string(cursor) if not isinstance(sql_query, str) else sql_query
|
|
218
245
|
logger.debug("SQL query: %s\nParameters: %s", sql_query_str, params)
|
|
@@ -220,7 +247,7 @@ class PgvectorDocumentStore:
|
|
|
220
247
|
try:
|
|
221
248
|
result = cursor.execute(sql_query, params)
|
|
222
249
|
except Error as e:
|
|
223
|
-
self.
|
|
250
|
+
self.connection.rollback()
|
|
224
251
|
detailed_error_msg = f"{error_msg}.\nYou can find the SQL query and the parameters in the debug logs."
|
|
225
252
|
raise DocumentStoreError(detailed_error_msg) from e
|
|
226
253
|
|
|
@@ -371,7 +398,7 @@ class PgvectorDocumentStore:
|
|
|
371
398
|
sql_filter,
|
|
372
399
|
params,
|
|
373
400
|
error_msg="Could not filter documents from PgvectorDocumentStore.",
|
|
374
|
-
cursor=self.
|
|
401
|
+
cursor=self.dict_cursor,
|
|
375
402
|
)
|
|
376
403
|
|
|
377
404
|
records = result.fetchall()
|
|
@@ -408,16 +435,16 @@ class PgvectorDocumentStore:
|
|
|
408
435
|
|
|
409
436
|
sql_insert += SQL(" RETURNING id")
|
|
410
437
|
|
|
411
|
-
sql_query_str = sql_insert.as_string(self.
|
|
438
|
+
sql_query_str = sql_insert.as_string(self.cursor) if not isinstance(sql_insert, str) else sql_insert
|
|
412
439
|
logger.debug("SQL query: %s\nParameters: %s", sql_query_str, db_documents)
|
|
413
440
|
|
|
414
441
|
try:
|
|
415
|
-
self.
|
|
442
|
+
self.cursor.executemany(sql_insert, db_documents, returning=True)
|
|
416
443
|
except IntegrityError as ie:
|
|
417
|
-
self.
|
|
444
|
+
self.connection.rollback()
|
|
418
445
|
raise DuplicateDocumentError from ie
|
|
419
446
|
except Error as e:
|
|
420
|
-
self.
|
|
447
|
+
self.connection.rollback()
|
|
421
448
|
error_msg = (
|
|
422
449
|
"Could not write documents to PgvectorDocumentStore. \n"
|
|
423
450
|
"You can find the SQL query and the parameters in the debug logs."
|
|
@@ -428,9 +455,9 @@ class PgvectorDocumentStore:
|
|
|
428
455
|
# https://www.psycopg.org/psycopg3/docs/api/cursors.html#psycopg.Cursor.executemany
|
|
429
456
|
written_docs = 0
|
|
430
457
|
while True:
|
|
431
|
-
if self.
|
|
458
|
+
if self.cursor.fetchone():
|
|
432
459
|
written_docs += 1
|
|
433
|
-
if not self.
|
|
460
|
+
if not self.cursor.nextset():
|
|
434
461
|
break
|
|
435
462
|
|
|
436
463
|
return written_docs
|
|
@@ -483,8 +510,8 @@ class PgvectorDocumentStore:
|
|
|
483
510
|
|
|
484
511
|
# postgresql returns the embedding as a string
|
|
485
512
|
# so we need to convert it to a list of floats
|
|
486
|
-
if document.get("embedding"):
|
|
487
|
-
haystack_dict["embedding"] =
|
|
513
|
+
if document.get("embedding") is not None:
|
|
514
|
+
haystack_dict["embedding"] = document["embedding"].tolist()
|
|
488
515
|
|
|
489
516
|
haystack_document = Document.from_dict(haystack_dict)
|
|
490
517
|
|
|
@@ -555,7 +582,7 @@ class PgvectorDocumentStore:
|
|
|
555
582
|
sql_query,
|
|
556
583
|
(query, *where_params),
|
|
557
584
|
error_msg="Could not retrieve documents from PgvectorDocumentStore.",
|
|
558
|
-
cursor=self.
|
|
585
|
+
cursor=self.dict_cursor,
|
|
559
586
|
)
|
|
560
587
|
|
|
561
588
|
records = result.fetchall()
|
|
@@ -633,7 +660,7 @@ class PgvectorDocumentStore:
|
|
|
633
660
|
sql_query,
|
|
634
661
|
params,
|
|
635
662
|
error_msg="Could not retrieve documents from PgvectorDocumentStore.",
|
|
636
|
-
cursor=self.
|
|
663
|
+
cursor=self.dict_cursor,
|
|
637
664
|
)
|
|
638
665
|
|
|
639
666
|
records = result.fetchall()
|
|
@@ -40,7 +40,6 @@ def patches_for_unit_tests():
|
|
|
40
40
|
) as mock_create_kw_index, patch(
|
|
41
41
|
"haystack_integrations.document_stores.pgvector.document_store.PgvectorDocumentStore._handle_hnsw"
|
|
42
42
|
) as mock_hnsw:
|
|
43
|
-
|
|
44
43
|
yield mock_connect, mock_register, mock_delete, mock_create, mock_create_kw_index, mock_hnsw
|
|
45
44
|
|
|
46
45
|
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
from unittest.mock import patch
|
|
6
6
|
|
|
7
|
+
import numpy as np
|
|
7
8
|
import pytest
|
|
8
9
|
from haystack.dataclasses.document import ByteStream, Document
|
|
9
10
|
from haystack.document_stores.errors import DuplicateDocumentError
|
|
10
11
|
from haystack.document_stores.types import DuplicatePolicy
|
|
11
12
|
from haystack.testing.document_store import CountDocumentsTest, DeleteDocumentsTest, WriteDocumentsTest
|
|
13
|
+
from haystack.utils import Secret
|
|
12
14
|
from haystack_integrations.document_stores.pgvector import PgvectorDocumentStore
|
|
13
15
|
from pandas import DataFrame
|
|
14
16
|
|
|
@@ -170,7 +172,7 @@ def test_from_pg_to_haystack_documents():
|
|
|
170
172
|
"blob_meta": None,
|
|
171
173
|
"blob_mime_type": None,
|
|
172
174
|
"meta": {"meta_key": "meta_value"},
|
|
173
|
-
"embedding":
|
|
175
|
+
"embedding": np.array([0.1, 0.2, 0.3]),
|
|
174
176
|
},
|
|
175
177
|
{
|
|
176
178
|
"id": "2",
|
|
@@ -180,7 +182,7 @@ def test_from_pg_to_haystack_documents():
|
|
|
180
182
|
"blob_meta": None,
|
|
181
183
|
"blob_mime_type": None,
|
|
182
184
|
"meta": {"meta_key": "meta_value"},
|
|
183
|
-
"embedding":
|
|
185
|
+
"embedding": np.array([0.4, 0.5, 0.6]),
|
|
184
186
|
},
|
|
185
187
|
{
|
|
186
188
|
"id": "3",
|
|
@@ -190,16 +192,11 @@ def test_from_pg_to_haystack_documents():
|
|
|
190
192
|
"blob_meta": {"blob_meta_key": "blob_meta_value"},
|
|
191
193
|
"blob_mime_type": "mime_type",
|
|
192
194
|
"meta": {"meta_key": "meta_value"},
|
|
193
|
-
"embedding":
|
|
195
|
+
"embedding": np.array([0.7, 0.8, 0.9]),
|
|
194
196
|
},
|
|
195
197
|
]
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
"haystack_integrations.document_stores.pgvector.document_store.PgvectorDocumentStore.__init__"
|
|
199
|
-
) as mock_init:
|
|
200
|
-
mock_init.return_value = None
|
|
201
|
-
ds = PgvectorDocumentStore(connection_string="test")
|
|
202
|
-
|
|
199
|
+
ds = PgvectorDocumentStore(connection_string=Secret.from_token("test"))
|
|
203
200
|
haystack_docs = ds._from_pg_to_haystack_documents(pg_docs)
|
|
204
201
|
|
|
205
202
|
assert haystack_docs[0].id == "1"
|
|
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
|