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.
@@ -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