milvus-haystack 0.0.1__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.
- milvus_haystack/__about__.py +4 -0
- milvus_haystack/__init__.py +4 -0
- milvus_haystack/filter_utils.py +478 -0
- milvus_haystack/milvus.py +841 -0
- milvus_haystack-0.0.1.dist-info/METADATA +48 -0
- milvus_haystack-0.0.1.dist-info/RECORD +8 -0
- milvus_haystack-0.0.1.dist-info/WHEEL +4 -0
- milvus_haystack-0.0.1.dist-info/licenses/LICENSE.txt +73 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, Generator, List, Optional, Set, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from pymilvus_simple.simple_api import SimpleAPI
|
|
9
|
+
except (ImportError, ModuleNotFoundError) as ie:
|
|
10
|
+
from haystack.utils.import_utils import _optional_component_not_installed
|
|
11
|
+
|
|
12
|
+
_optional_component_not_installed(__name__, "milvus2", ie)
|
|
13
|
+
|
|
14
|
+
from haystack.document_stores import BaseDocumentStore
|
|
15
|
+
from haystack.errors import DuplicateDocumentError
|
|
16
|
+
from haystack.schema import Document, FilterType, Label
|
|
17
|
+
|
|
18
|
+
from .filter_utils import LogicalFilterClause
|
|
19
|
+
|
|
20
|
+
ID_FIELD = "id"
|
|
21
|
+
EMPTY_FIELD = "empty"
|
|
22
|
+
VECTOR_FIELD = "embedding"
|
|
23
|
+
META_FIELD = "$meta"
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MilvusDocumentStore(BaseDocumentStore):
|
|
29
|
+
"""
|
|
30
|
+
Usage:
|
|
31
|
+
1. Start a Milvus service via docker (see https://milvus.io/docs/install_standalone-docker.md)
|
|
32
|
+
2. Run pip install pymilvus-haystack
|
|
33
|
+
3. Init a MilvusDocumentStore() in Haystack
|
|
34
|
+
|
|
35
|
+
Overview:
|
|
36
|
+
Milvus (https://milvus.io/) is a highly reliable, scalable Document Store specialized on storing and processing vectors.
|
|
37
|
+
Therefore, it is particularly suited for Haystack users that work with dense retrieval methods (like DPR).
|
|
38
|
+
|
|
39
|
+
In contrast to FAISS, Milvus ...
|
|
40
|
+
- runs as a separate service (e.g. a Docker container) and can scale easily in a distributed environment
|
|
41
|
+
- allows dynamic data management (i.e. you can insert/delete vectors without recreating the whole index)
|
|
42
|
+
- encapsulates multiple ANN libraries (FAISS, ANNOY ...)
|
|
43
|
+
|
|
44
|
+
This class uses Milvus for all vector related storage, processing and querying.
|
|
45
|
+
The meta-data (e.g. for filtering) and the document text are also stored within Milvus
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
uri: str = "http://localhost:19530/default",
|
|
51
|
+
api_key: Optional[str] = None,
|
|
52
|
+
user: Optional[str] = None,
|
|
53
|
+
password: Optional[str] = None,
|
|
54
|
+
index: str = "document",
|
|
55
|
+
embedding_dim: int = 768,
|
|
56
|
+
similarity: str = "dot_product",
|
|
57
|
+
index_params: Optional[Dict[str, Any]] = None,
|
|
58
|
+
return_embedding: bool = False,
|
|
59
|
+
progress_bar: bool = False,
|
|
60
|
+
consistency_level: str = "Session",
|
|
61
|
+
replicas: int = 1,
|
|
62
|
+
recreate_index: bool = False,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
:param uri: The connection URI to use for Milvus.
|
|
66
|
+
:param api_key: Optional API key to use instead of user and password.
|
|
67
|
+
:param user: Optional username to use to connect to Milvus instance.
|
|
68
|
+
:param password: Optional password to use to connect to Milvus instance.
|
|
69
|
+
:param index: The name of the collection to use for storing the data.
|
|
70
|
+
:param embedding_dim: The embedding vector size. Default: 768.
|
|
71
|
+
:param similarity: The similarity function used to compare document vectors. 'dot_product' is the default and recommended for DPR embeddings.
|
|
72
|
+
'cosine' is recommended for Sentence Transformers, but is not directly supported by Milvus.
|
|
73
|
+
However, you can normalize your embeddings and use `dot_product` to get the same results.
|
|
74
|
+
See https://milvus.io/docs/metric.md.
|
|
75
|
+
:param index_params: Configuration parameters for the chose index_type needed at indexing time.
|
|
76
|
+
For example: {"nlist": 16384} as the number of cluster units to create for index_type IVF_FLAT.
|
|
77
|
+
See https://milvus.io/docs/index.md
|
|
78
|
+
:param return_embedding: To return document embedding.
|
|
79
|
+
:param progress_bar: Whether to show a tqdm progress bar or not.
|
|
80
|
+
Can be helpful to disable in production deployments to keep the logs clean.
|
|
81
|
+
:param consistency_level: Which consistency level to use for Milvus index: Strong, Session, Bounded, Evenetually.
|
|
82
|
+
See https://milvus.io/docs/v2.1.x/tune_consistency.md.
|
|
83
|
+
:param replicas: How many in memory replicas to use for index.
|
|
84
|
+
See https://milvus.io/docs/replica.md.
|
|
85
|
+
:param recreate_index: If set to True, an existing Milvus index will be deleted and a new one will be
|
|
86
|
+
created using the config you are using for initialization. Be aware that all data in the old index will be
|
|
87
|
+
lost if you choose to recreate the index. Be aware that both the document_index and the label_index will
|
|
88
|
+
be recreated.
|
|
89
|
+
"""
|
|
90
|
+
self.collection_name = index
|
|
91
|
+
self.consistency_level = consistency_level
|
|
92
|
+
self.index_params = index_params
|
|
93
|
+
self.progress_bar = progress_bar
|
|
94
|
+
self.replicas = replicas
|
|
95
|
+
self.dimension = embedding_dim
|
|
96
|
+
self.dummy_data = [0] * embedding_dim
|
|
97
|
+
self.return_embedding = return_embedding
|
|
98
|
+
|
|
99
|
+
if similarity == "cosine":
|
|
100
|
+
self.metric_type = "IP"
|
|
101
|
+
self.normalize = True
|
|
102
|
+
elif similarity == "dot_product":
|
|
103
|
+
self.metric_type = "IP"
|
|
104
|
+
elif similarity in ("l2", "euclidean"):
|
|
105
|
+
self.metric_type = "L2"
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"The Milvus document store can currently only support dot_product, cosine and euclidean metrics. "
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.client = SimpleAPI(uri=uri, api_key=api_key, user=user, password=password)
|
|
112
|
+
self._create_collection(index, recreate_index)
|
|
113
|
+
|
|
114
|
+
def _create_collection(self, name, recreate_index=False):
|
|
115
|
+
self.client.create_collection(
|
|
116
|
+
collection_name=name,
|
|
117
|
+
dimension=self.dimension,
|
|
118
|
+
primary_field=ID_FIELD,
|
|
119
|
+
primary_type="str",
|
|
120
|
+
primary_auto_id=False,
|
|
121
|
+
vector_field=VECTOR_FIELD,
|
|
122
|
+
metric_type=self.metric_type,
|
|
123
|
+
partition_field={"name": EMPTY_FIELD, "type": "int"},
|
|
124
|
+
index=self.index_params,
|
|
125
|
+
overwrite=recreate_index,
|
|
126
|
+
consistency_level=self.consistency_level,
|
|
127
|
+
replicas=self.replicas,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def write_documents(
|
|
131
|
+
self,
|
|
132
|
+
documents: Union[List[dict], List[Document]],
|
|
133
|
+
index: Optional[str] = None,
|
|
134
|
+
batch_size: int = 10_000,
|
|
135
|
+
duplicate_documents: Optional[str] = None,
|
|
136
|
+
headers: Optional[Dict[str, str]] = None,
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
Indexes documents for later queries.
|
|
140
|
+
|
|
141
|
+
:param documents: a list of Python dictionaries or a list of Haystack Document objects.
|
|
142
|
+
For documents as dictionaries, the format is {"text": "<the-actual-text>"}.
|
|
143
|
+
Optionally: Include meta data via {"text": "<the-actual-text>",
|
|
144
|
+
"meta":{"name": "<some-document-name>, "author": "somebody", ...}}
|
|
145
|
+
It can be used for filtering and is accessible in the responses of the Finder.
|
|
146
|
+
:param index: Optional name of index where the documents shall be written to.
|
|
147
|
+
If None, the DocumentStore's default index (self.index) will be used.
|
|
148
|
+
:param batch_size: Number of documents that are passed to bulk function at a time.
|
|
149
|
+
:param duplicate_documents: Handle duplicates document based on parameter options.
|
|
150
|
+
Parameter options : ( 'skip','overwrite','fail')
|
|
151
|
+
skip: Ignore the duplicates documents
|
|
152
|
+
overwrite: Update any existing documents with the same ID when adding documents.
|
|
153
|
+
fail: an error is raised if the document ID of the document being added already
|
|
154
|
+
exists.
|
|
155
|
+
:param headers: Custom HTTP headers to pass to document store client if supported (e.g. {'Authorization': 'Basic YWRtaW46cm9vdA=='} for basic authentication)
|
|
156
|
+
|
|
157
|
+
:return: None
|
|
158
|
+
"""
|
|
159
|
+
if headers:
|
|
160
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
161
|
+
|
|
162
|
+
index = index or self.collection_name
|
|
163
|
+
if index not in self.client.list_collections():
|
|
164
|
+
self._create_collection(index)
|
|
165
|
+
docs = self._handle_duplicate_documents(
|
|
166
|
+
documents=documents,
|
|
167
|
+
index=index,
|
|
168
|
+
duplicate_documents=duplicate_documents,
|
|
169
|
+
headers=headers,
|
|
170
|
+
)
|
|
171
|
+
docs_modified = []
|
|
172
|
+
for doc in docs:
|
|
173
|
+
doc = doc.to_dict()
|
|
174
|
+
if doc.get("embedding") is None:
|
|
175
|
+
doc["embedding"] = self.dummy_data
|
|
176
|
+
doc[EMPTY_FIELD] = 1
|
|
177
|
+
else:
|
|
178
|
+
doc[EMPTY_FIELD] = 0
|
|
179
|
+
doc.pop("score", None)
|
|
180
|
+
meta = doc.pop("meta", None)
|
|
181
|
+
doc.update(meta)
|
|
182
|
+
docs_modified.append(doc)
|
|
183
|
+
self.client.insert(
|
|
184
|
+
collection_name=index,
|
|
185
|
+
data=docs_modified,
|
|
186
|
+
batch_size=batch_size,
|
|
187
|
+
progress_bar=self.progress_bar,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def get_all_documents(
|
|
191
|
+
self,
|
|
192
|
+
index: Optional[str] = None,
|
|
193
|
+
filters: Optional[FilterType] = None,
|
|
194
|
+
return_embedding: Optional[bool] = None,
|
|
195
|
+
batch_size: Optional[int] = None,
|
|
196
|
+
headers: Optional[Dict[str, str]] = None,
|
|
197
|
+
) -> List[Document]:
|
|
198
|
+
"""
|
|
199
|
+
Get documents from the document store.
|
|
200
|
+
|
|
201
|
+
:param index: Name of the index to get the documents from. If None, the
|
|
202
|
+
DocumentStore's default index (self.index) will be used.
|
|
203
|
+
:param filters: Optional filters to narrow down the search space to documents whose metadata fulfill certain
|
|
204
|
+
conditions.
|
|
205
|
+
Filters are defined as nested dictionaries. The keys of the dictionaries can be a logical
|
|
206
|
+
operator (`"$and"`, `"$or"`, `"$not"`), a comparison operator (`"$eq"`, `"$in"`, `"$gt"`,
|
|
207
|
+
`"$gte"`, `"$lt"`, `"$lte"`) or a metadata field name.
|
|
208
|
+
Logical operator keys take a dictionary of metadata field names and/or logical operators as
|
|
209
|
+
value. Metadata field names take a dictionary of comparison operators as value. Comparison
|
|
210
|
+
operator keys take a single value or (in case of `"$in"`) a list of values as value.
|
|
211
|
+
If no logical operator is provided, `"$and"` is used as default operation. If no comparison
|
|
212
|
+
operator is provided, `"$eq"` (or `"$in"` if the comparison value is a list) is used as default
|
|
213
|
+
operation.
|
|
214
|
+
|
|
215
|
+
__Example__:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
filters = {
|
|
219
|
+
"$and": {
|
|
220
|
+
"type": {"$eq": "article"},
|
|
221
|
+
"date": {"$gte": "2015-01-01", "$lt": "2021-01-01"},
|
|
222
|
+
"rating": {"$gte": 3},
|
|
223
|
+
"$or": {
|
|
224
|
+
"genre": {"$in": ["economy", "politics"]},
|
|
225
|
+
"publisher": {"$eq": "nytimes"}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
:param return_embedding: Whether to return the document embeddings.
|
|
232
|
+
:param batch_size: Not supported.
|
|
233
|
+
:param headers: Custom HTTP headers to pass to document store client if supported (e.g. {'Authorization': 'Basic YWRtaW46cm9vdA=='} for basic authentication)
|
|
234
|
+
"""
|
|
235
|
+
if headers:
|
|
236
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
237
|
+
|
|
238
|
+
if batch_size is not None:
|
|
239
|
+
logger.warn(
|
|
240
|
+
"The parameter batch_size is not supported, loading all results."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
index = index or self.collection_name
|
|
244
|
+
return_embedding = return_embedding or self.return_embedding
|
|
245
|
+
|
|
246
|
+
filters = (
|
|
247
|
+
LogicalFilterClause.parse(filters).convert_to_milvus() if filters else None
|
|
248
|
+
)
|
|
249
|
+
res = self.client.query(
|
|
250
|
+
collection_name=index,
|
|
251
|
+
filter_expression=filters,
|
|
252
|
+
include_vectors=return_embedding,
|
|
253
|
+
partition_keys=[0, 1],
|
|
254
|
+
)
|
|
255
|
+
res_docs = []
|
|
256
|
+
for hit in res:
|
|
257
|
+
hit.pop(EMPTY_FIELD)
|
|
258
|
+
res_docs.append(Document.from_dict(hit))
|
|
259
|
+
return res_docs
|
|
260
|
+
|
|
261
|
+
def get_all_documents_generator(
|
|
262
|
+
self,
|
|
263
|
+
index: Optional[str] = None,
|
|
264
|
+
filters: Optional[FilterType] = None,
|
|
265
|
+
return_embedding: Optional[bool] = None,
|
|
266
|
+
batch_size: Optional[int] = None,
|
|
267
|
+
headers: Optional[Dict[str, str]] = None,
|
|
268
|
+
) -> Generator[Document, None, None]:
|
|
269
|
+
"""
|
|
270
|
+
Get documents from the document store. Under-the-hood, documents are fetched in batches from the
|
|
271
|
+
document store and yielded as individual documents. This method can be used to iteratively process
|
|
272
|
+
a large number of documents without having to load all documents in memory.
|
|
273
|
+
|
|
274
|
+
:param index: Name of the index to get the documents from. If None, the
|
|
275
|
+
DocumentStore's default index (self.index) will be used.
|
|
276
|
+
:param filters: Optional filters to narrow down the search space to documents whose metadata fulfill certain
|
|
277
|
+
conditions.
|
|
278
|
+
Filters are defined as nested dictionaries. The keys of the dictionaries can be a logical
|
|
279
|
+
operator (`"$and"`, `"$or"`, `"$not"`), a comparison operator (`"$eq"`, `"$in"`, `"$gt"`,
|
|
280
|
+
`"$gte"`, `"$lt"`, `"$lte"`) or a metadata field name.
|
|
281
|
+
Logical operator keys take a dictionary of metadata field names and/or logical operators as
|
|
282
|
+
value. Metadata field names take a dictionary of comparison operators as value. Comparison
|
|
283
|
+
operator keys take a single value or (in case of `"$in"`) a list of values as value.
|
|
284
|
+
If no logical operator is provided, `"$and"` is used as default operation. If no comparison
|
|
285
|
+
operator is provided, `"$eq"` (or `"$in"` if the comparison value is a list) is used as default
|
|
286
|
+
operation.
|
|
287
|
+
|
|
288
|
+
__Example__:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
filters = {
|
|
292
|
+
"$and": {
|
|
293
|
+
"type": {"$eq": "article"},
|
|
294
|
+
"date": {"$gte": "2015-01-01", "$lt": "2021-01-01"},
|
|
295
|
+
"rating": {"$gte": 3},
|
|
296
|
+
"$or": {
|
|
297
|
+
"genre": {"$in": ["economy", "politics"]},
|
|
298
|
+
"publisher": {"$eq": "nytimes"}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
:param return_embedding: Whether to return the document embeddings.
|
|
305
|
+
:param batch_size: Not supported.
|
|
306
|
+
"""
|
|
307
|
+
for doc in self.get_all_documents(
|
|
308
|
+
index, filters, return_embedding, batch_size, headers
|
|
309
|
+
):
|
|
310
|
+
yield doc
|
|
311
|
+
|
|
312
|
+
def get_all_labels(
|
|
313
|
+
self,
|
|
314
|
+
index: Optional[str] = None,
|
|
315
|
+
filters: Optional[FilterType] = None,
|
|
316
|
+
headers: Optional[Dict[str, str]] = None,
|
|
317
|
+
) -> List[Label]:
|
|
318
|
+
if headers:
|
|
319
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
320
|
+
|
|
321
|
+
index = index or self.collection_name
|
|
322
|
+
|
|
323
|
+
filters = (
|
|
324
|
+
LogicalFilterClause.parse(filters).convert_to_milvus() if filters else None
|
|
325
|
+
)
|
|
326
|
+
res = self.client.query(
|
|
327
|
+
collection_name=index, filter_expression=filters, partition_keys=[2]
|
|
328
|
+
)
|
|
329
|
+
res_labels = []
|
|
330
|
+
for hit in res:
|
|
331
|
+
hit.pop(EMPTY_FIELD, None)
|
|
332
|
+
hit.pop("embedding", None)
|
|
333
|
+
res_labels.append(Label.from_dict(hit))
|
|
334
|
+
return res_labels
|
|
335
|
+
|
|
336
|
+
def get_document_count(
|
|
337
|
+
self,
|
|
338
|
+
filters: Optional[FilterType] = None,
|
|
339
|
+
index: Optional[str] = None,
|
|
340
|
+
only_documents_without_embedding: bool = False,
|
|
341
|
+
headers: Optional[Dict[str, str]] = None,
|
|
342
|
+
) -> int:
|
|
343
|
+
if headers:
|
|
344
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
345
|
+
|
|
346
|
+
if filters is not None:
|
|
347
|
+
filters = (
|
|
348
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
349
|
+
if filters
|
|
350
|
+
else None
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
index = index or self.collection_name
|
|
354
|
+
if index not in self.client.list_collections():
|
|
355
|
+
self._create_collection(index)
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
if only_documents_without_embedding:
|
|
359
|
+
only_documents_without_embedding = [1]
|
|
360
|
+
else:
|
|
361
|
+
only_documents_without_embedding = [0, 1]
|
|
362
|
+
|
|
363
|
+
return len(
|
|
364
|
+
self.client.query(
|
|
365
|
+
index,
|
|
366
|
+
filter_expression=filters,
|
|
367
|
+
output_fields=[ID_FIELD],
|
|
368
|
+
partition_keys=only_documents_without_embedding,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def get_embedding_count(
|
|
373
|
+
self,
|
|
374
|
+
filters: Optional[FilterType] = None,
|
|
375
|
+
index: Optional[str] = None,
|
|
376
|
+
headers: Optional[Dict[str, str]] = None,
|
|
377
|
+
) -> int:
|
|
378
|
+
if headers:
|
|
379
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
380
|
+
|
|
381
|
+
if filters is not None:
|
|
382
|
+
filters = (
|
|
383
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
384
|
+
if filters
|
|
385
|
+
else None
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
index = index or self.collection_name
|
|
389
|
+
if index not in self.client.list_collections():
|
|
390
|
+
self._create_collection(index)
|
|
391
|
+
return 0
|
|
392
|
+
|
|
393
|
+
return len(
|
|
394
|
+
self.client.query(
|
|
395
|
+
self.collection_name,
|
|
396
|
+
filter_expression=filters,
|
|
397
|
+
output_fields=[ID_FIELD],
|
|
398
|
+
partition_keys=[0],
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def query_by_embedding(
|
|
403
|
+
self,
|
|
404
|
+
query_emb: np.ndarray,
|
|
405
|
+
filters: Optional[FilterType] = None,
|
|
406
|
+
top_k: int = 10,
|
|
407
|
+
index: Optional[str] = None,
|
|
408
|
+
return_embedding: Optional[bool] = None,
|
|
409
|
+
headers: Optional[Dict[str, str]] = None,
|
|
410
|
+
scale_score: bool = True,
|
|
411
|
+
) -> List[Document]:
|
|
412
|
+
if headers:
|
|
413
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
414
|
+
|
|
415
|
+
if self.similarity == "cosine":
|
|
416
|
+
self.normalize_embedding(query_emb)
|
|
417
|
+
|
|
418
|
+
index = index or self.collection_name
|
|
419
|
+
return_embedding = return_embedding or self.return_embedding
|
|
420
|
+
|
|
421
|
+
if index not in self.client.list_collections():
|
|
422
|
+
self._create_collection(index)
|
|
423
|
+
return []
|
|
424
|
+
|
|
425
|
+
if filters is not None:
|
|
426
|
+
filters = (
|
|
427
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
428
|
+
if filters
|
|
429
|
+
else None
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
res = self.client.search(
|
|
433
|
+
collection_name=index,
|
|
434
|
+
data=[query_emb],
|
|
435
|
+
top_k=top_k,
|
|
436
|
+
include_vectors=return_embedding,
|
|
437
|
+
partition_keys=[0],
|
|
438
|
+
filter_expression=filters,
|
|
439
|
+
)
|
|
440
|
+
res_docs = []
|
|
441
|
+
for hit in res[0]:
|
|
442
|
+
hit.pop(EMPTY_FIELD)
|
|
443
|
+
doc = Document.from_dict(hit)
|
|
444
|
+
doc.score = (
|
|
445
|
+
hit.score
|
|
446
|
+
if not scale_score
|
|
447
|
+
else self.scale_to_unit_interval(hit.score, self.similarity)
|
|
448
|
+
)
|
|
449
|
+
res_docs.append(doc)
|
|
450
|
+
|
|
451
|
+
return res_docs
|
|
452
|
+
|
|
453
|
+
def get_label_count(
|
|
454
|
+
self, index: Optional[str] = None, headers: Optional[Dict[str, str]] = None
|
|
455
|
+
) -> int:
|
|
456
|
+
if headers:
|
|
457
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
458
|
+
|
|
459
|
+
index = index or self.collection_name
|
|
460
|
+
if index not in self.client.list_collections():
|
|
461
|
+
self._create_collection(index)
|
|
462
|
+
return 0
|
|
463
|
+
|
|
464
|
+
return len(
|
|
465
|
+
self.client.query(
|
|
466
|
+
index,
|
|
467
|
+
output_fields=[ID_FIELD],
|
|
468
|
+
partition_keys=[2],
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def write_labels(
|
|
473
|
+
self,
|
|
474
|
+
labels: Union[List[Label], List[dict]],
|
|
475
|
+
index: Optional[str] = None,
|
|
476
|
+
headers: Optional[Dict[str, str]] = None,
|
|
477
|
+
batch_size=100,
|
|
478
|
+
):
|
|
479
|
+
if headers:
|
|
480
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
481
|
+
|
|
482
|
+
index = index or self.collection_name
|
|
483
|
+
if index not in self.client.list_collections():
|
|
484
|
+
self._create_collection(index)
|
|
485
|
+
|
|
486
|
+
insert_labels = []
|
|
487
|
+
|
|
488
|
+
for label in labels:
|
|
489
|
+
if isinstance(label, Label):
|
|
490
|
+
label = label.to_json()
|
|
491
|
+
label = json.loads(label)
|
|
492
|
+
label["embedding"] = self.dummy_data
|
|
493
|
+
label[EMPTY_FIELD] = 2
|
|
494
|
+
insert_labels.append(label)
|
|
495
|
+
self.client.insert(
|
|
496
|
+
collection_name=index,
|
|
497
|
+
data=insert_labels,
|
|
498
|
+
batch_size=batch_size,
|
|
499
|
+
progress_bar=self.progress_bar,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def delete_documents(
|
|
503
|
+
self,
|
|
504
|
+
ids: Optional[List[str]] = None,
|
|
505
|
+
filters: Optional[FilterType] = None,
|
|
506
|
+
index: Optional[str] = None,
|
|
507
|
+
headers: Optional[Dict[str, str]] = None,
|
|
508
|
+
):
|
|
509
|
+
if headers:
|
|
510
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
511
|
+
|
|
512
|
+
index = index or self.collection_name
|
|
513
|
+
if index not in self.client.list_collections():
|
|
514
|
+
self._create_collection(index)
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
ids = [] if ids is None else ids
|
|
518
|
+
|
|
519
|
+
if len(ids) != 0 and filters is not None:
|
|
520
|
+
filters = (
|
|
521
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
522
|
+
if filters
|
|
523
|
+
else None
|
|
524
|
+
)
|
|
525
|
+
res = self.client.query(
|
|
526
|
+
collection_name=index,
|
|
527
|
+
filter_expression=filters,
|
|
528
|
+
output_fields=[ID_FIELD],
|
|
529
|
+
partition_keys=[0, 1],
|
|
530
|
+
)
|
|
531
|
+
filter_ids = [hit[ID_FIELD] for hit in res]
|
|
532
|
+
ids = list(set(ids).intersection(set(filter_ids)))
|
|
533
|
+
|
|
534
|
+
elif filters is not None and len(ids) == 0:
|
|
535
|
+
filters = (
|
|
536
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
537
|
+
if filters
|
|
538
|
+
else None
|
|
539
|
+
)
|
|
540
|
+
res = self.client.query(
|
|
541
|
+
collection_name=index,
|
|
542
|
+
filter_expression=filters,
|
|
543
|
+
output_fields=[ID_FIELD],
|
|
544
|
+
partition_keys=[0, 1],
|
|
545
|
+
)
|
|
546
|
+
ids = [hit[ID_FIELD] for hit in res]
|
|
547
|
+
|
|
548
|
+
elif filters is None and len(ids) != 0:
|
|
549
|
+
pass
|
|
550
|
+
else:
|
|
551
|
+
self.delete_all_documents(index=index)
|
|
552
|
+
|
|
553
|
+
self.client.delete(
|
|
554
|
+
collection_name=index,
|
|
555
|
+
field_name=ID_FIELD,
|
|
556
|
+
values=ids,
|
|
557
|
+
partition_keys=[0, 1],
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
def delete_labels(
|
|
561
|
+
self,
|
|
562
|
+
ids: Optional[List[str]] = None,
|
|
563
|
+
filters: Optional[FilterType] = None,
|
|
564
|
+
index: Optional[str] = None,
|
|
565
|
+
headers: Optional[Dict[str, str]] = None,
|
|
566
|
+
):
|
|
567
|
+
if headers:
|
|
568
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
569
|
+
|
|
570
|
+
index = index or self.collection_name
|
|
571
|
+
if index not in self.client.list_collections():
|
|
572
|
+
self._create_collection(index)
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
ids = [] if ids is None else ids
|
|
576
|
+
|
|
577
|
+
if len(ids) != 0 and filters is not None:
|
|
578
|
+
filters = (
|
|
579
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
580
|
+
if filters
|
|
581
|
+
else None
|
|
582
|
+
)
|
|
583
|
+
res = self.client.query(
|
|
584
|
+
collection_name=index,
|
|
585
|
+
filter_expression=filters,
|
|
586
|
+
output_fields=[ID_FIELD],
|
|
587
|
+
partition_keys=[2],
|
|
588
|
+
)
|
|
589
|
+
filter_ids = [hit[ID_FIELD] for hit in res]
|
|
590
|
+
ids = list(set(ids).intersection(set(filter_ids)))
|
|
591
|
+
|
|
592
|
+
elif filters is not None and len(ids) == 0:
|
|
593
|
+
filters = (
|
|
594
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
595
|
+
if filters
|
|
596
|
+
else None
|
|
597
|
+
)
|
|
598
|
+
res = self.client.query(
|
|
599
|
+
collection_name=index,
|
|
600
|
+
filter_expression=filters,
|
|
601
|
+
output_fields=[ID_FIELD],
|
|
602
|
+
partition_keys=[2],
|
|
603
|
+
)
|
|
604
|
+
ids = [hit[ID_FIELD] for hit in res]
|
|
605
|
+
|
|
606
|
+
elif filters is None and len(ids) != 0:
|
|
607
|
+
pass
|
|
608
|
+
else:
|
|
609
|
+
self.delete_all_labels(index=index)
|
|
610
|
+
|
|
611
|
+
self.client.delete(
|
|
612
|
+
collection_name=index, field_name=ID_FIELD, values=ids, partition_keys=[2]
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def delete_all_documents(
|
|
616
|
+
self,
|
|
617
|
+
index: Optional[str] = None,
|
|
618
|
+
filters: Optional[FilterType] = None,
|
|
619
|
+
headers: Optional[Dict[str, str]] = None,
|
|
620
|
+
):
|
|
621
|
+
if headers:
|
|
622
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
623
|
+
|
|
624
|
+
index = index or self.collection_name
|
|
625
|
+
if index not in self.client.list_collections():
|
|
626
|
+
self._create_collection(index)
|
|
627
|
+
return
|
|
628
|
+
if filters is not None:
|
|
629
|
+
filters = (
|
|
630
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
631
|
+
if filters
|
|
632
|
+
else None
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
filters = f'{ID_FIELD} >= ""'
|
|
636
|
+
|
|
637
|
+
res = self.client.query(
|
|
638
|
+
collection_name=index,
|
|
639
|
+
filter_expression=filters,
|
|
640
|
+
output_fields=[ID_FIELD],
|
|
641
|
+
partition_keys=[0, 1],
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
ids = [hit[ID_FIELD] for hit in res]
|
|
645
|
+
|
|
646
|
+
self.client.delete(
|
|
647
|
+
collection_name=index,
|
|
648
|
+
field_name=ID_FIELD,
|
|
649
|
+
values=ids,
|
|
650
|
+
partition_keys=[0, 1],
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def delete_all_labels(
|
|
654
|
+
self,
|
|
655
|
+
index: Optional[str] = None,
|
|
656
|
+
filters: Optional[FilterType] = None,
|
|
657
|
+
headers: Optional[Dict[str, str]] = None,
|
|
658
|
+
):
|
|
659
|
+
if headers:
|
|
660
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
661
|
+
|
|
662
|
+
index = index or self.collection_name
|
|
663
|
+
if index not in self.client.list_collections():
|
|
664
|
+
self._create_collection(index)
|
|
665
|
+
return
|
|
666
|
+
if filters is not None:
|
|
667
|
+
filters = (
|
|
668
|
+
LogicalFilterClause.parse(filters).convert_to_milvus()
|
|
669
|
+
if filters
|
|
670
|
+
else None
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
filters = f'{ID_FIELD} >= ""'
|
|
674
|
+
|
|
675
|
+
res = self.client.query(
|
|
676
|
+
collection_name=index,
|
|
677
|
+
filter_expression=filters,
|
|
678
|
+
output_fields=[ID_FIELD],
|
|
679
|
+
partition_keys=[2],
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
ids = [hit[ID_FIELD] for hit in res]
|
|
683
|
+
|
|
684
|
+
self.client.delete(
|
|
685
|
+
collection_name=index, field_name=ID_FIELD, values=ids, partition_keys=[2]
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
def delete_index(self, index: Optional[str] = None):
|
|
689
|
+
"""
|
|
690
|
+
Delete an existing index. The index including all data will be removed.
|
|
691
|
+
|
|
692
|
+
:param index: The name of the index to delete.
|
|
693
|
+
:return: None
|
|
694
|
+
"""
|
|
695
|
+
index = index or self.collection_name
|
|
696
|
+
self.client.drop_collection(index)
|
|
697
|
+
|
|
698
|
+
def _create_document_field_map(self) -> Dict:
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
def get_documents_by_id(
|
|
702
|
+
self,
|
|
703
|
+
ids: List[str],
|
|
704
|
+
index: Optional[str] = None,
|
|
705
|
+
batch_size: int = 10_000,
|
|
706
|
+
headers: Optional[Dict[str, str]] = None,
|
|
707
|
+
) -> List[Document]:
|
|
708
|
+
if headers:
|
|
709
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
710
|
+
|
|
711
|
+
logger.warn("Batch size ignored for queries")
|
|
712
|
+
|
|
713
|
+
index = index or self.collection_name
|
|
714
|
+
if index not in self.client.list_collections():
|
|
715
|
+
self._create_collection(index)
|
|
716
|
+
return []
|
|
717
|
+
res = self.client.fetch(
|
|
718
|
+
collection_name=index,
|
|
719
|
+
field_name=ID_FIELD,
|
|
720
|
+
values=ids,
|
|
721
|
+
partition_keys=[0, 1],
|
|
722
|
+
)
|
|
723
|
+
res_docs = []
|
|
724
|
+
for hit in res:
|
|
725
|
+
hit.pop(EMPTY_FIELD)
|
|
726
|
+
res_docs.append(Document.from_dict(hit))
|
|
727
|
+
return res_docs
|
|
728
|
+
|
|
729
|
+
def get_document_by_id(
|
|
730
|
+
self,
|
|
731
|
+
id: str,
|
|
732
|
+
index: Optional[str] = None,
|
|
733
|
+
headers: Optional[Dict[str, str]] = None,
|
|
734
|
+
) -> Optional[Document]:
|
|
735
|
+
if headers:
|
|
736
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
737
|
+
|
|
738
|
+
docs = self.get_documents_by_id(ids=[id], index=index, headers=headers)
|
|
739
|
+
if len(docs) == 0:
|
|
740
|
+
return None
|
|
741
|
+
else:
|
|
742
|
+
return docs[0]
|
|
743
|
+
|
|
744
|
+
def update_document_meta(
|
|
745
|
+
self, id: str, meta: Dict[str, Any], index: Optional[str] = None
|
|
746
|
+
):
|
|
747
|
+
index = index or self.collection_name
|
|
748
|
+
if index not in self.client.list_collections():
|
|
749
|
+
self._create_collection(index)
|
|
750
|
+
return
|
|
751
|
+
res = self.client.fetch(
|
|
752
|
+
collection_name=index,
|
|
753
|
+
field_name=ID_FIELD,
|
|
754
|
+
values=[id],
|
|
755
|
+
partition_keys=[0, 1],
|
|
756
|
+
include_vectors=True,
|
|
757
|
+
)
|
|
758
|
+
if len(res) != 0:
|
|
759
|
+
res[0].update(meta)
|
|
760
|
+
self.client.delete(
|
|
761
|
+
collection_name=index,
|
|
762
|
+
field_name=ID_FIELD,
|
|
763
|
+
values=[id],
|
|
764
|
+
partition_keys=[0, 1],
|
|
765
|
+
)
|
|
766
|
+
self.client.insert(collection_name=index, data=[res[0]])
|
|
767
|
+
|
|
768
|
+
def _handle_duplicate_documents(
|
|
769
|
+
self,
|
|
770
|
+
documents: List[Document],
|
|
771
|
+
index: Optional[str] = None,
|
|
772
|
+
duplicate_documents: Optional[str] = None,
|
|
773
|
+
headers: Optional[Dict[str, str]] = None,
|
|
774
|
+
):
|
|
775
|
+
"""
|
|
776
|
+
Checks whether any of the passed documents is already existing in the chosen index and returns a list of
|
|
777
|
+
documents that are not in the index yet.
|
|
778
|
+
|
|
779
|
+
:param documents: A list of Haystack Document objects.
|
|
780
|
+
:param index: name of the index
|
|
781
|
+
:param duplicate_documents: Handle duplicates document based on parameter options.
|
|
782
|
+
Parameter options : ( 'skip','overwrite','fail')
|
|
783
|
+
skip (default option): Ignore the duplicates documents
|
|
784
|
+
overwrite: Update any existing documents with the same ID when adding documents.
|
|
785
|
+
fail: an error is raised if the document ID of the document being added already
|
|
786
|
+
exists.
|
|
787
|
+
:param headers: Custom HTTP headers to pass to document store client if supported (e.g. {'Authorization': 'Basic YWRtaW46cm9vdA=='} for basic authentication)
|
|
788
|
+
:return: A list of Haystack Document objects.
|
|
789
|
+
"""
|
|
790
|
+
if headers:
|
|
791
|
+
raise NotImplementedError("MilvusDocumentStore does not support headers.")
|
|
792
|
+
|
|
793
|
+
index = index or self.collection_name
|
|
794
|
+
|
|
795
|
+
documents = self._drop_duplicate_documents(documents, index)
|
|
796
|
+
documents_found = self.get_documents_by_id(
|
|
797
|
+
ids=[doc.id for doc in documents], index=index, headers=headers
|
|
798
|
+
)
|
|
799
|
+
ids_exist_in_db: List[str] = [doc.id for doc in documents_found]
|
|
800
|
+
|
|
801
|
+
if len(ids_exist_in_db) > 0 and duplicate_documents == "fail":
|
|
802
|
+
raise DuplicateDocumentError(
|
|
803
|
+
f"Document with ids '{', '.join(ids_exist_in_db)} already exists"
|
|
804
|
+
f" in index = '{index}'."
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
if len(ids_exist_in_db) > 0 and duplicate_documents == "overwrite":
|
|
808
|
+
self.delete_documents(ids=ids_exist_in_db, index=index)
|
|
809
|
+
return documents
|
|
810
|
+
|
|
811
|
+
documents = list(filter(lambda doc: doc.id not in ids_exist_in_db, documents))
|
|
812
|
+
|
|
813
|
+
return documents
|
|
814
|
+
|
|
815
|
+
def _drop_duplicate_documents(
|
|
816
|
+
self, documents: List[Document], index: Optional[str] = None
|
|
817
|
+
) -> List[Document]:
|
|
818
|
+
"""
|
|
819
|
+
Drop duplicates documents based on same hash ID
|
|
820
|
+
|
|
821
|
+
:param documents: A list of Haystack Document objects.
|
|
822
|
+
:param index: name of the index
|
|
823
|
+
:return: A list of Haystack Document objects.
|
|
824
|
+
"""
|
|
825
|
+
_hash_ids: Set = set([])
|
|
826
|
+
_documents: List[Document] = []
|
|
827
|
+
|
|
828
|
+
for document in documents:
|
|
829
|
+
if isinstance(document, dict):
|
|
830
|
+
document = Document.from_dict(document)
|
|
831
|
+
if document.id in _hash_ids:
|
|
832
|
+
logger.info(
|
|
833
|
+
"Duplicate Documents: Document with id '%s' already exists in collection '%s'",
|
|
834
|
+
document.id,
|
|
835
|
+
index or self.collection_name,
|
|
836
|
+
)
|
|
837
|
+
continue
|
|
838
|
+
_documents.append(document)
|
|
839
|
+
_hash_ids.add(document.id)
|
|
840
|
+
|
|
841
|
+
return _documents
|