@mastra/qdrant 0.0.0-commonjs-20250227130920

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,290 @@
1
+ import { BaseFilterTranslator } from '@mastra/core/filter';
2
+ import type { FieldCondition, Filter, LogicalOperator, OperatorSupport } from '@mastra/core/filter';
3
+
4
+ /**
5
+ * Translates MongoDB-style filters to Qdrant compatible filters.
6
+ *
7
+ * Key transformations:
8
+ * - $and -> must
9
+ * - $or -> should
10
+ * - $not -> must_not
11
+ * - { field: { $op: value } } -> { key: field, match/range: { value/gt/lt: value } }
12
+ *
13
+ * Custom operators (Qdrant-specific):
14
+ * - $count -> values_count (array length/value count)
15
+ * - $geo -> geo filters (box, radius, polygon)
16
+ * - $hasId -> has_id filter
17
+ * - $nested -> nested object filters
18
+ * - $hasVector -> vector existence check
19
+ * - $datetime -> RFC 3339 datetime range
20
+ * - $null -> is_null check
21
+ * - $empty -> is_empty check
22
+ */
23
+ export class QdrantFilterTranslator extends BaseFilterTranslator {
24
+ protected override isLogicalOperator(key: string): key is LogicalOperator {
25
+ return super.isLogicalOperator(key) || key === '$hasId' || key === '$hasVector';
26
+ }
27
+
28
+ protected override getSupportedOperators(): OperatorSupport {
29
+ return {
30
+ ...BaseFilterTranslator.DEFAULT_OPERATORS,
31
+ logical: ['$and', '$or', '$not'],
32
+ array: ['$in', '$nin'],
33
+ regex: ['$regex'],
34
+ custom: ['$count', '$geo', '$nested', '$datetime', '$null', '$empty', '$hasId', '$hasVector'],
35
+ };
36
+ }
37
+
38
+ translate(filter?: Filter): Filter | undefined {
39
+ if (this.isEmpty(filter)) return filter;
40
+ this.validateFilter(filter as Filter);
41
+ return this.translateNode(filter);
42
+ }
43
+
44
+ private createCondition(type: string, value: any, fieldKey?: string) {
45
+ const condition = { [type]: value };
46
+ return fieldKey ? { key: fieldKey, ...condition } : condition;
47
+ }
48
+
49
+ private translateNode(node: Filter | FieldCondition, isNested: boolean = false, fieldKey?: string): any {
50
+ if (!this.isEmpty(node) && typeof node === 'object' && 'must' in node) {
51
+ return node;
52
+ }
53
+
54
+ if (this.isPrimitive(node)) {
55
+ if (node === null) {
56
+ return { is_null: { key: fieldKey } };
57
+ }
58
+ return this.createCondition('match', { value: this.normalizeComparisonValue(node) }, fieldKey);
59
+ }
60
+
61
+ if (this.isRegex(node)) {
62
+ throw new Error('Direct regex pattern format is not supported in Qdrant');
63
+ }
64
+
65
+ if (Array.isArray(node)) {
66
+ return node.length === 0
67
+ ? { is_empty: { key: fieldKey } }
68
+ : this.createCondition('match', { any: this.normalizeArrayValues(node) }, fieldKey);
69
+ }
70
+
71
+ const entries = Object.entries(node as Record<string, any>);
72
+
73
+ // Handle logical operators first
74
+ const logicalResult = this.handleLogicalOperators(entries, isNested);
75
+ if (logicalResult) {
76
+ return logicalResult;
77
+ }
78
+
79
+ // Handle field conditions
80
+ const { conditions, range, matchCondition } = this.handleFieldConditions(entries, fieldKey);
81
+
82
+ if (Object.keys(range).length > 0) {
83
+ conditions.push({ key: fieldKey, range });
84
+ }
85
+
86
+ if (matchCondition) {
87
+ conditions.push({ key: fieldKey, match: matchCondition });
88
+ }
89
+
90
+ return this.buildFinalConditions(conditions, isNested);
91
+ }
92
+
93
+ private buildFinalConditions(conditions: any[], isNested: boolean): any {
94
+ if (conditions.length === 0) {
95
+ return {};
96
+ } else if (conditions.length === 1 && isNested) {
97
+ return conditions[0];
98
+ } else {
99
+ return { must: conditions };
100
+ }
101
+ }
102
+
103
+ private handleLogicalOperators(entries: [string, any][], isNested: boolean): any | null {
104
+ const firstKey = entries[0]?.[0];
105
+
106
+ if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
107
+ const [key, value] = entries[0]!;
108
+ const qdrantOp = this.getQdrantLogicalOp(key);
109
+ return {
110
+ [qdrantOp]: Array.isArray(value)
111
+ ? value.map(v => this.translateNode(v, true))
112
+ : [this.translateNode(value, true)],
113
+ };
114
+ }
115
+
116
+ if (
117
+ entries.length > 1 &&
118
+ !isNested &&
119
+ entries.every(([key]) => !this.isOperator(key) && !this.isCustomOperator(key))
120
+ ) {
121
+ return {
122
+ must: entries.map(([key, value]) => this.translateNode(value, true, key)),
123
+ };
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ private handleFieldConditions(
130
+ entries: [string, any][],
131
+ fieldKey?: string,
132
+ ): { conditions: any[]; range: Record<string, any>; matchCondition: Record<string, any> | null } {
133
+ const conditions = [];
134
+ let range: Record<string, any> = {};
135
+ let matchCondition: Record<string, any> | null = null;
136
+
137
+ for (const [key, value] of entries) {
138
+ if (this.isCustomOperator(key)) {
139
+ const customOp = this.translateCustomOperator(key, value, fieldKey);
140
+ conditions.push(customOp);
141
+ } else if (this.isOperator(key)) {
142
+ const opResult = this.translateOperatorValue(key, value);
143
+ if (opResult.range) {
144
+ Object.assign(range, opResult.range);
145
+ } else {
146
+ matchCondition = opResult;
147
+ }
148
+ } else {
149
+ const nestedKey = fieldKey ? `${fieldKey}.${key}` : key;
150
+ const nestedCondition = this.translateNode(value, true, nestedKey);
151
+
152
+ if (nestedCondition.must) {
153
+ conditions.push(...nestedCondition.must);
154
+ } else if (!this.isEmpty(nestedCondition)) {
155
+ conditions.push(nestedCondition);
156
+ }
157
+ }
158
+ }
159
+
160
+ return { conditions, range, matchCondition };
161
+ }
162
+
163
+ private translateCustomOperator(op: string, value: any, fieldKey?: string): any {
164
+ switch (op) {
165
+ case '$count':
166
+ const countConditions = Object.entries(value).reduce(
167
+ (acc, [k, v]) => ({
168
+ ...acc,
169
+ [k.replace('$', '')]: v,
170
+ }),
171
+ {},
172
+ );
173
+ return { key: fieldKey, values_count: countConditions };
174
+ case '$geo':
175
+ const geoOp = this.translateGeoFilter(value.type, value);
176
+ return { key: fieldKey, ...geoOp };
177
+ case '$hasId':
178
+ return { has_id: Array.isArray(value) ? value : [value] };
179
+ case '$nested':
180
+ return {
181
+ nested: {
182
+ key: fieldKey,
183
+ filter: this.translateNode(value),
184
+ },
185
+ };
186
+ case '$hasVector':
187
+ return { has_vector: value };
188
+ case '$datetime':
189
+ return {
190
+ key: fieldKey,
191
+ range: this.normalizeDatetimeRange(value.range),
192
+ };
193
+ case '$null':
194
+ return { is_null: { key: fieldKey } };
195
+ case '$empty':
196
+ return { is_empty: { key: fieldKey } };
197
+ default:
198
+ throw new Error(`Unsupported custom operator: ${op}`);
199
+ }
200
+ }
201
+
202
+ private getQdrantLogicalOp(op: string): string {
203
+ switch (op) {
204
+ case '$and':
205
+ return 'must';
206
+ case '$or':
207
+ return 'should';
208
+ case '$not':
209
+ return 'must_not';
210
+ default:
211
+ throw new Error(`Unsupported logical operator: ${op}`);
212
+ }
213
+ }
214
+
215
+ private translateOperatorValue(operator: string, value: any): any {
216
+ const normalizedValue = this.normalizeComparisonValue(value);
217
+
218
+ switch (operator) {
219
+ case '$eq':
220
+ return { value: normalizedValue };
221
+ case '$ne':
222
+ return { except: [normalizedValue] };
223
+ case '$gt':
224
+ return { range: { gt: normalizedValue } };
225
+ case '$gte':
226
+ return { range: { gte: normalizedValue } };
227
+ case '$lt':
228
+ return { range: { lt: normalizedValue } };
229
+ case '$lte':
230
+ return { range: { lte: normalizedValue } };
231
+ case '$in':
232
+ return { any: this.normalizeArrayValues(value) };
233
+ case '$nin':
234
+ return { except: this.normalizeArrayValues(value) };
235
+ case '$regex':
236
+ return { text: value };
237
+ case 'exists':
238
+ return value
239
+ ? {
240
+ must_not: [{ is_null: { key: value } }, { is_empty: { key: value } }],
241
+ }
242
+ : {
243
+ is_empty: { key: value },
244
+ };
245
+ default:
246
+ throw new Error(`Unsupported operator: ${operator}`);
247
+ }
248
+ }
249
+
250
+ private translateGeoFilter(type: string, value: any): any {
251
+ switch (type) {
252
+ case 'box':
253
+ return {
254
+ geo_bounding_box: {
255
+ top_left: value.top_left,
256
+ bottom_right: value.bottom_right,
257
+ },
258
+ };
259
+ case 'radius':
260
+ return {
261
+ geo_radius: {
262
+ center: value.center,
263
+ radius: value.radius,
264
+ },
265
+ };
266
+ case 'polygon':
267
+ return {
268
+ geo_polygon: {
269
+ exterior: value.exterior,
270
+ interiors: value.interiors,
271
+ },
272
+ };
273
+ default:
274
+ throw new Error(`Unsupported geo filter type: ${type}`);
275
+ }
276
+ }
277
+
278
+ private normalizeDatetimeRange(value: any): any {
279
+ const range: Record<string, string> = {};
280
+ for (const [op, val] of Object.entries(value)) {
281
+ if (val instanceof Date) {
282
+ range[op] = val.toISOString();
283
+ } else if (typeof val === 'string') {
284
+ // Assume string is already in proper format
285
+ range[op] = val;
286
+ }
287
+ }
288
+ return range;
289
+ }
290
+ }