pinecone-haystack 6.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.
- haystack_integrations/components/retrievers/pinecone/__init__.py +3 -0
- haystack_integrations/components/retrievers/pinecone/embedding_retriever.py +173 -0
- haystack_integrations/components/retrievers/py.typed +0 -0
- haystack_integrations/document_stores/pinecone/__init__.py +6 -0
- haystack_integrations/document_stores/pinecone/document_store.py +1102 -0
- haystack_integrations/document_stores/pinecone/filters.py +191 -0
- haystack_integrations/document_stores/py.typed +0 -0
- pinecone_haystack-6.2.0.dist-info/METADATA +38 -0
- pinecone_haystack-6.2.0.dist-info/RECORD +10 -0
- pinecone_haystack-6.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from copy import copy
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from haystack import default_from_dict, default_to_dict, logging
|
|
10
|
+
from haystack.dataclasses import Document
|
|
11
|
+
from haystack.document_stores.types import DuplicatePolicy
|
|
12
|
+
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
13
|
+
|
|
14
|
+
from pinecone import Pinecone, PineconeAsyncio, PodSpec, ServerlessSpec
|
|
15
|
+
from pinecone.db_data import _Index, _IndexAsyncio
|
|
16
|
+
from pinecone.exceptions import NotFoundException
|
|
17
|
+
|
|
18
|
+
from .filters import _normalize_filters, _validate_filters
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Pinecone has a limit of 1000 documents that can be returned in a query
|
|
23
|
+
# with include_metadata=True or include_data=True
|
|
24
|
+
# https://docs.pinecone.io/docs/limits
|
|
25
|
+
TOP_K_LIMIT = 1_000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DEFAULT_STARTER_PLAN_SPEC = {"serverless": {"region": "us-east-1", "cloud": "aws"}}
|
|
29
|
+
METADATA_SUPPORTED_TYPES = str, int, bool, float # List[str] is supported and checked separately
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PineconeDocumentStore:
|
|
33
|
+
"""
|
|
34
|
+
A Document Store using [Pinecone vector database](https://www.pinecone.io/).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
api_key: Secret = Secret.from_env_var("PINECONE_API_KEY"), # noqa: B008
|
|
41
|
+
index: str = "default",
|
|
42
|
+
namespace: str = "default",
|
|
43
|
+
batch_size: int = 100,
|
|
44
|
+
dimension: int = 768,
|
|
45
|
+
spec: dict[str, Any] | None = None,
|
|
46
|
+
metric: Literal["cosine", "euclidean", "dotproduct"] = "cosine",
|
|
47
|
+
show_progress: bool = True,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Creates a new PineconeDocumentStore instance.
|
|
51
|
+
|
|
52
|
+
It is meant to be connected to a Pinecone index and namespace.
|
|
53
|
+
|
|
54
|
+
:param api_key: The Pinecone API key.
|
|
55
|
+
:param index: The Pinecone index to connect to. If the index does not exist, it will be created.
|
|
56
|
+
:param namespace: The Pinecone namespace to connect to. If the namespace does not exist, it will be created
|
|
57
|
+
at the first write.
|
|
58
|
+
:param batch_size: The number of documents to write in a single batch. When setting this parameter,
|
|
59
|
+
consider [documented Pinecone limits](https://docs.pinecone.io/reference/quotas-and-limits).
|
|
60
|
+
:param dimension: The dimension of the embeddings. This parameter is only used when creating a new index.
|
|
61
|
+
:param spec: The Pinecone spec to use when creating a new index. Allows choosing between serverless and pod
|
|
62
|
+
deployment options and setting additional parameters. Refer to the
|
|
63
|
+
[Pinecone documentation](https://docs.pinecone.io/reference/api/control-plane/create_index) for more
|
|
64
|
+
details.
|
|
65
|
+
If not provided, a default spec with serverless deployment in the `us-east-1` region will be used
|
|
66
|
+
(compatible with the free tier).
|
|
67
|
+
:param metric: The metric to use for similarity search. This parameter is only used when creating a new index.
|
|
68
|
+
:param show_progress: Whether to show a progress bar when upserting documents. Set to False to disable
|
|
69
|
+
(e.g. in tests or scripts where quiet output is preferred).
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
self.api_key = api_key
|
|
73
|
+
spec = spec or DEFAULT_STARTER_PLAN_SPEC
|
|
74
|
+
self.namespace = namespace
|
|
75
|
+
self.batch_size = batch_size
|
|
76
|
+
self.metric = metric
|
|
77
|
+
self.spec = spec
|
|
78
|
+
self.dimension = dimension
|
|
79
|
+
self.index_name = index
|
|
80
|
+
self.show_progress = show_progress
|
|
81
|
+
|
|
82
|
+
self._index: _Index | None = None
|
|
83
|
+
self._async_index: _IndexAsyncio | None = None
|
|
84
|
+
self._dummy_vector = [-10.0] * self.dimension
|
|
85
|
+
|
|
86
|
+
def _initialize_index(self) -> None:
|
|
87
|
+
if self._index is not None:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
client = Pinecone(api_key=self.api_key.resolve_value(), source_tag="haystack")
|
|
91
|
+
|
|
92
|
+
if self.index_name not in client.list_indexes().names():
|
|
93
|
+
logger.info(f"Index {self.index_name} does not exist. Creating a new index.")
|
|
94
|
+
pinecone_spec = self._convert_dict_spec_to_pinecone_object(self.spec)
|
|
95
|
+
client.create_index(name=self.index_name, dimension=self.dimension, spec=pinecone_spec, metric=self.metric)
|
|
96
|
+
else:
|
|
97
|
+
logger.info(
|
|
98
|
+
f"Connecting to existing index {self.index_name}. `dimension`, `spec`, and `metric` will be ignored."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# client.Index returns _Index | GrpcIndex, but using the correct type would require bumping up the pinecone
|
|
102
|
+
# minimum supported version
|
|
103
|
+
self._index = client.Index(name=self.index_name) # type: ignore[assignment]
|
|
104
|
+
|
|
105
|
+
assert self._index is not None
|
|
106
|
+
|
|
107
|
+
actual_dimension = self._index.describe_index_stats().get("dimension")
|
|
108
|
+
if actual_dimension and actual_dimension != self.dimension:
|
|
109
|
+
logger.warning(
|
|
110
|
+
f"Dimension of index {self.index_name} is {actual_dimension}, but {self.dimension} was specified. "
|
|
111
|
+
"The specified dimension will be ignored."
|
|
112
|
+
"If you need an index with a different dimension, please create a new one."
|
|
113
|
+
)
|
|
114
|
+
self.dimension = actual_dimension or self.dimension
|
|
115
|
+
self._dummy_vector = [-10.0] * self.dimension
|
|
116
|
+
|
|
117
|
+
async def _initialize_async_index(self) -> None:
|
|
118
|
+
if self._async_index is not None:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
async_client = PineconeAsyncio(api_key=self.api_key.resolve_value(), source_tag="haystack")
|
|
122
|
+
|
|
123
|
+
indexes = await async_client.list_indexes()
|
|
124
|
+
if self.index_name not in indexes.names():
|
|
125
|
+
logger.info(f"Index {self.index_name} does not exist. Creating a new index.")
|
|
126
|
+
pinecone_spec = self._convert_dict_spec_to_pinecone_object(self.spec)
|
|
127
|
+
new_index = await async_client.create_index(
|
|
128
|
+
name=self.index_name, dimension=self.dimension, spec=pinecone_spec, metric=self.metric
|
|
129
|
+
)
|
|
130
|
+
host = new_index["host"]
|
|
131
|
+
else:
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Connecting to existing index {self.index_name}. `dimension`, `spec`, and `metric` will be ignored."
|
|
134
|
+
)
|
|
135
|
+
host = next((index["host"] for index in indexes if index["name"] == self.index_name), None)
|
|
136
|
+
|
|
137
|
+
self._async_index = async_client.IndexAsyncio(host=host)
|
|
138
|
+
|
|
139
|
+
index_stats = await self._async_index.describe_index_stats()
|
|
140
|
+
actual_dimension = index_stats.get("dimension")
|
|
141
|
+
if actual_dimension and actual_dimension != self.dimension:
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"Dimension of index {self.index_name} is {actual_dimension}, but {self.dimension} was specified. "
|
|
144
|
+
"The specified dimension will be ignored."
|
|
145
|
+
"If you need an index with a different dimension, please create a new one."
|
|
146
|
+
)
|
|
147
|
+
self.dimension = actual_dimension or self.dimension
|
|
148
|
+
self._dummy_vector = [-10.0] * self.dimension
|
|
149
|
+
|
|
150
|
+
await async_client.close()
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Close the associated synchronous resources.
|
|
155
|
+
"""
|
|
156
|
+
if self._index:
|
|
157
|
+
self._index.close()
|
|
158
|
+
self._index = None
|
|
159
|
+
|
|
160
|
+
async def close_async(self) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Close the associated asynchronous resources. To be invoked manually when the Document Store is no longer needed.
|
|
163
|
+
"""
|
|
164
|
+
if self._async_index:
|
|
165
|
+
await self._async_index.close()
|
|
166
|
+
self._async_index = None
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _convert_dict_spec_to_pinecone_object(spec: dict[str, Any]) -> ServerlessSpec | PodSpec:
|
|
170
|
+
"""Convert the spec dictionary to a Pinecone spec object"""
|
|
171
|
+
|
|
172
|
+
if "serverless" in spec:
|
|
173
|
+
serverless_spec = spec["serverless"]
|
|
174
|
+
return ServerlessSpec(**serverless_spec)
|
|
175
|
+
if "pod" in spec:
|
|
176
|
+
pod_spec = spec["pod"]
|
|
177
|
+
return PodSpec(**pod_spec)
|
|
178
|
+
|
|
179
|
+
msg = (
|
|
180
|
+
"Invalid spec. Must contain either `serverless` or `pod` key. "
|
|
181
|
+
"Refer to https://docs.pinecone.io/reference/api/control-plane/create_index for more details."
|
|
182
|
+
)
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, data: dict[str, Any]) -> "PineconeDocumentStore":
|
|
187
|
+
"""
|
|
188
|
+
Deserializes the component from a dictionary.
|
|
189
|
+
|
|
190
|
+
:param data:
|
|
191
|
+
Dictionary to deserialize from.
|
|
192
|
+
:returns:
|
|
193
|
+
Deserialized component.
|
|
194
|
+
"""
|
|
195
|
+
deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
|
|
196
|
+
return default_from_dict(cls, data)
|
|
197
|
+
|
|
198
|
+
def to_dict(self) -> dict[str, Any]:
|
|
199
|
+
"""
|
|
200
|
+
Serializes the component to a dictionary.
|
|
201
|
+
|
|
202
|
+
:returns:
|
|
203
|
+
Dictionary with serialized data.
|
|
204
|
+
"""
|
|
205
|
+
return default_to_dict(
|
|
206
|
+
self,
|
|
207
|
+
api_key=self.api_key.to_dict(),
|
|
208
|
+
spec=self.spec,
|
|
209
|
+
index=self.index_name,
|
|
210
|
+
dimension=self.dimension,
|
|
211
|
+
namespace=self.namespace,
|
|
212
|
+
batch_size=self.batch_size,
|
|
213
|
+
metric=self.metric,
|
|
214
|
+
show_progress=self.show_progress,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def count_documents(self) -> int:
|
|
218
|
+
"""
|
|
219
|
+
Returns how many documents are present in the document store.
|
|
220
|
+
"""
|
|
221
|
+
self._initialize_index()
|
|
222
|
+
assert self._index is not None, "Index is not initialized"
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
count = self._index.describe_index_stats()["namespaces"][self.namespace]["vector_count"]
|
|
226
|
+
except KeyError:
|
|
227
|
+
count = 0
|
|
228
|
+
return count
|
|
229
|
+
|
|
230
|
+
async def count_documents_async(self) -> int:
|
|
231
|
+
"""
|
|
232
|
+
Asynchronously returns how many documents are present in the document store.
|
|
233
|
+
"""
|
|
234
|
+
await self._initialize_async_index()
|
|
235
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
index_stats = await self._async_index.describe_index_stats()
|
|
239
|
+
count = index_stats["namespaces"][self.namespace]["vector_count"]
|
|
240
|
+
except KeyError:
|
|
241
|
+
count = 0
|
|
242
|
+
return count
|
|
243
|
+
|
|
244
|
+
def write_documents(self, documents: list[Document], policy: DuplicatePolicy = DuplicatePolicy.NONE) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Writes Documents to Pinecone.
|
|
247
|
+
|
|
248
|
+
:param documents: A list of Documents to write to the document store.
|
|
249
|
+
:param policy: The duplicate policy to use when writing documents.
|
|
250
|
+
PineconeDocumentStore only supports `DuplicatePolicy.OVERWRITE`.
|
|
251
|
+
|
|
252
|
+
:returns: The number of documents written to the document store.
|
|
253
|
+
"""
|
|
254
|
+
self._initialize_index()
|
|
255
|
+
assert self._index is not None, "Index is not initialized"
|
|
256
|
+
|
|
257
|
+
documents_for_pinecone = self._prepare_documents_for_writing(documents, policy)
|
|
258
|
+
|
|
259
|
+
result = self._index.upsert(
|
|
260
|
+
vectors=documents_for_pinecone,
|
|
261
|
+
namespace=self.namespace,
|
|
262
|
+
batch_size=self.batch_size,
|
|
263
|
+
show_progress=self.show_progress,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# if the operation is successful, result will have the upserted_count attribute
|
|
267
|
+
return result.upserted_count # type: ignore[union-attr]
|
|
268
|
+
|
|
269
|
+
async def write_documents_async(
|
|
270
|
+
self, documents: list[Document], policy: DuplicatePolicy = DuplicatePolicy.NONE
|
|
271
|
+
) -> int:
|
|
272
|
+
"""
|
|
273
|
+
Asynchronously writes Documents to Pinecone.
|
|
274
|
+
|
|
275
|
+
:param documents: A list of Documents to write to the document store.
|
|
276
|
+
:param policy: The duplicate policy to use when writing documents.
|
|
277
|
+
PineconeDocumentStore only supports `DuplicatePolicy.OVERWRITE`.
|
|
278
|
+
|
|
279
|
+
:returns: The number of documents written to the document store.
|
|
280
|
+
"""
|
|
281
|
+
await self._initialize_async_index()
|
|
282
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
283
|
+
|
|
284
|
+
documents_for_pinecone = self._prepare_documents_for_writing(documents, policy)
|
|
285
|
+
|
|
286
|
+
result = await self._async_index.upsert(
|
|
287
|
+
vectors=documents_for_pinecone,
|
|
288
|
+
namespace=self.namespace,
|
|
289
|
+
batch_size=self.batch_size,
|
|
290
|
+
show_progress=self.show_progress,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# if the operation is successful, result will have the upserted_count attribute
|
|
294
|
+
return result.upserted_count # type: ignore[union-attr]
|
|
295
|
+
|
|
296
|
+
def filter_documents(self, filters: dict[str, Any] | None = None) -> list[Document]:
|
|
297
|
+
"""
|
|
298
|
+
Returns the documents that match the filters provided.
|
|
299
|
+
|
|
300
|
+
For a detailed specification of the filters,
|
|
301
|
+
refer to the [documentation](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
302
|
+
|
|
303
|
+
:param filters: The filters to apply to the document list.
|
|
304
|
+
:returns: A list of Documents that match the given filters.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
_validate_filters(filters)
|
|
308
|
+
|
|
309
|
+
self._initialize_index()
|
|
310
|
+
assert self._index is not None, "Index is not initialized"
|
|
311
|
+
|
|
312
|
+
# Pinecone only performs vector similarity search
|
|
313
|
+
# here we are querying with a dummy vector and the max compatible top_k
|
|
314
|
+
documents = self._embedding_retrieval(query_embedding=self._dummy_vector, filters=filters, top_k=TOP_K_LIMIT)
|
|
315
|
+
|
|
316
|
+
# when simply filtering, we don't want to return any scores
|
|
317
|
+
# furthermore, we are querying with a dummy vector, so the scores are meaningless
|
|
318
|
+
documents = [replace(doc, score=None) for doc in documents]
|
|
319
|
+
|
|
320
|
+
if len(documents) == TOP_K_LIMIT:
|
|
321
|
+
logger.warning(
|
|
322
|
+
f"PineconeDocumentStore can return at most {TOP_K_LIMIT} documents and the query has hit this limit. "
|
|
323
|
+
f"It is likely that there are more matching documents in the document store. "
|
|
324
|
+
)
|
|
325
|
+
return documents
|
|
326
|
+
|
|
327
|
+
async def filter_documents_async(self, filters: dict[str, Any] | None = None) -> list[Document]:
|
|
328
|
+
"""
|
|
329
|
+
Asynchronously returns the documents that match the filters provided.
|
|
330
|
+
|
|
331
|
+
:param filters: The filters to apply to the document list.
|
|
332
|
+
:returns: A list of Documents that match the given filters.
|
|
333
|
+
"""
|
|
334
|
+
_validate_filters(filters)
|
|
335
|
+
|
|
336
|
+
await self._initialize_async_index()
|
|
337
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
338
|
+
|
|
339
|
+
documents = await self._embedding_retrieval_async(
|
|
340
|
+
query_embedding=self._dummy_vector, filters=filters, top_k=TOP_K_LIMIT
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
documents = [replace(doc, score=None) for doc in documents]
|
|
344
|
+
|
|
345
|
+
if len(documents) == TOP_K_LIMIT:
|
|
346
|
+
logger.warning(
|
|
347
|
+
f"PineconeDocumentStore can return at most {TOP_K_LIMIT} documents and the query has hit this limit. "
|
|
348
|
+
f"It is likely that there are more matching documents in the document store. "
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return documents
|
|
352
|
+
|
|
353
|
+
def delete_documents(self, document_ids: list[str]) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Deletes documents that match the provided `document_ids` from the document store.
|
|
356
|
+
|
|
357
|
+
:param document_ids: the document ids to delete
|
|
358
|
+
"""
|
|
359
|
+
self._initialize_index()
|
|
360
|
+
assert self._index is not None, "Index is not initialized"
|
|
361
|
+
self._index.delete(ids=document_ids, namespace=self.namespace)
|
|
362
|
+
|
|
363
|
+
async def delete_documents_async(self, document_ids: list[str]) -> None:
|
|
364
|
+
"""
|
|
365
|
+
Asynchronously deletes documents that match the provided `document_ids` from the document store.
|
|
366
|
+
|
|
367
|
+
:param document_ids: the document ids to delete
|
|
368
|
+
"""
|
|
369
|
+
await self._initialize_async_index()
|
|
370
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
371
|
+
await self._async_index.delete(ids=document_ids, namespace=self.namespace)
|
|
372
|
+
|
|
373
|
+
def delete_all_documents(self) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Deletes all documents in the document store.
|
|
376
|
+
"""
|
|
377
|
+
self._initialize_index()
|
|
378
|
+
assert self._index is not None, "Index is not initialized"
|
|
379
|
+
try:
|
|
380
|
+
self._index.delete(delete_all=True, namespace=self.namespace)
|
|
381
|
+
except NotFoundException:
|
|
382
|
+
# Namespace doesn't exist (empty collection), which is fine - nothing to delete
|
|
383
|
+
logger.debug("Namespace '{namespace}' not found. Nothing to delete.", namespace=self.namespace or "default")
|
|
384
|
+
|
|
385
|
+
async def delete_all_documents_async(self) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Asynchronously deletes all documents in the document store.
|
|
388
|
+
"""
|
|
389
|
+
await self._initialize_async_index()
|
|
390
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
391
|
+
try:
|
|
392
|
+
await self._async_index.delete(delete_all=True, namespace=self.namespace)
|
|
393
|
+
except NotFoundException:
|
|
394
|
+
# Namespace doesn't exist (empty collection), which is fine - nothing to delete
|
|
395
|
+
logger.debug("Namespace '{namespace}' not found. Nothing to delete.", namespace=self.namespace or "default")
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def _update_documents_metadata(documents: list[Document], meta: dict[str, Any]) -> None:
|
|
399
|
+
"""
|
|
400
|
+
Updates metadata for a list of documents by merging the provided meta dictionary.
|
|
401
|
+
|
|
402
|
+
:param documents: List of documents to update.
|
|
403
|
+
:param meta: Metadata fields to merge into each document's existing metadata.
|
|
404
|
+
"""
|
|
405
|
+
for i, document in enumerate(documents):
|
|
406
|
+
documents[i] = replace(document, meta={**(document.meta or {}), **meta})
|
|
407
|
+
|
|
408
|
+
def delete_by_filter(self, filters: dict[str, Any]) -> int:
|
|
409
|
+
"""
|
|
410
|
+
Deletes all documents that match the provided filters.
|
|
411
|
+
|
|
412
|
+
Pinecone does not support server-side delete by filter, so this method
|
|
413
|
+
first searches for matching documents, then deletes them by ID.
|
|
414
|
+
|
|
415
|
+
:param filters: The filters to apply to select documents for deletion.
|
|
416
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
417
|
+
:returns: The number of documents deleted.
|
|
418
|
+
"""
|
|
419
|
+
_validate_filters(filters)
|
|
420
|
+
|
|
421
|
+
self._initialize_index()
|
|
422
|
+
assert self._index is not None, "Index is not initialized"
|
|
423
|
+
|
|
424
|
+
documents = self.filter_documents(filters=filters)
|
|
425
|
+
if not documents:
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
document_ids = [doc.id for doc in documents]
|
|
429
|
+
|
|
430
|
+
self.delete_documents(document_ids)
|
|
431
|
+
|
|
432
|
+
deleted_count = len(document_ids)
|
|
433
|
+
logger.info(
|
|
434
|
+
"Deleted {n_docs} documents from index '{index}' using filters.",
|
|
435
|
+
n_docs=deleted_count,
|
|
436
|
+
index=self.index_name,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return deleted_count
|
|
440
|
+
|
|
441
|
+
async def delete_by_filter_async(self, filters: dict[str, Any]) -> int:
|
|
442
|
+
"""
|
|
443
|
+
Asynchronously deletes all documents that match the provided filters.
|
|
444
|
+
|
|
445
|
+
Pinecone does not support server-side delete by filter, so this method
|
|
446
|
+
first searches for matching documents, then deletes them by ID.
|
|
447
|
+
|
|
448
|
+
:param filters: The filters to apply to select documents for deletion.
|
|
449
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
450
|
+
:returns: The number of documents deleted.
|
|
451
|
+
"""
|
|
452
|
+
_validate_filters(filters)
|
|
453
|
+
|
|
454
|
+
await self._initialize_async_index()
|
|
455
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
456
|
+
|
|
457
|
+
documents = await self.filter_documents_async(filters=filters)
|
|
458
|
+
if not documents:
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
document_ids = [doc.id for doc in documents]
|
|
462
|
+
|
|
463
|
+
await self.delete_documents_async(document_ids)
|
|
464
|
+
|
|
465
|
+
deleted_count = len(document_ids)
|
|
466
|
+
logger.info(
|
|
467
|
+
"Deleted {n_docs} documents from index '{index}' using filters.",
|
|
468
|
+
n_docs=deleted_count,
|
|
469
|
+
index=self.index_name,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
return deleted_count
|
|
473
|
+
|
|
474
|
+
def update_by_filter(self, filters: dict[str, Any], meta: dict[str, Any]) -> int:
|
|
475
|
+
"""
|
|
476
|
+
Updates the metadata of all documents that match the provided filters.
|
|
477
|
+
|
|
478
|
+
Pinecone does not support server-side update by filter, so this method
|
|
479
|
+
first searches for matching documents, then updates their metadata and re-writes them.
|
|
480
|
+
|
|
481
|
+
:param filters: The filters to apply to select documents for updating.
|
|
482
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
483
|
+
:param meta: The metadata fields to update. This will be merged with existing metadata.
|
|
484
|
+
:returns: The number of documents updated.
|
|
485
|
+
"""
|
|
486
|
+
_validate_filters(filters)
|
|
487
|
+
|
|
488
|
+
if not isinstance(meta, dict):
|
|
489
|
+
msg = "meta must be a dictionary"
|
|
490
|
+
raise ValueError(msg)
|
|
491
|
+
|
|
492
|
+
self._initialize_index()
|
|
493
|
+
assert self._index is not None, "Index is not initialized"
|
|
494
|
+
|
|
495
|
+
documents = self.filter_documents(filters=filters)
|
|
496
|
+
if not documents:
|
|
497
|
+
return 0
|
|
498
|
+
|
|
499
|
+
self._update_documents_metadata(documents, meta)
|
|
500
|
+
|
|
501
|
+
# Re-write documents with updated metadata
|
|
502
|
+
# Using OVERWRITE policy to update existing documents
|
|
503
|
+
self.write_documents(documents, policy=DuplicatePolicy.OVERWRITE)
|
|
504
|
+
|
|
505
|
+
updated_count = len(documents)
|
|
506
|
+
logger.info(
|
|
507
|
+
"Updated {n_docs} documents in index '{index}' using filters.",
|
|
508
|
+
n_docs=updated_count,
|
|
509
|
+
index=self.index_name,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return updated_count
|
|
513
|
+
|
|
514
|
+
async def update_by_filter_async(self, filters: dict[str, Any], meta: dict[str, Any]) -> int:
|
|
515
|
+
"""
|
|
516
|
+
Asynchronously updates the metadata of all documents that match the provided filters.
|
|
517
|
+
|
|
518
|
+
Pinecone does not support server-side update by filter, so this method
|
|
519
|
+
first searches for matching documents, then updates their metadata and re-writes them.
|
|
520
|
+
|
|
521
|
+
:param filters: The filters to apply to select documents for updating.
|
|
522
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
523
|
+
:param meta: The metadata fields to update. This will be merged with existing metadata.
|
|
524
|
+
:returns: The number of documents updated.
|
|
525
|
+
"""
|
|
526
|
+
_validate_filters(filters)
|
|
527
|
+
|
|
528
|
+
if not isinstance(meta, dict):
|
|
529
|
+
msg = "meta must be a dictionary"
|
|
530
|
+
raise ValueError(msg)
|
|
531
|
+
|
|
532
|
+
await self._initialize_async_index()
|
|
533
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
534
|
+
|
|
535
|
+
documents = await self.filter_documents_async(filters=filters)
|
|
536
|
+
if not documents:
|
|
537
|
+
return 0
|
|
538
|
+
|
|
539
|
+
self._update_documents_metadata(documents, meta)
|
|
540
|
+
|
|
541
|
+
# Re-write documents with updated metadata
|
|
542
|
+
# Using OVERWRITE policy to update existing documents
|
|
543
|
+
await self.write_documents_async(documents, policy=DuplicatePolicy.OVERWRITE)
|
|
544
|
+
|
|
545
|
+
updated_count = len(documents)
|
|
546
|
+
logger.info(
|
|
547
|
+
"Updated {n_docs} documents in index '{index}' using filters.",
|
|
548
|
+
n_docs=updated_count,
|
|
549
|
+
index=self.index_name,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return updated_count
|
|
553
|
+
|
|
554
|
+
def _embedding_retrieval(
|
|
555
|
+
self,
|
|
556
|
+
query_embedding: list[float],
|
|
557
|
+
*,
|
|
558
|
+
namespace: str | None = None,
|
|
559
|
+
filters: dict[str, Any] | None = None,
|
|
560
|
+
top_k: int = 10,
|
|
561
|
+
) -> list[Document]:
|
|
562
|
+
"""
|
|
563
|
+
Retrieves documents that are most similar to the query embedding using a vector similarity metric.
|
|
564
|
+
|
|
565
|
+
This method is not mean to be part of the public interface of
|
|
566
|
+
`PineconeDocumentStore` nor called directly.
|
|
567
|
+
`PineconeEmbeddingRetriever` uses this method directly and is the public interface for it.
|
|
568
|
+
|
|
569
|
+
:param query_embedding: Embedding of the query.
|
|
570
|
+
:param namespace: Pinecone namespace to query. Defaults the namespace of the document store.
|
|
571
|
+
:param filters: Filters applied to the retrieved Documents.
|
|
572
|
+
:param top_k: Maximum number of Documents to return.
|
|
573
|
+
|
|
574
|
+
:returns: List of Document that are most similar to `query_embedding`
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
if not query_embedding:
|
|
578
|
+
msg = "query_embedding must be a non-empty list of floats"
|
|
579
|
+
raise ValueError(msg)
|
|
580
|
+
|
|
581
|
+
_validate_filters(filters)
|
|
582
|
+
filters = _normalize_filters(filters) if filters else None
|
|
583
|
+
self._initialize_index()
|
|
584
|
+
assert self._index is not None, "Index is not initialized"
|
|
585
|
+
|
|
586
|
+
result = self._index.query(
|
|
587
|
+
vector=query_embedding,
|
|
588
|
+
top_k=top_k,
|
|
589
|
+
namespace=namespace or self.namespace,
|
|
590
|
+
filter=filters,
|
|
591
|
+
include_values=True,
|
|
592
|
+
include_metadata=True,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
return self._convert_query_result_to_documents(result)
|
|
596
|
+
|
|
597
|
+
async def _embedding_retrieval_async(
|
|
598
|
+
self,
|
|
599
|
+
query_embedding: list[float],
|
|
600
|
+
*,
|
|
601
|
+
namespace: str | None = None,
|
|
602
|
+
filters: dict[str, Any] | None = None,
|
|
603
|
+
top_k: int = 10,
|
|
604
|
+
) -> list[Document]:
|
|
605
|
+
"""
|
|
606
|
+
Asynchronously retrieves documents that are similar to the query embedding using a vector similarity metric.
|
|
607
|
+
|
|
608
|
+
:param query_embedding: Embedding of the query.
|
|
609
|
+
:param namespace: Pinecone namespace to query. Defaults the namespace of the document store.
|
|
610
|
+
:param filters: Filters applied to the retrieved Documents.
|
|
611
|
+
:param top_k: Maximum number of Documents to return.
|
|
612
|
+
|
|
613
|
+
:returns: List of Document that are most similar to `query_embedding`
|
|
614
|
+
"""
|
|
615
|
+
if not query_embedding:
|
|
616
|
+
msg = "query_embedding must be a non-empty list of floats"
|
|
617
|
+
raise ValueError(msg)
|
|
618
|
+
|
|
619
|
+
_validate_filters(filters)
|
|
620
|
+
filters = _normalize_filters(filters) if filters else None
|
|
621
|
+
|
|
622
|
+
await self._initialize_async_index()
|
|
623
|
+
assert self._async_index is not None, "Index is not initialized"
|
|
624
|
+
|
|
625
|
+
result = await self._async_index.query(
|
|
626
|
+
vector=query_embedding,
|
|
627
|
+
top_k=top_k,
|
|
628
|
+
namespace=namespace or self.namespace,
|
|
629
|
+
filter=filters,
|
|
630
|
+
include_values=True,
|
|
631
|
+
include_metadata=True,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
return self._convert_query_result_to_documents(result)
|
|
635
|
+
|
|
636
|
+
@staticmethod
|
|
637
|
+
def _convert_meta_to_int(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
638
|
+
"""
|
|
639
|
+
Convert specific numeric metadata values from `float` back to `int`.
|
|
640
|
+
|
|
641
|
+
Pinecone stores numeric metadata values as `float`. Some specific metadata are used in Retrievers
|
|
642
|
+
components and are expected to be `int`. This method converts them back to integers.
|
|
643
|
+
"""
|
|
644
|
+
values_to_convert = ["split_id", "split_idx_start", "page_number"]
|
|
645
|
+
|
|
646
|
+
for value in values_to_convert:
|
|
647
|
+
if value in metadata:
|
|
648
|
+
metadata[value] = int(metadata[value]) if isinstance(metadata[value], float) else metadata[value]
|
|
649
|
+
|
|
650
|
+
return metadata
|
|
651
|
+
|
|
652
|
+
def _convert_query_result_to_documents(self, query_result: Any) -> list[Document]:
|
|
653
|
+
pinecone_docs = query_result.matches
|
|
654
|
+
documents = []
|
|
655
|
+
for pinecone_doc in pinecone_docs:
|
|
656
|
+
content = pinecone_doc["metadata"].pop("content", None)
|
|
657
|
+
|
|
658
|
+
# we always store vectors during writing but we don't want to return them if they are dummy vectors
|
|
659
|
+
embedding = None
|
|
660
|
+
if pinecone_doc["values"] != self._dummy_vector:
|
|
661
|
+
embedding = pinecone_doc["values"]
|
|
662
|
+
|
|
663
|
+
doc = Document(
|
|
664
|
+
id=pinecone_doc["id"],
|
|
665
|
+
content=content,
|
|
666
|
+
meta=self._convert_meta_to_int(pinecone_doc["metadata"]),
|
|
667
|
+
embedding=embedding,
|
|
668
|
+
score=pinecone_doc["score"],
|
|
669
|
+
)
|
|
670
|
+
documents.append(doc)
|
|
671
|
+
|
|
672
|
+
return documents
|
|
673
|
+
|
|
674
|
+
@staticmethod
|
|
675
|
+
def _discard_invalid_meta(document: Document) -> Document:
|
|
676
|
+
"""
|
|
677
|
+
Remove metadata fields with unsupported types from the document.
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
def valid_type(value: Any) -> bool:
|
|
681
|
+
return isinstance(value, METADATA_SUPPORTED_TYPES) or (
|
|
682
|
+
isinstance(value, list) and all(isinstance(i, str) for i in value)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
if document.meta:
|
|
686
|
+
discarded_keys = []
|
|
687
|
+
new_meta = {}
|
|
688
|
+
for key, value in document.meta.items():
|
|
689
|
+
if not valid_type(value):
|
|
690
|
+
discarded_keys.append(key)
|
|
691
|
+
else:
|
|
692
|
+
new_meta[key] = value
|
|
693
|
+
|
|
694
|
+
if discarded_keys:
|
|
695
|
+
msg = (
|
|
696
|
+
f"Document {document.id} has metadata fields with unsupported types: {discarded_keys}. "
|
|
697
|
+
f"Only str, int, bool, and List[str] are supported. The values of these fields will be discarded."
|
|
698
|
+
)
|
|
699
|
+
logger.warning(msg)
|
|
700
|
+
|
|
701
|
+
return replace(document, meta=new_meta)
|
|
702
|
+
|
|
703
|
+
return document
|
|
704
|
+
|
|
705
|
+
def _convert_documents_to_pinecone_format(
|
|
706
|
+
self, documents: list[Document]
|
|
707
|
+
) -> list[tuple[str, list[float], dict[str, Any]]]:
|
|
708
|
+
documents_for_pinecone = []
|
|
709
|
+
for document in documents:
|
|
710
|
+
embedding = copy(document.embedding)
|
|
711
|
+
if embedding is None:
|
|
712
|
+
logger.warning(
|
|
713
|
+
f"Document {document.id} has no embedding. Pinecone is a purely vector database. "
|
|
714
|
+
"A dummy embedding will be used, but this can affect the search results. "
|
|
715
|
+
)
|
|
716
|
+
embedding = self._dummy_vector
|
|
717
|
+
|
|
718
|
+
filtered_meta = self._discard_invalid_meta(document).meta if document.meta else {}
|
|
719
|
+
|
|
720
|
+
metadata = dict(filtered_meta) if filtered_meta else {}
|
|
721
|
+
|
|
722
|
+
# we save content as metadata
|
|
723
|
+
if document.content is not None:
|
|
724
|
+
metadata["content"] = document.content
|
|
725
|
+
|
|
726
|
+
# currently, storing blob in Pinecone is not supported
|
|
727
|
+
if document.blob is not None:
|
|
728
|
+
logger.warning(
|
|
729
|
+
f"Document {document.id} has the `blob` field set, but storing `ByteStream` "
|
|
730
|
+
"objects in Pinecone is not supported. "
|
|
731
|
+
"The content of the `blob` field will be ignored."
|
|
732
|
+
)
|
|
733
|
+
if hasattr(document, "sparse_embedding") and document.sparse_embedding is not None:
|
|
734
|
+
logger.warning(
|
|
735
|
+
"Document {document_id} has the `sparse_embedding` field set,"
|
|
736
|
+
"but storing sparse embeddings in Pinecone is not currently supported."
|
|
737
|
+
"The `sparse_embedding` field will be ignored.",
|
|
738
|
+
document_id=document.id,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
documents_for_pinecone.append((document.id, embedding, metadata))
|
|
742
|
+
return documents_for_pinecone
|
|
743
|
+
|
|
744
|
+
def _prepare_documents_for_writing(
|
|
745
|
+
self, documents: list[Document], policy: DuplicatePolicy
|
|
746
|
+
) -> list[tuple[str, list[float], dict[str, Any]]]:
|
|
747
|
+
"""
|
|
748
|
+
Helper method to prepare documents for writing to Pinecone.
|
|
749
|
+
"""
|
|
750
|
+
if len(documents) > 0 and not isinstance(documents[0], Document):
|
|
751
|
+
msg = "param 'documents' must contain a list of objects of type Document"
|
|
752
|
+
raise ValueError(msg)
|
|
753
|
+
|
|
754
|
+
if policy not in [DuplicatePolicy.NONE, DuplicatePolicy.OVERWRITE]:
|
|
755
|
+
logger.warning(
|
|
756
|
+
f"PineconeDocumentStore only supports `DuplicatePolicy.OVERWRITE`"
|
|
757
|
+
f"but got {policy}. Overwriting duplicates is enabled by default."
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
return self._convert_documents_to_pinecone_format(documents)
|
|
761
|
+
|
|
762
|
+
@staticmethod
|
|
763
|
+
def _count_documents_impl(documents: list[Document]) -> int:
|
|
764
|
+
"""Helper method to count documents and log warning if at TOP_K_LIMIT."""
|
|
765
|
+
count = len(documents)
|
|
766
|
+
if count == TOP_K_LIMIT:
|
|
767
|
+
logger.warning(
|
|
768
|
+
f"Count reached Pinecone's limit of {TOP_K_LIMIT} documents. "
|
|
769
|
+
f"The actual number of matching documents may be higher."
|
|
770
|
+
)
|
|
771
|
+
return count
|
|
772
|
+
|
|
773
|
+
@staticmethod
|
|
774
|
+
def _count_unique_metadata_impl(documents: list[Document], metadata_fields: list[str]) -> dict[str, int]:
|
|
775
|
+
"""Helper method to count unique metadata values across specified fields."""
|
|
776
|
+
result = {}
|
|
777
|
+
for field in metadata_fields:
|
|
778
|
+
unique_values = set()
|
|
779
|
+
for doc in documents:
|
|
780
|
+
if doc.meta and field in doc.meta:
|
|
781
|
+
value = doc.meta[field]
|
|
782
|
+
# Handle list values
|
|
783
|
+
if isinstance(value, list):
|
|
784
|
+
unique_values.update(value)
|
|
785
|
+
else:
|
|
786
|
+
unique_values.add(value)
|
|
787
|
+
result[field] = len(unique_values)
|
|
788
|
+
|
|
789
|
+
if len(documents) == TOP_K_LIMIT:
|
|
790
|
+
logger.warning(
|
|
791
|
+
f"Analysis limited to {TOP_K_LIMIT} documents due to Pinecone's limits. "
|
|
792
|
+
f"Unique value counts may be incomplete."
|
|
793
|
+
)
|
|
794
|
+
return result
|
|
795
|
+
|
|
796
|
+
@staticmethod
|
|
797
|
+
def _get_metadata_fields_info_impl(documents: list[Document]) -> dict[str, dict[str, str]]:
|
|
798
|
+
"""Helper method to infer metadata field types from documents."""
|
|
799
|
+
if not documents:
|
|
800
|
+
return {}
|
|
801
|
+
|
|
802
|
+
field_types: dict[str, dict[str, str]] = {}
|
|
803
|
+
|
|
804
|
+
# Check if any document has content
|
|
805
|
+
if any(doc.content is not None for doc in documents):
|
|
806
|
+
field_types["content"] = {"type": "text"}
|
|
807
|
+
|
|
808
|
+
# Collect all field values to infer types accurately
|
|
809
|
+
field_samples: dict[str, set[str]] = {}
|
|
810
|
+
|
|
811
|
+
for doc in documents:
|
|
812
|
+
if doc.meta:
|
|
813
|
+
for field, value in doc.meta.items():
|
|
814
|
+
if field not in field_samples:
|
|
815
|
+
field_samples[field] = set()
|
|
816
|
+
|
|
817
|
+
# Note: bool check MUST come before int/float because bool is a subclass of int in Python
|
|
818
|
+
if isinstance(value, bool):
|
|
819
|
+
field_samples[field].add("boolean")
|
|
820
|
+
elif isinstance(value, (int, float)):
|
|
821
|
+
field_samples[field].add("long")
|
|
822
|
+
elif isinstance(value, str):
|
|
823
|
+
field_samples[field].add("keyword")
|
|
824
|
+
elif isinstance(value, list):
|
|
825
|
+
# For lists, check the type of elements if list is non-empty
|
|
826
|
+
if value:
|
|
827
|
+
# Sample first element to determine list type
|
|
828
|
+
if isinstance(value[0], str):
|
|
829
|
+
field_samples[field].add("keyword")
|
|
830
|
+
elif isinstance(value[0], (int, float)):
|
|
831
|
+
field_samples[field].add("long")
|
|
832
|
+
elif isinstance(value[0], bool):
|
|
833
|
+
field_samples[field].add("boolean")
|
|
834
|
+
else:
|
|
835
|
+
# Empty list, default to keyword
|
|
836
|
+
field_samples[field].add("keyword")
|
|
837
|
+
|
|
838
|
+
# Assign types based on collected samples
|
|
839
|
+
for field, types_seen in field_samples.items():
|
|
840
|
+
if len(types_seen) == 1:
|
|
841
|
+
# Consistent type across all documents
|
|
842
|
+
field_types[field] = {"type": types_seen.pop()}
|
|
843
|
+
else:
|
|
844
|
+
# Mixed types - default to keyword and log warning
|
|
845
|
+
logger.warning(
|
|
846
|
+
f"Field '{field}' has mixed types {types_seen} across documents. "
|
|
847
|
+
f"Defaulting to 'keyword' type. Consider using consistent types for better query performance."
|
|
848
|
+
)
|
|
849
|
+
field_types[field] = {"type": "keyword"}
|
|
850
|
+
|
|
851
|
+
if len(documents) == TOP_K_LIMIT:
|
|
852
|
+
logger.info(
|
|
853
|
+
f"Schema inference based on {TOP_K_LIMIT} documents (Pinecone's query limit). "
|
|
854
|
+
f"If you have more documents with different metadata fields, they won't be reflected here."
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
return field_types
|
|
858
|
+
|
|
859
|
+
@staticmethod
|
|
860
|
+
def _get_metadata_field_min_max_impl(documents: list[Document], metadata_field: str) -> dict[str, Any]:
|
|
861
|
+
"""Helper method to get min/max values for a metadata field (supports numeric, boolean, and string types)."""
|
|
862
|
+
field_name = metadata_field.removeprefix("meta.")
|
|
863
|
+
values: list[bool | int | float | str] = []
|
|
864
|
+
for doc in documents:
|
|
865
|
+
if doc.meta and field_name in doc.meta:
|
|
866
|
+
value = doc.meta[field_name]
|
|
867
|
+
# Note: bool check must come before numeric because bool is subclass of int
|
|
868
|
+
if isinstance(value, bool):
|
|
869
|
+
values.append(value)
|
|
870
|
+
elif isinstance(value, (int, float)):
|
|
871
|
+
values.append(value)
|
|
872
|
+
elif isinstance(value, str):
|
|
873
|
+
values.append(value)
|
|
874
|
+
|
|
875
|
+
if not values:
|
|
876
|
+
return {"min": None, "max": None}
|
|
877
|
+
|
|
878
|
+
result = {"min": min(values), "max": max(values)}
|
|
879
|
+
|
|
880
|
+
if len(documents) == TOP_K_LIMIT:
|
|
881
|
+
logger.warning(
|
|
882
|
+
f"Min/max calculation limited to {TOP_K_LIMIT} documents. "
|
|
883
|
+
f"Results may not reflect the true min/max across all documents."
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
return result
|
|
887
|
+
|
|
888
|
+
@staticmethod
|
|
889
|
+
def _get_metadata_field_unique_values_impl(
|
|
890
|
+
documents: list[Document], metadata_field: str, search_term: str | None, from_: int, size: int
|
|
891
|
+
) -> tuple[list[str], int]:
|
|
892
|
+
"""Helper method to get unique values for a metadata field with search and pagination."""
|
|
893
|
+
unique_values: set[str] = set()
|
|
894
|
+
for doc in documents:
|
|
895
|
+
if doc.meta and metadata_field in doc.meta:
|
|
896
|
+
value = doc.meta[metadata_field]
|
|
897
|
+
# Handle list values
|
|
898
|
+
if isinstance(value, list):
|
|
899
|
+
unique_values.update(str(v) for v in value)
|
|
900
|
+
else:
|
|
901
|
+
unique_values.add(str(value))
|
|
902
|
+
|
|
903
|
+
# Convert to sorted list
|
|
904
|
+
unique_values_list = sorted(unique_values)
|
|
905
|
+
|
|
906
|
+
# Apply search term filter if provided
|
|
907
|
+
if search_term:
|
|
908
|
+
search_term_lower = search_term.lower()
|
|
909
|
+
unique_values_list = [v for v in unique_values_list if search_term_lower in v.lower()]
|
|
910
|
+
|
|
911
|
+
total_count = len(unique_values_list)
|
|
912
|
+
|
|
913
|
+
# Apply pagination
|
|
914
|
+
paginated_values = unique_values_list[from_ : from_ + size]
|
|
915
|
+
|
|
916
|
+
if len(documents) == TOP_K_LIMIT:
|
|
917
|
+
logger.warning(f"Unique values extraction limited to {TOP_K_LIMIT} documents. Results may be incomplete.")
|
|
918
|
+
|
|
919
|
+
return paginated_values, total_count
|
|
920
|
+
|
|
921
|
+
def count_documents_by_filter(self, filters: dict[str, Any]) -> int:
|
|
922
|
+
"""
|
|
923
|
+
Returns the count of documents that match the provided filters.
|
|
924
|
+
|
|
925
|
+
Note: Due to Pinecone's limitations, this method fetches documents and counts them.
|
|
926
|
+
For large result sets, this is subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
927
|
+
|
|
928
|
+
:param filters: The filters to apply to the document list.
|
|
929
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
930
|
+
:returns: The number of documents that match the filters.
|
|
931
|
+
"""
|
|
932
|
+
documents = self.filter_documents(filters=filters)
|
|
933
|
+
return self._count_documents_impl(documents)
|
|
934
|
+
|
|
935
|
+
async def count_documents_by_filter_async(self, filters: dict[str, Any]) -> int:
|
|
936
|
+
"""
|
|
937
|
+
Asynchronously returns the count of documents that match the provided filters.
|
|
938
|
+
|
|
939
|
+
Note: Due to Pinecone's limitations, this method fetches documents and counts them.
|
|
940
|
+
For large result sets, this is subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
941
|
+
|
|
942
|
+
:param filters: The filters to apply to the document list.
|
|
943
|
+
:returns: The number of documents that match the filters.
|
|
944
|
+
"""
|
|
945
|
+
documents = await self.filter_documents_async(filters=filters)
|
|
946
|
+
return self._count_documents_impl(documents)
|
|
947
|
+
|
|
948
|
+
def count_unique_metadata_by_filter(self, filters: dict[str, Any], metadata_fields: list[str]) -> dict[str, int]:
|
|
949
|
+
"""
|
|
950
|
+
Counts unique values for each specified metadata field in documents matching the filters.
|
|
951
|
+
|
|
952
|
+
Note: Due to Pinecone's limitations, this method fetches documents and aggregates in Python.
|
|
953
|
+
Subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
954
|
+
|
|
955
|
+
:param filters: The filters to apply to select documents.
|
|
956
|
+
:param metadata_fields: List of metadata field names to count unique values for.
|
|
957
|
+
:returns: Dictionary mapping field names to counts of unique values.
|
|
958
|
+
"""
|
|
959
|
+
documents = self.filter_documents(filters=filters)
|
|
960
|
+
return self._count_unique_metadata_impl(documents, metadata_fields)
|
|
961
|
+
|
|
962
|
+
async def count_unique_metadata_by_filter_async(
|
|
963
|
+
self, filters: dict[str, Any], metadata_fields: list[str]
|
|
964
|
+
) -> dict[str, int]:
|
|
965
|
+
"""
|
|
966
|
+
Asynchronously counts unique values for each specified metadata field in documents matching the filters.
|
|
967
|
+
|
|
968
|
+
Note: Due to Pinecone's limitations, this method fetches documents and aggregates in Python.
|
|
969
|
+
Subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
970
|
+
|
|
971
|
+
:param filters: The filters to apply to select documents.
|
|
972
|
+
:param metadata_fields: List of metadata field names to count unique values for.
|
|
973
|
+
:returns: Dictionary mapping field names to counts of unique values.
|
|
974
|
+
"""
|
|
975
|
+
documents = await self.filter_documents_async(filters=filters)
|
|
976
|
+
return self._count_unique_metadata_impl(documents, metadata_fields)
|
|
977
|
+
|
|
978
|
+
def get_metadata_fields_info(self) -> dict[str, dict[str, str]]:
|
|
979
|
+
"""
|
|
980
|
+
Returns information about metadata fields and their types by sampling documents.
|
|
981
|
+
|
|
982
|
+
Note: Pinecone doesn't provide a schema introspection API, so this method infers field types
|
|
983
|
+
by examining the metadata of documents stored in the index (up to 1000 documents).
|
|
984
|
+
|
|
985
|
+
Type mappings:
|
|
986
|
+
- 'text': Document content field
|
|
987
|
+
- 'keyword': String metadata values
|
|
988
|
+
- 'long': Numeric metadata values (int or float)
|
|
989
|
+
- 'boolean': Boolean metadata values
|
|
990
|
+
|
|
991
|
+
:returns: Dictionary mapping field names to type information.
|
|
992
|
+
Example:
|
|
993
|
+
```python
|
|
994
|
+
{
|
|
995
|
+
'content': {'type': 'text'},
|
|
996
|
+
'category': {'type': 'keyword'},
|
|
997
|
+
'priority': {'type': 'long'},
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
"""
|
|
1001
|
+
documents = self.filter_documents(filters=None)
|
|
1002
|
+
return self._get_metadata_fields_info_impl(documents)
|
|
1003
|
+
|
|
1004
|
+
async def get_metadata_fields_info_async(self) -> dict[str, dict[str, str]]:
|
|
1005
|
+
"""
|
|
1006
|
+
Asynchronously returns information about metadata fields and their types by sampling documents.
|
|
1007
|
+
|
|
1008
|
+
Note: Pinecone doesn't provide a schema introspection API, so this method infers field types
|
|
1009
|
+
by examining the metadata of documents stored in the index (up to 1000 documents).
|
|
1010
|
+
|
|
1011
|
+
Type mappings:
|
|
1012
|
+
- 'text': Document content field
|
|
1013
|
+
- 'keyword': String metadata values
|
|
1014
|
+
- 'long': Numeric metadata values (int or float)
|
|
1015
|
+
- 'boolean': Boolean metadata values
|
|
1016
|
+
|
|
1017
|
+
:returns: Dictionary mapping field names to type information.
|
|
1018
|
+
Example:
|
|
1019
|
+
```python
|
|
1020
|
+
{
|
|
1021
|
+
'content': {'type': 'text'},
|
|
1022
|
+
'category': {'type': 'keyword'},
|
|
1023
|
+
'priority': {'type': 'long'},
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
1026
|
+
"""
|
|
1027
|
+
documents = await self.filter_documents_async(filters=None)
|
|
1028
|
+
return self._get_metadata_fields_info_impl(documents)
|
|
1029
|
+
|
|
1030
|
+
def get_metadata_field_min_max(self, metadata_field: str) -> dict[str, Any]:
|
|
1031
|
+
"""
|
|
1032
|
+
Returns the minimum and maximum values for a metadata field.
|
|
1033
|
+
|
|
1034
|
+
Supports numeric (int, float), boolean, and string (keyword) types:
|
|
1035
|
+
- Numeric: Returns min/max based on numeric value
|
|
1036
|
+
- Boolean: Returns False as min, True as max
|
|
1037
|
+
- String: Returns min/max based on alphabetical ordering
|
|
1038
|
+
|
|
1039
|
+
Note: This method fetches all documents and computes min/max in Python.
|
|
1040
|
+
Subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
1041
|
+
|
|
1042
|
+
:param metadata_field: The metadata field name to analyze.
|
|
1043
|
+
:returns: Dictionary with 'min' and 'max' keys. Both values are None if the field has no
|
|
1044
|
+
values (empty store, field absent, or unsupported field type).
|
|
1045
|
+
"""
|
|
1046
|
+
documents = self.filter_documents(filters=None)
|
|
1047
|
+
return self._get_metadata_field_min_max_impl(documents, metadata_field)
|
|
1048
|
+
|
|
1049
|
+
async def get_metadata_field_min_max_async(self, metadata_field: str) -> dict[str, Any]:
|
|
1050
|
+
"""
|
|
1051
|
+
Asynchronously returns the minimum and maximum values for a metadata field.
|
|
1052
|
+
|
|
1053
|
+
Supports numeric (int, float), boolean, and string (keyword) types:
|
|
1054
|
+
- Numeric: Returns min/max based on numeric value
|
|
1055
|
+
- Boolean: Returns False as min, True as max
|
|
1056
|
+
- String: Returns min/max based on alphabetical ordering
|
|
1057
|
+
|
|
1058
|
+
Note: This method fetches all documents and computes min/max in Python.
|
|
1059
|
+
Subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
1060
|
+
|
|
1061
|
+
:param metadata_field: The metadata field name to analyze.
|
|
1062
|
+
:returns: Dictionary with 'min' and 'max' keys. Both values are None if the field has no
|
|
1063
|
+
values (empty store, field absent, or unsupported field type).
|
|
1064
|
+
"""
|
|
1065
|
+
documents = await self.filter_documents_async(filters=None)
|
|
1066
|
+
return self._get_metadata_field_min_max_impl(documents, metadata_field)
|
|
1067
|
+
|
|
1068
|
+
def get_metadata_field_unique_values(
|
|
1069
|
+
self, metadata_field: str, search_term: str | None = None, from_: int = 0, size: int = 10
|
|
1070
|
+
) -> tuple[list[str], int]:
|
|
1071
|
+
"""
|
|
1072
|
+
Retrieves unique values for a metadata field with optional search and pagination.
|
|
1073
|
+
|
|
1074
|
+
Note: This method fetches documents and extracts unique values in Python.
|
|
1075
|
+
Subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
1076
|
+
|
|
1077
|
+
:param metadata_field: The metadata field name to get unique values for.
|
|
1078
|
+
:param search_term: Optional search term to filter values (case-insensitive substring match).
|
|
1079
|
+
:param from_: Starting offset for pagination (default: 0).
|
|
1080
|
+
:param size: Number of values to return (default: 10).
|
|
1081
|
+
:returns: Tuple of (list of unique values, total count of matching values).
|
|
1082
|
+
"""
|
|
1083
|
+
documents = self.filter_documents(filters=None)
|
|
1084
|
+
return self._get_metadata_field_unique_values_impl(documents, metadata_field, search_term, from_, size)
|
|
1085
|
+
|
|
1086
|
+
async def get_metadata_field_unique_values_async(
|
|
1087
|
+
self, metadata_field: str, search_term: str | None = None, from_: int = 0, size: int = 10
|
|
1088
|
+
) -> tuple[list[str], int]:
|
|
1089
|
+
"""
|
|
1090
|
+
Asynchronously retrieves unique values for a metadata field with optional search and pagination.
|
|
1091
|
+
|
|
1092
|
+
Note: This method fetches documents and extracts unique values in Python.
|
|
1093
|
+
Subject to Pinecone's TOP_K_LIMIT of 1000 documents.
|
|
1094
|
+
|
|
1095
|
+
:param metadata_field: The metadata field name to get unique values for.
|
|
1096
|
+
:param search_term: Optional search term to filter values (case-insensitive substring match).
|
|
1097
|
+
:param from_: Starting offset for pagination (default: 0).
|
|
1098
|
+
:param size: Number of values to return (default: 10).
|
|
1099
|
+
:returns: Tuple of (list of unique values, total count of matching values).
|
|
1100
|
+
"""
|
|
1101
|
+
documents = await self.filter_documents_async(filters=None)
|
|
1102
|
+
return self._get_metadata_field_unique_values_impl(documents, metadata_field, search_term, from_, size)
|