pgvector-haystack 0.0.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -2,5 +2,6 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
  from .embedding_retriever import PgvectorEmbeddingRetriever
5
+ from .keyword_retriever import PgvectorKeywordRetriever
5
6
 
6
- __all__ = ["PgvectorEmbeddingRetriever"]
7
+ __all__ = ["PgvectorEmbeddingRetriever", "PgvectorKeywordRetriever"]
@@ -12,9 +12,47 @@ from haystack_integrations.document_stores.pgvector.document_store import VALID_
12
12
  @component
13
13
  class PgvectorEmbeddingRetriever:
14
14
  """
15
- Retrieves documents from the PgvectorDocumentStore, based on their dense embeddings.
15
+ Retrieves documents from the `PgvectorDocumentStore`, based on their dense embeddings.
16
16
 
17
- Needs to be connected to the PgvectorDocumentStore.
17
+ Example usage:
18
+ ```python
19
+ from haystack.document_stores import DuplicatePolicy
20
+ from haystack import Document, Pipeline
21
+ from haystack.components.embedders import SentenceTransformersTextEmbedder, SentenceTransformersDocumentEmbedder
22
+
23
+ from haystack_integrations.document_stores.pgvector import PgvectorDocumentStore
24
+ from haystack_integrations.components.retrievers.pgvector import PgvectorEmbeddingRetriever
25
+
26
+ # Set an environment variable `PG_CONN_STR` with the connection string to your PostgreSQL database.
27
+ # e.g., "postgresql://USER:PASSWORD@HOST:PORT/DB_NAME"
28
+
29
+ document_store = PgvectorDocumentStore(
30
+ embedding_dimension=768,
31
+ vector_function="cosine_similarity",
32
+ recreate_table=True,
33
+ )
34
+
35
+ documents = [Document(content="There are over 7,000 languages spoken around the world today."),
36
+ Document(content="Elephants have been observed to behave in a way that indicates..."),
37
+ Document(content="In certain places, you can witness the phenomenon of bioluminescent waves.")]
38
+
39
+ document_embedder = SentenceTransformersDocumentEmbedder()
40
+ document_embedder.warm_up()
41
+ documents_with_embeddings = document_embedder.run(documents)
42
+
43
+ document_store.write_documents(documents_with_embeddings.get("documents"), policy=DuplicatePolicy.OVERWRITE)
44
+
45
+ query_pipeline = Pipeline()
46
+ query_pipeline.add_component("text_embedder", SentenceTransformersTextEmbedder())
47
+ query_pipeline.add_component("retriever", PgvectorEmbeddingRetriever(document_store=document_store))
48
+ query_pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
49
+
50
+ query = "How many languages are there?"
51
+
52
+ res = query_pipeline.run({"text_embedder": {"text": query}})
53
+
54
+ assert res['retriever']['documents'][0].content == "There are over 7,000 languages spoken around the world today."
55
+ ```
18
56
  """
19
57
 
20
58
  def __init__(
@@ -26,23 +64,20 @@ class PgvectorEmbeddingRetriever:
26
64
  vector_function: Optional[Literal["cosine_similarity", "inner_product", "l2_distance"]] = None,
27
65
  ):
28
66
  """
29
- Create the PgvectorEmbeddingRetriever component.
30
-
31
- :param document_store: An instance of PgvectorDocumentStore.
32
- :param filters: Filters applied to the retrieved Documents. Defaults to None.
33
- :param top_k: Maximum number of Documents to return, defaults to 10.
67
+ :param document_store: An instance of `PgvectorDocumentStore`.
68
+ :param filters: Filters applied to the retrieved Documents.
69
+ :param top_k: Maximum number of Documents to return.
34
70
  :param vector_function: The similarity function to use when searching for similar embeddings.
35
71
  Defaults to the one set in the `document_store` instance.
36
- "cosine_similarity" and "inner_product" are similarity functions and
72
+ `"cosine_similarity"` and `"inner_product"` are similarity functions and
37
73
  higher scores indicate greater similarity between the documents.
38
- "l2_distance" returns the straight-line distance between vectors,
74
+ `"l2_distance"` returns the straight-line distance between vectors,
39
75
  and the most similar documents are the ones with the smallest score.
40
-
41
- Important: if the document store is using the "hnsw" search strategy, the vector function
76
+ **Important**: if the document store is using the `"hnsw"` search strategy, the vector function
42
77
  should match the one utilized during index creation to take advantage of the index.
43
- :type vector_function: Literal["cosine_similarity", "inner_product", "l2_distance"]
44
78
 
45
- :raises ValueError: If `document_store` is not an instance of PgvectorDocumentStore.
79
+ :raises ValueError: If `document_store` is not an instance of `PgvectorDocumentStore` or if `vector_function`
80
+ is not one of the valid options.
46
81
  """
47
82
  if not isinstance(document_store, PgvectorDocumentStore):
48
83
  msg = "document_store must be an instance of PgvectorDocumentStore"
@@ -58,6 +93,12 @@ class PgvectorEmbeddingRetriever:
58
93
  self.vector_function = vector_function or document_store.vector_function
59
94
 
