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,4 @@
1
+ # SPDX-FileCopyrightText: 2023-present Tuana Celik <tuana.celik@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.0.1"
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ from .milvus import MilvusDocumentStore
@@ -0,0 +1,478 @@
1
+ from typing import Union, List, Dict
2
+ from abc import ABC, abstractmethod
3
+ from collections import defaultdict
4
+
5
+
6
+ from haystack.errors import FilterError
7
+
8
+
9
+ def nested_defaultdict() -> defaultdict:
10
+ """
11
+ Data structure that recursively adds a dictionary as value if a key does not exist. Advantage: In nested dictionary
12
+ structures, we don't need to check if a key already exists (which can become hard to maintain in nested dictionaries
13
+ with many levels) but access the existing value if a key exists and create an empty dictionary if a key does not
14
+ exist.
15
+ """
16
+ return defaultdict(nested_defaultdict)
17
+
18
+
19
+ class LogicalFilterClause(ABC):
20
+ """
21
+ Class that is able to parse a filter and convert it to the format that the underlying databases of our
22
+ DocumentStores require.
23
+
24
+ Filters are defined as nested dictionaries. The keys of the dictionaries can be a logical
25
+ operator (`"$and"`, `"$or"`, `"$not"`), a comparison operator (`"$eq"`, `"$in"`, `"$gt"`, `"$gte"`, `"$lt"`,
26
+ `"$lte"`) or a metadata field name.
27
+ Logical operator keys take a dictionary of metadata field names and/or logical operators as
28
+ value. Metadata field names take a dictionary of comparison operators as value. Comparison
29
+ operator keys take a single value or (in case of `"$in"`) a list of values as value.
30
+ If no logical operator is provided, `"$and"` is used as default operation. If no comparison
31
+ operator is provided, `"$eq"` (or `"$in"` if the comparison value is a list) is used as default
32
+ operation.
33
+ Example:
34
+ ```python
35
+ filters = {
36
+ "$and": {
37
+ "type": {"$eq": "article"},
38
+ "date": {"$gte": "2015-01-01", "$lt": "2021-01-01"},
39
+ "rating": {"$gte": 3},
40
+ "$or": {
41
+ "genre": {"$in": ["economy", "politics"]},
42
+ "publisher": {"$eq": "nytimes"}
43
+ }
44
+ }
45
+ }
46
+ # or simpler using default operators
47
+ filters = {
48
+ "type": "article",
49
+ "date": {"$gte": "2015-01-01", "$lt": "2021-01-01"},
50
+ "rating": {"$gte": 3},
51
+ "$or": {
52
+ "genre": ["economy", "politics"],
53
+ "publisher": "nytimes"
54
+ }
55
+ }
56
+ ```
57
+
58
+ To use the same logical operator multiple times on the same level, logical operators take optionally a list of
59
+ dictionaries as value.
60
+
61
+ Example:
62
+ ```python
63
+ filters = {
64
+ "$or": [
65
+ {
66
+ "$and": {
67
+ "Type": "News Paper",
68
+ "Date": {
69
+ "$lt": "2019-01-01"
70
+ }
71
+ }
72
+ },
73
+ {
74
+ "$and": {
75
+ "Type": "Blog Post",
76
+ "Date": {
77
+ "$gte": "2019-01-01"
78
+ }
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ """
86
+
87
+ def __init__(
88
+ self, conditions: List[Union["LogicalFilterClause", "ComparisonOperation"]]
89
+ ):
90
+ self.conditions = conditions
91
+
92
+ @abstractmethod
93
+ def evaluate(self, fields) -> bool:
94
+ pass
95
+
96
+ @classmethod
97
+ def parse(
98
+ cls, filter_term: Union[dict, List[dict]]
99
+ ) -> Union["LogicalFilterClause", "ComparisonOperation"]:
100
+ """
101
+ Parses a filter dictionary/list and returns a LogicalFilterClause instance.
102
+
103
+ :param filter_term: Dictionary or list that contains the filter definition.
104
+ """
105
+ conditions: List[Union[LogicalFilterClause, ComparisonOperation]] = []
106
+
107
+ if isinstance(filter_term, dict):
108
+ filter_term = [filter_term]
109
+ for item in filter_term:
110
+ for key, value in item.items():
111
+ if key == "$not":
112
+ conditions.append(NotOperation.parse(value))
113
+ elif key == "$and":
114
+ conditions.append(AndOperation.parse(value))
115
+ elif key == "$or":
116
+ conditions.append(OrOperation.parse(value))
117
+ # Key needs to be a metadata field
118
+ else:
119
+ conditions.extend(ComparisonOperation.parse(key, value))
120
+
121
+ if cls == LogicalFilterClause:
122
+ if len(conditions) == 1:
123
+ return conditions[0]
124
+ else:
125
+ return AndOperation(conditions)
126
+ else:
127
+ return cls(conditions)
128
+
129
+ def convert_to_milvus(self):
130
+ """
131
+ Converts the LogicalFilterClause instance to a Milvus filter.
132
+ """
133
+ pass
134
+
135
+ @abstractmethod
136
+ def invert(self) -> Union["LogicalFilterClause", "ComparisonOperation"]:
137
+ """
138
+ Inverts the LogicalOperation instance.
139
+ Necessary for Weaviate as Weaviate doesn't seem to support the 'Not' operator anymore.
140
+ (https://github.com/semi-technologies/weaviate/issues/1717)
141
+ """
142
+ pass
143
+
144
+
145
+ class ComparisonOperation(ABC):
146
+ def __init__(
147
+ self, field_name: str, comparison_value: Union[str, int, float, bool, List]
148
+ ):
149
+ self.field_name = field_name
150
+ self.comparison_value = comparison_value
151
+
152
+ @abstractmethod
153
+ def evaluate(self, fields) -> bool:
154
+ pass
155
+
156
+ @classmethod
157
+ def parse(
158
+ cls, field_name, comparison_clause: Union[Dict, List, str, float]
159
+ ) -> List["ComparisonOperation"]:
160
+ comparison_operations: List[ComparisonOperation] = []
161
+
162
+ if isinstance(comparison_clause, dict):
163
+ for comparison_operation, comparison_value in comparison_clause.items():
164
+ if comparison_operation == "$eq":
165
+ comparison_operations.append(
166
+ EqOperation(field_name, comparison_value)
167
+ )
168
+ elif comparison_operation == "$in":
169
+ comparison_operations.append(
170
+ InOperation(field_name, comparison_value)
171
+ )
172
+ elif comparison_operation == "$ne":
173
+ comparison_operations.append(
174
+ NeOperation(field_name, comparison_value)
175
+ )
176
+ elif comparison_operation == "$nin":
177
+ comparison_operations.append(
178
+ NinOperation(field_name, comparison_value)
179
+ )
180
+ elif comparison_operation == "$gt":
181
+ comparison_operations.append(
182
+ GtOperation(field_name, comparison_value)
183
+ )
184
+ elif comparison_operation == "$gte":
185
+ comparison_operations.append(
186
+ GteOperation(field_name, comparison_value)
187
+ )
188
+ elif comparison_operation == "$lt":
189
+ comparison_operations.append(
190
+ LtOperation(field_name, comparison_value)
191
+ )
192
+ elif comparison_operation == "$lte":
193
+ comparison_operations.append(
194
+ LteOperation(field_name, comparison_value)
195
+ )
196
+
197
+ # No comparison operator is given, so we use the default operators "$in" if the comparison value is a list and
198
+ # "$eq" in every other case
199
+ elif isinstance(comparison_clause, list):
200
+ comparison_operations.append(InOperation(field_name, comparison_clause))
201
+ else:
202
+ comparison_operations.append((EqOperation(field_name, comparison_clause)))
203
+
204
+ return comparison_operations
205
+
206
+ def convert_to_milvus(self):
207
+ """
208
+ Converts the ComparisonOperation instance to a Milvus comparison operator.
209
+ """
210
+ pass
211
+
212
+ @abstractmethod
213
+ def invert(self) -> "ComparisonOperation":
214
+ """
215
+ Inverts the ComparisonOperation.
216
+ Necessary for Weaviate as Weaviate doesn't seem to support the 'Not' operator anymore.
217
+ (https://github.com/semi-technologies/weaviate/issues/1717)
218
+ """
219
+ pass
220
+
221
+
222
+ class NotOperation(LogicalFilterClause):
223
+ """
224
+ Handles conversion of logical 'NOT' operations.
225
+ """
226
+
227
+ def evaluate(self, fields) -> bool:
228
+ return not any(condition.evaluate(fields) for condition in self.conditions)
229
+
230
+ def convert_to_milvus(self) -> str:
231
+ conditions = [
232
+ condition.invert().convert_to_milvus() for condition in self.conditions
233
+ ]
234
+ if len(conditions) > 1:
235
+ # Conditions in self.conditions are by default combined with AND which becomes OR according to DeMorgan
236
+ return f"({' or '.join(conditions)})"
237
+ else:
238
+ return conditions[0]
239
+
240
+ def invert(self) -> Union[LogicalFilterClause, ComparisonOperation]:
241
+ # This method is called when a "$not" operation is embedded in another "$not" operation. Therefore, we don't
242
+ # invert the operations here, as two "$not" operation annihilate each other.
243
+ # (If we have more than one condition, we return an AndOperation, the default logical operation for combining
244
+ # multiple conditions.)
245
+ if len(self.conditions) > 1:
246
+ return AndOperation(self.conditions)
247
+ else:
248
+ return self.conditions[0]
249
+
250
+
251
+ class AndOperation(LogicalFilterClause):
252
+ """
253
+ Handles conversion of logical 'AND' operations.
254
+ """
255
+
256
+ def evaluate(self, fields) -> bool:
257
+ return all(condition.evaluate(fields) for condition in self.conditions)
258
+
259
+ def convert_to_milvus(self) -> Dict[str, Union[str, List[Dict]]]:
260
+ conditions = [condition.convert_to_milvus() for condition in self.conditions]
261
+ return f"({' and '.join(conditions)})"
262
+
263
+ def invert(self) -> "OrOperation":
264
+ return OrOperation([condition.invert() for condition in self.conditions])
265
+
266
+
267
+ class OrOperation(LogicalFilterClause):
268
+ """
269
+ Handles conversion of logical 'OR' operations.
270
+ """
271
+
272
+ def evaluate(self, fields) -> bool:
273
+ return any(condition.evaluate(fields) for condition in self.conditions)
274
+
275
+ def convert_to_milvus(self) -> str:
276
+ conditions = [condition.convert_to_milvus() for condition in self.conditions]
277
+ return f"({' or '.join(conditions)})"
278
+
279
+ def invert(self) -> AndOperation:
280
+ return AndOperation([condition.invert() for condition in self.conditions])
281
+
282
+
283
+ class EqOperation(ComparisonOperation):
284
+ """
285
+ Handles conversion of the '$eq' comparison operation.
286
+ """
287
+
288
+ def evaluate(self, fields) -> bool:
289
+ if self.field_name not in fields:
290
+ return False
291
+ return fields[self.field_name] == self.comparison_value
292
+
293
+ def convert_to_milvus(self) -> str:
294
+ if isinstance(self.comparison_value, str):
295
+ comp_val = '"' + str(self.comparison_value) + '"'
296
+ else:
297
+ comp_val = self.comparison_value
298
+ return f"({self.field_name} == {comp_val})"
299
+
300
+ def invert(self) -> "NeOperation":
301
+ return NeOperation(self.field_name, self.comparison_value)
302
+
303
+
304
+ class InOperation(ComparisonOperation):
305
+ """
306
+ Handles conversion of the '$in' comparison operation.
307
+ """
308
+
309
+ def evaluate(self, fields) -> bool:
310
+ if self.field_name not in fields:
311
+ return False
312
+ return fields[self.field_name] in self.comparison_value # type: ignore
313
+ # is only initialized with lists, but changing the type annotation would mean duplicating __init__
314
+
315
+ def convert_to_milvus(self) -> str:
316
+ if not isinstance(self.comparison_value, list):
317
+ raise FilterError("'$in' operation requires comparison value to be a list.")
318
+ comp_val = []
319
+ for x in self.comparison_value:
320
+ if isinstance(x, str):
321
+ comp_val.append('"' + str(x) + '"')
322
+ else:
323
+ comp_val.append(x)
324
+
325
+ return f"""({self.field_name} in [{','.join(comp_val)}])"""
326
+
327
+ def invert(self) -> "NinOperation":
328
+ return NinOperation(self.field_name, self.comparison_value)
329
+
330
+
331
+ class NeOperation(ComparisonOperation):
332
+ """
333
+ Handles conversion of the '$ne' comparison operation.
334
+ """
335
+
336
+ def evaluate(self, fields) -> bool:
337
+ if self.field_name not in fields:
338
+ return False
339
+ return fields[self.field_name] != self.comparison_value
340
+
341
+ def convert_to_milvus(self) -> str:
342
+ if isinstance(self.comparison_value, str):
343
+ comp_val = '"' + str(self.comparison_value) + '"'
344
+ else:
345
+ comp_val = self.comparison_value
346
+ return f"({self.field_name} != {comp_val})"
347
+
348
+ def invert(self) -> "EqOperation":
349
+ return EqOperation(self.field_name, self.comparison_value)
350
+
351
+
352
+ class NinOperation(ComparisonOperation):
353
+ """
354
+ Handles conversion of the '$nin' comparison operation.
355
+ """
356
+
357
+ def evaluate(self, fields) -> bool:
358
+ if self.field_name not in fields:
359
+ return False
360
+ return fields[self.field_name] not in self.comparison_value # type: ignore
361
+ # is only initialized with lists, but changing the type annotation would mean duplicating __init__
362
+
363
+ def convert_to_milvus(self) -> str:
364
+ if not isinstance(self.comparison_value, list):
365
+ raise FilterError(
366
+ "'$nin' operation requires comparison value to be a list."
367
+ )
368
+ comp_val = []
369
+ for x in self.comparison_value:
370
+ if isinstance(x, str):
371
+ comp_val.append('"' + str(x) + '"')
372
+ else:
373
+ comp_val.append(x)
374
+
375
+ return f"""({self.field_name} not in [{','.join(comp_val)}])"""
376
+
377
+ def invert(self) -> "InOperation":
378
+ return InOperation(self.field_name, self.comparison_value)
379
+
380
+
381
+ class GtOperation(ComparisonOperation):
382
+ """
383
+ Handles conversion of the '$gt' comparison operation.
384
+ """
385
+
386
+ def evaluate(self, fields) -> bool:
387
+ if self.field_name not in fields:
388
+ return False
389
+ return fields[self.field_name] > self.comparison_value
390
+
391
+ def convert_to_milvus(self) -> str:
392
+ if not isinstance(self.comparison_value, (float, int, str)):
393
+ raise FilterError(
394
+ "Comparison value for '$gt' operation must be a float or int or str."
395
+ )
396
+ if isinstance(self.comparison_value, str):
397
+ comp_val = '"' + str(self.comparison_value) + '"'
398
+ else:
399
+ comp_val = self.comparison_value
400
+ return f"({self.field_name} > {comp_val})"
401
+
402
+ def invert(self) -> "LteOperation":
403
+ return LteOperation(self.field_name, self.comparison_value)
404
+
405
+
406
+ class GteOperation(ComparisonOperation):
407
+ """
408
+ Handles conversion of the '$gte' comparison operation.
409
+ """
410
+
411
+ def evaluate(self, fields) -> bool:
412
+ if self.field_name not in fields:
413
+ return False
414
+ return fields[self.field_name] >= self.comparison_value
415
+
416
+ def convert_to_milvus(self) -> str:
417
+ if not isinstance(self.comparison_value, (float, int, str)):
418
+ raise FilterError(
419
+ "Comparison value for '$gte' operation must be a float or int or str."
420
+ )
421
+ if isinstance(self.comparison_value, str):
422
+ comp_val = '"' + str(self.comparison_value) + '"'
423
+ else:
424
+ comp_val = self.comparison_value
425
+ return f"({self.field_name} >= {comp_val})"
426
+
427
+ def invert(self) -> "LtOperation":
428
+ return LtOperation(self.field_name, self.comparison_value)
429
+
430
+
431
+ class LtOperation(ComparisonOperation):
432
+ """
433
+ Handles conversion of the '$lt' comparison operation.
434
+ """
435
+
436
+ def evaluate(self, fields) -> bool:
437
+ if self.field_name not in fields:
438
+ return False
439
+ return fields[self.field_name] < self.comparison_value
440
+
441
+ def convert_to_milvus(self) -> str:
442
+ if not isinstance(self.comparison_value, (float, int, str)):
443
+ raise FilterError(
444
+ "Comparison value for '$lt' operation must be a float or int or str."
445
+ )
446
+ if isinstance(self.comparison_value, str):
447
+ comp_val = '"' + str(self.comparison_value) + '"'
448
+ else:
449
+ comp_val = self.comparison_value
450
+ return f"({self.field_name} < {comp_val})"
451
+
452
+ def invert(self) -> "GteOperation":
453
+ return GteOperation(self.field_name, self.comparison_value)
454
+
455
+
456
+ class LteOperation(ComparisonOperation):
457
+ """
458
+ Handles conversion of the '$lte' comparison operation.
459
+ """
460
+
461
+ def evaluate(self, fields) -> bool:
462
+ if self.field_name not in fields:
463
+ return False
464
+ return fields[self.field_name] <= self.comparison_value
465
+
466
+ def convert_to_milvus(self) -> str:
467
+ if not isinstance(self.comparison_value, (float, int, str)):
468
+ raise FilterError(
469
+ "Comparison value for '$lte' operation must be a float or int or str."
470
+ )
471
+ if isinstance(self.comparison_value, str):
472
+ comp_val = '"' + str(self.comparison_value) + '"'
473
+ else:
474
+ comp_val = self.comparison_value
475
+ return f"({self.field_name} <= {comp_val})"
476
+
477
+ def invert(self) -> "GtOperation":
478
+ return GtOperation(self.field_name, self.comparison_value)