60
95
  def to_dict(self) -> Dict[str, Any]:
96
+ """
97
+ Serializes the component to a dictionary.
98
+
99
+ :returns:
100
+ Dictionary with serialized data.
101
+ """
61
102
  return default_to_dict(
62
103
  self,
63
104
  filters=self.filters,
@@ -68,9 +109,16 @@ class PgvectorEmbeddingRetriever:
68
109
 
69
110
  @classmethod
70
111
  def from_dict(cls, data: Dict[str, Any]) -> "PgvectorEmbeddingRetriever":
71
- data["init_parameters"]["document_store"] = default_from_dict(
72
- PgvectorDocumentStore, data["init_parameters"]["document_store"]
73
- )
112
+ """
113
+ Deserializes the component from a dictionary.
114
+
115
+ :param data:
116
+ Dictionary to deserialize from.
117
+ :returns:
118
+ Deserialized component.
119
+ """
120
+ doc_store_params = data["init_parameters"]["document_store"]
121
+ data["init_parameters"]["document_store"] = PgvectorDocumentStore.from_dict(doc_store_params)
74
122
  return default_from_dict(cls, data)
75
123
 
76
124
  @component.output_types(documents=List[Document])
@@ -82,14 +130,14 @@ class PgvectorEmbeddingRetriever:
82
130
  vector_function: Optional[Literal["cosine_similarity", "inner_product", "l2_distance"]] = None,
83
131
  ):
84
132
  """
85
- Retrieve documents from the PgvectorDocumentStore, based on their embeddings.
133
+ Retrieve documents from the `PgvectorDocumentStore`, based on their embeddings.
86
134
 
87
135
  :param query_embedding: Embedding of the query.
88
136
  :param filters: Filters applied to the retrieved Documents.
89
137
  :param top_k: Maximum number of Documents to return.
90
138
  :param vector_function: The similarity function to use when searching for similar embeddings.
91
- :type vector_function: Literal["cosine_similarity", "inner_product", "l2_distance"]
92
- :return: List of Documents similar to `query_embedding`.
139
+
140
+ :returns: List of Documents similar to `query_embedding`.
93
141
  """
94
142
  filters = filters or self.filters
95
143
  top_k = top_k or self.top_k
@@ -0,0 +1,123 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from haystack import component, default_from_dict, default_to_dict
7
+ from haystack.dataclasses import Document
8
+ from haystack_integrations.document_stores.pgvector import PgvectorDocumentStore
9
+
10
+
11
+ @component
12
+ class PgvectorKeywordRetriever:
13
+ """
14
+ Retrieve documents from the `PgvectorDocumentStore`, based on keywords.
15
+
16
+ To rank the documents, the `ts_rank_cd` function of PostgreSQL is used.
17
+ It considers how often the query terms appear in the document, how close together the terms are in the document,
18
+ and how important is the part of the document where they occur.
19
+ For more details, see
20
+ [Postgres documentation](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING).
21
+
22
+ Usage example:
23
+ ```python
24
+ from haystack.document_stores import DuplicatePolicy
25
+ from haystack import Document
26
+
27
+ from haystack_integrations.document_stores.pgvector import PgvectorDocumentStore
28
+ from haystack_integrations.components.retrievers.pgvector import PgvectorKeywordRetriever
29
+
30
+ # Set an environment variable `PG_CONN_STR` with the connection string to your PostgreSQL database.
31
+ # e.g., "postgresql://USER:PASSWORD@HOST:PORT/DB_NAME"
32
+
33
+ document_store = PgvectorDocumentStore(language="english", recreate_table=True)
34
+
35
+ documents = [Document(content="There are over 7,000 languages spoken around the world today."),
36
+ Document(content="Elephants have been observed to behave in a way that indicates..."),
37
+ Document(content="In certain places, you can witness the phenomenon of bioluminescent waves.")]
38
+
39
+ document_store.write_documents(documents_with_embeddings.get("documents"), policy=DuplicatePolicy.OVERWRITE)
40
+
41
+ retriever = PgvectorKeywordRetriever(document_store=document_store)
42
+
43
+ result = retriever.run(query="languages")
44
+
45
+ assert res['retriever']['documents'][0].content == "There are over 7,000 languages spoken around the world today."
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ *,
51
+ document_store: PgvectorDocumentStore,
52
+ filters: Optional[Dict[str, Any]] = None,
53
+ top_k: int = 10,
54
+ ):
55
+ """
56
+ :param document_store: An instance of `PgvectorDocumentStore`.
57
+ :param filters: Filters applied to the retrieved Documents.
58
+ :param top_k: Maximum number of Documents to return.
59
+
60
+ :raises ValueError: If `document_store` is not an instance of `PgvectorDocumentStore`.
61
+ """
62
+ if not isinstance(document_store, PgvectorDocumentStore):
63
+ msg = "document_store must be an instance of PgvectorDocumentStore"
64
+ raise ValueError(msg)
65
+
66
+ self.document_store = document_store
67
+ self.filters = filters or {}
68
+ self.top_k = top_k
69
+
70
+ def to_dict(self) -> Dict[str, Any]:
71
+ """
72
+ Serializes the component to a dictionary.
73
+
74
+ :returns:
75
+ Dictionary with serialized data.
76
+ """
77
+ return default_to_dict(
78
+ self,
79
+ filters=self.filters,
80
+ top_k=self.top_k,
81
+ document_store=self.document_store.to_dict(),
82
+ )
83
+
84
+ @classmethod
85
+ def from_dict(cls, data: Dict[str, Any]) -> "PgvectorKeywordRetriever":
86
+ """
87
+ Deserializes the component from a dictionary.
88
+
89
+ :param data:
90
+ Dictionary to deserialize from.
91
+ :returns:
92
+ Deserialized component.
93
+ """
94
+ doc_store_params = data["init_parameters"]["document_store"]
95
+ data["init_parameters"]["document_store"] = PgvectorDocumentStore.from_dict(doc_store_params)
96
+ return default_from_dict(cls, data)
97
+
98
+ @component.output_types(documents=List[Document])
99
+ def run(
100
+ self,
101
+ query: str,
102
+ filters: Optional[Dict[str, Any]] = None,
103
+ top_k: Optional[int] = None,
104
+ ):
105
+ """
106
+ Retrieve documents from the `PgvectorDocumentStore`, based on keywords.
107
+
108
+ :param query: String to search in `Document`s' content.
109
+ :param filters: Filters applied to the retrieved Documents.
110
+ :param top_k: Maximum number of Documents to return.
111
+
112
+ :returns: A dictionary with the following keys:
113
+ - `documents`: List of `Document`s that match the query.
114
+ """
115
+ filters = filters or self.filters
116
+ top_k = top_k or self.top_k
117
+
118
+ docs = self.document_store._keyword_retrieval(
119
+ query=query,
120
+ filters=filters,
121
+ top_k=top_k,
122
+ )
123
+ return {"documents": docs}
@@ -4,10 +4,11 @@
4
4
  import logging
5
5
  from typing import Any, Dict, List, Literal, Optional
6
6
 
7
- from haystack import default_to_dict
7
+ from haystack import default_from_dict, default_to_dict
8
8
  from haystack.dataclasses.document import ByteStream, Document
9
9
  from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
10
10
  from haystack.document_stores.types import DuplicatePolicy
11
+ from haystack.utils.auth import Secret, deserialize_secrets_inplace
11
12
  from haystack.utils.filters import convert
12
13
  from psycopg import Error, IntegrityError, connect
13
14
  from psycopg.abc import Query
@@ -52,6 +53,12 @@ blob_mime_type = EXCLUDED.blob_mime_type,
52
53
  meta = EXCLUDED.meta
53
54
  """
54
55
 
56
+ KEYWORD_QUERY = """
57
+ SELECT {table_name}.*, ts_rank_cd(to_tsvector({language}, content), query) AS score
58
+ FROM {table_name}, plainto_tsquery({language}, %s) query
59
+ WHERE to_tsvector({language}, content) @@ query
60
+ """
61
+
55
62
  VALID_VECTOR_FUNCTIONS = ["cosine_similarity", "inner_product", "l2_distance"]
56
63
 
57
64
  VECTOR_FUNCTION_TO_POSTGRESQL_OPS = {
@@ -64,13 +71,20 @@ HNSW_INDEX_CREATION_VALID_KWARGS = ["m", "ef_construction"]
64
71
 
65
72
  HNSW_INDEX_NAME = "haystack_hnsw_index"
66
73
 
74
+ KEYWORD_INDEX_NAME = "haystack_keyword_index"
75
+
67
76
 
68
77
  class PgvectorDocumentStore:
78
+ """
79
+ A Document Store using PostgreSQL with the [pgvector extension](https://github.com/pgvector/pgvector) installed.
80
+ """
81
+
69
82
  def __init__(
70
83
  self,
71
84
  *,
72
- connection_string: str,
85
+ connection_string: Secret = Secret.from_env_var("PG_CONN_STR"),
73
86
  table_name: str = "haystack_documents",
87
+ language: str = "english",
74
88
  embedding_dimension: int = 768,
75
89
  vector_function: Literal["cosine_similarity", "inner_product", "l2_distance"] = "cosine_similarity",
76
90
  recreate_table: bool = False,
@@ -84,37 +98,38 @@ class PgvectorDocumentStore:
84
98
  It is meant to be connected to a PostgreSQL database with the pgvector extension installed.
85
99
  A specific table to store Haystack documents will be created if it doesn't exist yet.
86
100
 
87
- :param connection_string: The connection string to use to connect to the PostgreSQL database.
88
- e.g. "postgresql://USER:PASSWORD@HOST:PORT/DB_NAME"
89
- :param table_name: The name of the table to use to store Haystack documents. Defaults to "haystack_documents".
90
- :param embedding_dimension: The dimension of the embedding. Defaults to 768.
101
+ :param connection_string: The connection string to use to connect to the PostgreSQL database, defined as an
102
+ environment variable, e.g.: `PG_CONN_STR="postgresql://USER:PASSWORD@HOST:PORT/DB_NAME"`
103
+ :param table_name: The name of the table to use to store Haystack documents.
104
+ :param language: The language to be used to parse query and document content in keyword retrieval.
105
+ To see the list of available languages, you can run the following SQL query in your PostgreSQL database:
106
+ `SELECT cfgname FROM pg_ts_config;`.
107
+ More information can be found in this [StackOverflow answer](https://stackoverflow.com/a/39752553).
108
+ :param embedding_dimension: The dimension of the embedding.
91
109
  :param vector_function: The similarity function to use when searching for similar embeddings.
92
- Defaults to "cosine_similarity". "cosine_similarity" and "inner_product" are similarity functions and
110
+ `"cosine_similarity"` and `"inner_product"` are similarity functions and
93
111
  higher scores indicate greater similarity between the documents.
94
- "l2_distance" returns the straight-line distance between vectors,
112
+ `"l2_distance"` returns the straight-line distance between vectors,
95
113
  and the most similar documents are the ones with the smallest score.
96
-
97
- Important: when using the "hnsw" search strategy, an index will be created that depends on the
114
+ **Important**: when using the `"hnsw"` search strategy, an index will be created that depends on the
98
115
  `vector_function` passed here. Make sure subsequent queries will keep using the same
99
116
  vector similarity function in order to take advantage of the index.
100
- :type vector_function: Literal["cosine_similarity", "inner_product", "l2_distance"]
101
- :param recreate_table: Whether to recreate the table if it already exists. Defaults to False.
117
+ :param recreate_table: Whether to recreate the table if it already exists.
102
118
  :param search_strategy: The search strategy to use when searching for similar embeddings.
103
- Defaults to "exact_nearest_neighbor". "hnsw" is an approximate nearest neighbor search strategy,
119
+ `"exact_nearest_neighbor"` provides perfect recall but can be slow for large numbers of documents.
120
+ `"hnsw"` is an approximate nearest neighbor search strategy,
104
121
  which trades off some accuracy for speed; it is recommended for large numbers of documents.
105
-
106
- Important: when using the "hnsw" search strategy, an index will be created that depends on the
122
+ **Important**: when using the `"hnsw"` search strategy, an index will be created that depends on the
107
123
  `vector_function` passed here. Make sure subsequent queries will keep using the same
108
124
  vector similarity function in order to take advantage of the index.
109
- :type search_strategy: Literal["exact_nearest_neighbor", "hnsw"]
110
125
  :param hnsw_recreate_index_if_exists: Whether to recreate the HNSW index if it already exists.
111
- Defaults to False. Only used if search_strategy is set to "hnsw".
126
+ Only used if search_strategy is set to `"hnsw"`.
112
127
  :param hnsw_index_creation_kwargs: Additional keyword arguments to pass to the HNSW index creation.
113
- Only used if search_strategy is set to "hnsw". You can find the list of valid arguments in the
114
- pgvector documentation: https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw
115
- :param hnsw_ef_search: The ef_search parameter to use at query time. Only used if search_strategy is set to
116
- "hnsw". You can find more information about this parameter in the pgvector documentation:
117
- https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw
128
+ Only used if search_strategy is set to `"hnsw"`. You can find the list of valid arguments in the
129
+ [pgvector documentation](https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw)
130
+ :param hnsw_ef_search: The `ef_search` parameter to use at query time. Only used if search_strategy is set to
131
+ `"hnsw"`. You can find more information about this parameter in the
132
+ [pgvector documentation](https://github.com/pgvector/pgvector?tab=readme-ov-file#hnsw).
118
133
  """
119
134
 
120
135
  self.connection_string = connection_string
@@ -129,8 +144,9 @@ class PgvectorDocumentStore:
129
144
  self.hnsw_recreate_index_if_exists = hnsw_recreate_index_if_exists
130
145
  self.hnsw_index_creation_kwargs = hnsw_index_creation_kwargs or {}
131
146
  self.hnsw_ef_search = hnsw_ef_search
147
+ self.language = language
132
148
 
133
- connection = connect(connection_string)
149
+ connection = connect(self.connection_string.resolve_value())
134
150
  connection.autocommit = True
135
151
  self._connection = connection
136
152
 
@@ -144,14 +160,21 @@ class PgvectorDocumentStore:
144
160
  if recreate_table:
145
161
  self.delete_table()
146
162
  self._create_table_if_not_exists()
163
+ self._create_keyword_index_if_not_exists()
147
164
 
148
165
  if search_strategy == "hnsw":
149
166
  self._handle_hnsw()
150
167
 
151
168
  def to_dict(self) -> Dict[str, Any]:
169
+ """
170
+ Serializes the component to a dictionary.
171
+
172
+ :returns:
173
+ Dictionary with serialized data.
174
+ """
152
175
  return default_to_dict(
153
176
  self,
154
- connection_string=self.connection_string,
177
+ connection_string=self.connection_string.to_dict(),
155
178
  table_name=self.table_name,
156
179
  embedding_dimension=self.embedding_dimension,
157
180
  vector_function=self.vector_function,
@@ -160,8 +183,22 @@ class PgvectorDocumentStore:
160
183
  hnsw_recreate_index_if_exists=self.hnsw_recreate_index_if_exists,
161
184
  hnsw_index_creation_kwargs=self.hnsw_index_creation_kwargs,
162
185
  hnsw_ef_search=self.hnsw_ef_search,
186
+ language=self.language,
163
187
  )
164
188
 
189
+ @classmethod
190
+ def from_dict(cls, data: Dict[str, Any]) -> "PgvectorDocumentStore":
191
+ """
192
+ Deserializes the component from a dictionary.
193
+
194
+ :param data:
195
+ Dictionary to deserialize from.
196
+ :returns:
197
+ Deserialized component.
198
+ """
199
+ deserialize_secrets_inplace(data["init_parameters"], ["connection_string"])
200
+ return default_from_dict(cls, data)
201
+
165
202
  def _execute_sql(
166
203
  self, sql_query: Query, params: Optional[tuple] = None, error_msg: str = "", cursor: Optional[Cursor] = None
167
204
  ):
@@ -203,16 +240,40 @@ class PgvectorDocumentStore:
203
240
  def delete_table(self):
204
241
  """
205
242
  Deletes the table used to store Haystack documents.
243
+ The name of the table (`table_name`) is defined when initializing the `PgvectorDocumentStore`.
206
244
  """
207
245
 
208
246
  delete_sql = SQL("DROP TABLE IF EXISTS {table_name}").format(table_name=Identifier(self.table_name))
209
247
 
210
248
  self._execute_sql(delete_sql, error_msg=f"Could not delete table {self.table_name} in PgvectorDocumentStore")
211
249
 
250
+ def _create_keyword_index_if_not_exists(self):
251
+ """
252
+ Internal method to create the keyword index if not exists.
253
+ """
254
+ index_exists = bool(
255
+ self._execute_sql(
256
+ "SELECT 1 FROM pg_indexes WHERE tablename = %s AND indexname = %s",
257
+ (self.table_name, KEYWORD_INDEX_NAME),
258
+ "Could not check if keyword index exists",
259
+ ).fetchone()
260
+ )
261
+
262
+ sql_create_index = SQL(
263
+ "CREATE INDEX {index_name} ON {table_name} USING GIN (to_tsvector({language}, content))"
264
+ ).format(
265
+ index_name=Identifier(KEYWORD_INDEX_NAME),
266
+ table_name=Identifier(self.table_name),
267
+ language=SQLLiteral(self.language),
268
+ )
269
+
270
+ if not index_exists:
271
+ self._execute_sql(sql_create_index, error_msg="Could not create keyword index on table")
272
+
212
273
  def _handle_hnsw(self):
213
274
  """
214
275
  Internal method to handle the HNSW index creation.
215
- It also sets the hnsw.ef_search parameter for queries if it is specified.
276
+ It also sets the `hnsw.ef_search` parameter for queries if it is specified.
216
277
  """
217
278
 
218
279
  if self.hnsw_ef_search:
@@ -221,7 +282,7 @@ class PgvectorDocumentStore:
221
282
  )
222
283
  self._execute_sql(sql_set_hnsw_ef_search, error_msg="Could not set hnsw.ef_search")
223
284
 
224
- index_esists = bool(
285
+ index_exists = bool(
225
286
  self._execute_sql(
226
287
  "SELECT 1 FROM pg_indexes WHERE tablename = %s AND indexname = %s",
227
288
  (self.table_name, HNSW_INDEX_NAME),
@@ -229,7 +290,7 @@ class PgvectorDocumentStore:
229
290
  ).fetchone()
230
291
  )
231
292
 
232
- if index_esists and not self.hnsw_recreate_index_if_exists:
293
+ if index_exists and not self.hnsw_recreate_index_if_exists:
233
294
  logger.warning(
234
295
  "HNSW index already exists and won't be recreated. "
235
296
  "If you want to recreate it, pass 'hnsw_recreate_index_if_exists=True' to the "
@@ -289,7 +350,8 @@ class PgvectorDocumentStore:
289
350
  refer to the [documentation](https://docs.haystack.deepset.ai/v2.0/docs/metadata-filtering)
290
351
 
291
352
  :param filters: The filters to apply to the document list.
292
- :return: A list of Documents that match the given filters.
353
+ :raises TypeError: If `filters` is not a dictionary.
354
+ :returns: A list of Documents that match the given filters.
293
355
  """
294
356
  if filters:
295
357
  if not isinstance(filters, dict):
@@ -318,13 +380,13 @@ class PgvectorDocumentStore:
318
380
 
319
381
  def write_documents(self, documents: List[Document], policy: DuplicatePolicy = DuplicatePolicy.NONE) -> int:
320
382
  """
321
- Writes documents into to PgvectorDocumentStore.
383
+ Writes documents to the document store.
322
384
 
323
385
  :param documents: A list of Documents to write to the document store.
324
386
  :param policy: The duplicate policy to use when writing documents.
325
387
  :raises DuplicateDocumentError: If a document with the same id already exists in the document store
326
- and the policy is set to DuplicatePolicy.FAIL (or not specified).
327
- :return: The number of documents written to the document store.
388
+ and the policy is set to `DuplicatePolicy.FAIL` (or not specified).
389
+ :returns: The number of documents written to the document store.
328
390
  """
329
391
 
330
392
  if len(documents) > 0:
@@ -373,7 +435,8 @@ class PgvectorDocumentStore:
373
435
 
374
436
  return written_docs
375
437
 
376
- def _from_haystack_to_pg_documents(self, documents: List[Document]) -> List[Dict[str, Any]]:
438
+ @staticmethod
439
+ def _from_haystack_to_pg_documents(documents: List[Document]) -> List[Dict[str, Any]]:
377
440
  """
378
441
  Internal method to convert a list of Haystack Documents to a list of dictionaries that can be used to insert
379
442
  documents into the PgvectorDocumentStore.
@@ -391,11 +454,22 @@ class PgvectorDocumentStore:
391
454
  db_document["dataframe"] = Jsonb(db_document["dataframe"]) if db_document["dataframe"] else None
392
455
  db_document["meta"] = Jsonb(db_document["meta"])
393
456
 
457
+ if "sparse_embedding" in db_document:
458
+ sparse_embedding = db_document.pop("sparse_embedding", None)
459
+ if sparse_embedding:
460
+ logger.warning(
461
+ "Document %s has the `sparse_embedding` field set,"
462
+ "but storing sparse embeddings in Pgvector is not currently supported."
463
+ "The `sparse_embedding` field will be ignored.",
464
+ db_document["id"],
465
+ )
466
+
394
467
  db_documents.append(db_document)
395
468
 
396
469
  return db_documents
397
470
 
398
- def _from_pg_to_haystack_documents(self, documents: List[Dict[str, Any]]) -> List[Document]:
471
+ @staticmethod
472
+ def _from_pg_to_haystack_documents(documents: List[Dict[str, Any]]) -> List[Document]:
399
473
  """
400
474
  Internal method to convert a list of dictionaries from pgvector to a list of Haystack Documents.
401
475
  """
@@ -424,7 +498,7 @@ class PgvectorDocumentStore:
424
498
 
425
499
  def delete_documents(self, document_ids: List[str]) -> None:
426
500
  """
427
- Deletes all documents with a matching document_ids from the document store.
501
+ Deletes documents that match the provided `document_ids` from the document store.
428
502
 
429
503
  :param document_ids: the document ids to delete
430
504
  """
@@ -440,6 +514,54 @@ class PgvectorDocumentStore:
440
514
 
441
515
  self._execute_sql(delete_sql, error_msg="Could not delete documents from PgvectorDocumentStore")
442
516
 
517
+ def _keyword_retrieval(
518
+ self,
519
+ query: str,
520
+ *,
521
+ filters: Optional[Dict[str, Any]] = None,
522
+ top_k: int = 10,
523
+ ) -> List[Document]:
524
+ """
525
+ Retrieves documents that are most similar to the query using a full-text search.
526
+
527
+ This method is not meant to be part of the public interface of
528
+ `PgvectorDocumentStore` and it should not be called directly.
529
+ `PgvectorKeywordRetriever` uses this method directly and is the public interface for it.
530
+
531
+ :returns: List of Documents that are most similar to `query`
532
+ """
533
+ if not query:
534
+ msg = "query must be a non-empty string"
535
+ raise ValueError(msg)
536
+
537
+ sql_select = SQL(KEYWORD_QUERY).format(
538
+ table_name=Identifier(self.table_name),
539
+ language=SQLLiteral(self.language),
540
+ query=SQLLiteral(query),
541
+ )
542
+
543
+ where_params = ()
544
+ sql_where_clause = SQL("")
545
+ if filters:
546
+ sql_where_clause, where_params = _convert_filters_to_where_clause_and_params(
547
+ filters=filters, operator="AND"
548
+ )
549
+
550
+ sql_sort = SQL(" ORDER BY score DESC LIMIT {top_k}").format(top_k=SQLLiteral(top_k))
551
+
552
+ sql_query = sql_select + sql_where_clause + sql_sort
553
+
554
+ result = self._execute_sql(
555
+ sql_query,
556
+ (query, *where_params),
557
+ error_msg="Could not retrieve documents from PgvectorDocumentStore.",
558
+ cursor=self._dict_cursor,
559
+ )
560
+
561
+ records = result.fetchall()
562
+ docs = self._from_pg_to_haystack_documents(records)
563
+ return docs
564
+
443
565
  def _embedding_retrieval(
444
566
  self,
445
567
  query_embedding: List[float],
@@ -454,8 +576,8 @@ class PgvectorDocumentStore:
454
576
  This method is not meant to be part of the public interface of
455
577
  `PgvectorDocumentStore` and it should not be called directly.
456
578
  `PgvectorEmbeddingRetriever` uses this method directly and is the public interface for it.
457
- :raises ValueError
458
- :return: List of Documents that are most similar to `query_embedding`
579
+
580
+ :returns: List of Documents that are most similar to `query_embedding`
459
581
  """
460
582
 
461
583
  if not query_embedding:
@@ -3,7 +3,7 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
  from datetime import datetime
5
5
  from itertools import chain
6
- from typing import Any, Dict, List
6
+ from typing import Any, Dict, List, Literal, Tuple
7
7
 
8
8
  from haystack.errors import FilterError
9
9
  from pandas import DataFrame
@@ -22,7 +22,9 @@ PYTHON_TYPES_TO_PG_TYPES = {
22
22
  NO_VALUE = "no_value"
23
23
 
24
24
 
25
- def _convert_filters_to_where_clause_and_params(filters: Dict[str, Any]) -> tuple[SQL, tuple]:
25
+ def _convert_filters_to_where_clause_and_params(
26
+ filters: Dict[str, Any], operator: Literal["WHERE", "AND"] = "WHERE"
27
+ ) -> Tuple[SQL, Tuple]:
26
28
  """
27
29
  Convert Haystack filters to a WHERE clause and a tuple of params to query PostgreSQL.
28
30
  """
@@ -31,13 +33,13 @@ def _convert_filters_to_where_clause_and_params(filters: Dict[str, Any]) -> tupl
31
33
  else:
32
34
  query, values = _parse_logical_condition(filters)
33
35
 
34
- where_clause = SQL(" WHERE ") + SQL(query)
36
+ where_clause = SQL(f" {operator} ") + SQL(query)
35
37
  params = tuple(value for value in values if value != NO_VALUE)
36
38
 
37
39
  return where_clause, params
38
40
 
39
41
 
40
- def _parse_logical_condition(condition: Dict[str, Any]) -> tuple[str, List[Any]]:
42
+ def _parse_logical_condition(condition: Dict[str, Any]) -> Tuple[str, List[Any]]:
41
43
  if "operator" not in condition:
42
44
  msg = f"'operator' key missing in {condition}"
43
45
  raise FilterError(msg)
@@ -77,7 +79,7 @@ def _parse_logical_condition(condition: Dict[str, Any]) -> tuple[str, List[Any]]
77
79
  return sql_query, values
78
80
 
79
81
 
80
- def _parse_comparison_condition(condition: Dict[str, Any]) -> tuple[str, List[Any]]:
82
+ def _parse_comparison_condition(condition: Dict[str, Any]) -> Tuple[str, List[Any]]:
81
83
  field: str = condition["field"]
82
84
  if "operator" not in condition:
83
85
  msg = f"'operator' key missing in {condition}"
@@ -132,20 +134,20 @@ def _treat_meta_field(field: str, value: Any) -> str:
132
134
  return field
133
135
 
134
136
 
135
- def _equal(field: str, value: Any) -> tuple[str, Any]:
137
+ def _equal(field: str, value: Any) -> Tuple[str, Any]:
136
138
  if value is None:
137
139
  # NO_VALUE is a placeholder that will be removed in _convert_filters_to_where_clause_and_params
138
140
  return f"{field} IS NULL", NO_VALUE
139
141
  return f"{field} = %s", value
140
142
 
141
143
 
142
- def _not_equal(field: str, value: Any) -> tuple[str, Any]:
144
+ def _not_equal(field: str, value: Any) -> Tuple[str, Any]:
143
145
  # we use IS DISTINCT FROM to correctly handle NULL values
144
146
  # (not handled by !=)
145
147
  return f"{field} IS DISTINCT FROM %s", value
146
148
 
147
149
 
148
- def _greater_than(field: str, value: Any) -> tuple[str, Any]:
150
+ def _greater_than(field: str, value: Any) -> Tuple[str, Any]:
149
151
  if isinstance(value, str):
150
152
  try:
151
153
  datetime.fromisoformat(value)
@@ -162,7 +164,7 @@ def _greater_than(field: str, value: Any) -> tuple[str, Any]:
162
164
  return f"{field} > %s", value
163
165
 
164
166
 
165
- def _greater_than_equal(field: str, value: Any) -> tuple[str, Any]:
167
+ def _greater_than_equal(field: str, value: Any) -> Tuple[str, Any]:
166
168
  if isinstance(value, str):
167
169
  try:
168
170
  datetime.fromisoformat(value)
@@ -179,7 +181,7 @@ def _greater_than_equal(field: str, value: Any) -> tuple[str, Any]:
179
181
  return f"{field} >= %s", value
180
182
 
181
183
 
182
- def _less_than(field: str, value: Any) -> tuple[str, Any]:
184
+ def _less_than(field: str, value: Any) -> Tuple[str, Any]:
183
185
  if isinstance(value, str):
184
186
  try:
185
187
  datetime.fromisoformat(value)
@@ -196,7 +198,7 @@ def _less_than(field: str, value: Any) -> tuple[str, Any]:
196
198
  return f"{field} < %s", value
197
199
 
198
200
 
199
- def _less_than_equal(field: str, value: Any) -> tuple[str, Any]:
201
+ def _less_than_equal(field: str, value: Any) -> Tuple[str, Any]:
200
202
  if isinstance(value, str):
201
203
  try:
202
204
  datetime.fromisoformat(value)
@@ -213,7 +215,7 @@ def _less_than_equal(field: str, value: Any) -> tuple[str, Any]:
213
215
  return f"{field} <= %s", value
214
216
 
215
217
 
216
- def _not_in(field: str, value: Any) -> tuple[str, List]:
218
+ def _not_in(field: str, value: Any) -> Tuple[str, List]:
217
219
  if not isinstance(value, list):
218
220
  msg = f"{field}'s value must be a list when using 'not in' comparator in Pinecone"
219
221
  raise FilterError(msg)
@@ -221,7 +223,7 @@ def _not_in(field: str, value: Any) -> tuple[str, List]:
221
223
  return f"{field} IS NULL OR {field} != ALL(%s)", [value]
222
224
 
223
225
 
224
- def _in(field: str, value: Any) -> tuple[str, List]:
226
+ def _in(field: str, value: Any) -> Tuple[str, List]:
225
227
  if not isinstance(value, list):
226
228
  msg = f"{field}'s value must be a list when using 'in' comparator in Pinecone"
227
229
  raise FilterError(msg)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pgvector-haystack
3
- Version: 0.0.1
3
+ Version: 0.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
@@ -9,6 +9,7 @@ Author-email: deepset GmbH <info@deepset.ai>
9
9
  License-Expression: Apache-2.0
10
10
  License-File: LICENSE.txt
11
11
  Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: Apache Software License
12
13
  Classifier: Programming Language :: Python
13
14
  Classifier: Programming Language :: Python :: 3.8
14
15
  Classifier: Programming Language :: Python :: 3.9
@@ -45,12 +46,23 @@ pip install pgvector-haystack
45
46
 
46
47
  ## Testing
47
48
 
48
- TODO
49
+ Ensure that you have a PostgreSQL running with the `pgvector` extension. For a quick setup using Docker, run:
50
+ ```
51
+ docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres ankane/pgvector
52
+ ```
53
+
54
+ then run the tests:
49
55
 
50
56
  ```console
51
57
  hatch run test
52
58
  ```
53
59
 
60
+ To run the coverage report:
61
+
62
+ ```console
63
+ hatch run cov
64
+ ```
65
+
54
66
  ## License
55
67
 
56
68
  `pgvector-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
@@ -0,0 +1,10 @@
1
+ haystack_integrations/components/retrievers/pgvector/__init__.py,sha256=n4q4pUApJ9DZakJ_JL-egwOzlefdnI_ZxjU1Lu44Sqo,296
2
+ haystack_integrations/components/retrievers/pgvector/embedding_retriever.py,sha256=6LuQmF7JmPOCU_I10ksPcbDC6QMu-2SOq0--BQxBxpg,6523
3
+ haystack_integrations/components/retrievers/pgvector/keyword_retriever.py,sha256=fAJ3M2l_Pt8zKqHES1l1L4gM16BOHeZWq8xc18Bhq7A,4624
4
+ haystack_integrations/document_stores/pgvector/__init__.py,sha256=Ge-7ggarfr3x8UqNdFHUuFGiu52u49sOVe3l16WEwZU,197
5
+ haystack_integrations/document_stores/pgvector/document_store.py,sha256=KY0FZGTjz4ejxKNqQ9UHDJUArsghwf7Iag45YfXgm2g,27011
6
+ haystack_integrations/document_stores/pgvector/filters.py,sha256=751_Z1QnXJCG0GWC2jcfeZ25NNYfJewQEIUCeJS70PA,8212
7
+ pgvector_haystack-0.2.0.dist-info/METADATA,sha256=fD4i8wnlapdeHvJMpj11zlmGS_MxN_d6GDFvc3W_i1A,2219
8
+ pgvector_haystack-0.2.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
9
+ pgvector_haystack-0.2.0.dist-info/licenses/LICENSE.txt,sha256=B05uMshqTA74s-0ltyHKI6yoPfJ3zYgQbvcXfDVGFf8,10280
10
+ pgvector_haystack-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.21.1
2
+ Generator: hatchling 1.24.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,9 +0,0 @@
1
- haystack_integrations/components/retrievers/pgvector/__init__.py,sha256=Ecyh_BCqILZVpuYVw9m8d7nwv0PoPomZxjfBSIYDVkI,212
2
- haystack_integrations/components/retrievers/pgvector/embedding_retriever.py,sha256=acVgvwHvLE4s_dbMPRmnISduAdc2JPewhdhhO2znsNo,4523
3
- haystack_integrations/document_stores/pgvector/__init__.py,sha256=Ge-7ggarfr3x8UqNdFHUuFGiu52u49sOVe3l16WEwZU,197
4
- haystack_integrations/document_stores/pgvector/document_store.py,sha256=PdV_Dpo_2y4Tyiy-6s11A7v_cKQPf5bSrOMPxYwftms,22299
5
- haystack_integrations/document_stores/pgvector/filters.py,sha256=_yj-5ZVccFc_uzDcwdVxa3UpiI9PzaNUErwEbp-lSo0,8139
6
- pgvector_haystack-0.0.1.dist-info/METADATA,sha256=8WYXmmFZLiJQrUrSjRx_MLkuUvzP1tK9Ab0C7EHinzg,1840
7
- pgvector_haystack-0.0.1.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
8
- pgvector_haystack-0.0.1.dist-info/licenses/LICENSE.txt,sha256=B05uMshqTA74s-0ltyHKI6yoPfJ3zYgQbvcXfDVGFf8,10280
9
- pgvector_haystack-0.0.1.dist-info/RECORD,,