@mastra/qdrant 0.11.8 → 0.11.9-alpha.1

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.
@@ -1,388 +0,0 @@
1
- import { BaseFilterTranslator } from '@mastra/core/vector/filter';
2
- import type {
3
- VectorFilter,
4
- LogicalOperator,
5
- OperatorSupport,
6
- OperatorValueMap,
7
- LogicalOperatorValueMap,
8
- BlacklistedRootOperators,
9
- } from '@mastra/core/vector/filter';
10
-
11
- type QdrantOperatorValueMap = Omit<OperatorValueMap, '$options' | '$elemMatch' | '$all'> & {
12
- /**
13
- * $count: Filter by array length or value count.
14
- * Example: { tags: { $count: { gt: 2 } } }
15
- */
16
- $count: {
17
- $gt?: number;
18
- $gte?: number;
19
- $lt?: number;
20
- $lte?: number;
21
- $eq?: number;
22
- };
23
-
24
- /**
25
- * $geo: Geospatial filter.
26
- * Example: { location: { $geo: { type: 'geo_radius', center: [lon, lat], radius: 1000 } } }
27
- */
28
- $geo: {
29
- type: string;
30
- [key: string]: any;
31
- };
32
-
33
- /**
34
- * $hasId: Filter by point IDs.
35
- * Allowed at root level.
36
- * Example: { $hasId: '123' } or { $hasId: ['123', '456'] }
37
- */
38
- $hasId: string | string[];
39
-
40
- /**
41
- * $nested: Nested object filter.
42
- * Example: { metadata: { $nested: { key: 'foo', filter: { $eq: 'bar' } } } }
43
- */
44
- $nested: {
45
- // Additional properties depend on the nested object structure
46
- [key: string]: any;
47
- };
48
-
49
- /**
50
- * $hasVector: Filter by vector existence or field.
51
- * Allowed at root level.
52
- * Example: { $hasVector: true } or { $hasVector: 'vector_field' }
53
- */
54
- $hasVector: boolean | string;
55
-
56
- /**
57
- * $datetime: RFC 3339 datetime range.
58
- * Example: { createdAt: { $datetime: { gte: '2024-01-01T00:00:00Z' } } }
59
- */
60
- $datetime: {
61
- key?: string;
62
- range?: {
63
- gt?: Date | string;
64
- gte?: Date | string;
65
- lt?: Date | string;
66
- lte?: Date | string;
67
- eq?: Date | string;
68
- };
69
- };
70
-
71
- /**
72
- * $null: Check if a field is null.
73
- * Example: { metadata: { $null: true } }
74
- */
75
- $null: boolean;
76
-
77
- /**
78
- * $empty: Check if an array or object field is empty.
79
- * Example: { tags: { $empty: true } }
80
- */
81
- $empty: boolean;
82
- };
83
-
84
- type QdrantLogicalOperatorValueMap = Omit<LogicalOperatorValueMap, '$nor'>;
85
-
86
- type QdrantBlacklistedRootOperators =
87
- | BlacklistedRootOperators
88
- | '$count'
89
- | '$geo'
90
- | '$nested'
91
- | '$datetime'
92
- | '$null'
93
- | '$empty';
94
-
95
- export type QdrantVectorFilter = VectorFilter<
96
- keyof QdrantOperatorValueMap,
97
- QdrantOperatorValueMap,
98
- QdrantLogicalOperatorValueMap,
99
- QdrantBlacklistedRootOperators
100
- >;
101
-
102
- /**
103
- * Translates MongoDB-style filters to Qdrant compatible filters.
104
- *
105
- * Key transformations:
106
- * - $and -> must
107
- * - $or -> should
108
- * - $not -> must_not
109
- * - { field: { $op: value } } -> { key: field, match/range: { value/gt/lt: value } }
110
- *
111
- * Custom operators (Qdrant-specific):
112
- * - $count -> values_count (array length/value count)
113
- * - $geo -> geo filters (box, radius, polygon)
114
- * - $hasId -> has_id filter
115
- * - $nested -> nested object filters
116
- * - $hasVector -> vector existence check
117
- * - $datetime -> RFC 3339 datetime range
118
- * - $null -> is_null check
119
- * - $empty -> is_empty check
120
- */
121
- export class QdrantFilterTranslator extends BaseFilterTranslator<QdrantVectorFilter> {
122
- protected override isLogicalOperator(key: string): key is LogicalOperator {
123
- return super.isLogicalOperator(key) || key === '$hasId' || key === '$hasVector';
124
- }
125
-
126
- protected override getSupportedOperators(): OperatorSupport {
127
- return {
128
- ...BaseFilterTranslator.DEFAULT_OPERATORS,
129
- logical: ['$and', '$or', '$not'],
130
- array: ['$in', '$nin'],
131
- regex: ['$regex'],
132
- custom: ['$count', '$geo', '$nested', '$datetime', '$null', '$empty', '$hasId', '$hasVector'],
133
- };
134
- }
135
-
136
- translate(filter?: QdrantVectorFilter): QdrantVectorFilter {
137
- if (this.isEmpty(filter)) return filter;
138
- this.validateFilter(filter);
139
- return this.translateNode(filter);
140
- }
141
-
142
- private createCondition(type: string, value: any, fieldKey?: string) {
143
- const condition = { [type]: value };
144
- return fieldKey ? { key: fieldKey, ...condition } : condition;
145
- }
146
-
147
- private translateNode(node: QdrantVectorFilter, isNested: boolean = false, fieldKey?: string): any {
148
- if (!this.isEmpty(node) && !!node && typeof node === 'object' && 'must' in node) {
149
- return node;
150
- }
151
-
152
- if (this.isPrimitive(node)) {
153
- if (node === null) {
154
- return { is_null: { key: fieldKey } };
155
- }
156
- return this.createCondition('match', { value: this.normalizeComparisonValue(node) }, fieldKey);
157
- }
158
-
159
- if (this.isRegex(node)) {
160
- throw new Error('Direct regex pattern format is not supported in Qdrant');
161
- }
162
-
163
- if (Array.isArray(node)) {
164
- return node.length === 0
165
- ? { is_empty: { key: fieldKey } }
166
- : this.createCondition('match', { any: this.normalizeArrayValues(node) }, fieldKey);
167
- }
168
-
169
- const entries = Object.entries(node as Record<string, any>);
170
-
171
- // Handle logical operators first
172
- const logicalResult = this.handleLogicalOperators(entries, isNested);
173
- if (logicalResult) {
174
- return logicalResult;
175
- }
176
-
177
- // Handle field conditions
178
- const { conditions, range, matchCondition } = this.handleFieldConditions(entries, fieldKey);
179
-
180
- if (Object.keys(range).length > 0) {
181
- conditions.push({ key: fieldKey, range });
182
- }
183
-
184
- if (matchCondition) {
185
- conditions.push({ key: fieldKey, match: matchCondition });
186
- }
187
-
188
- return this.buildFinalConditions(conditions, isNested);
189
- }
190
-
191
- private buildFinalConditions(conditions: any[], isNested: boolean): any {
192
- if (conditions.length === 0) {
193
- return {};
194
- } else if (conditions.length === 1 && isNested) {
195
- return conditions[0];
196
- } else {
197
- return { must: conditions };
198
- }
199
- }
200
-
201
- private handleLogicalOperators(entries: [string, any][], isNested: boolean): any | null {
202
- const firstKey = entries[0]?.[0];
203
-
204
- if (firstKey && this.isLogicalOperator(firstKey) && !this.isCustomOperator(firstKey)) {
205
- const [key, value] = entries[0]!;
206
- const qdrantOp = this.getQdrantLogicalOp(key);
207
- return {
208
- [qdrantOp]: Array.isArray(value)
209
- ? value.map(v => this.translateNode(v, true))
210
- : [this.translateNode(value, true)],
211
- };
212
- }
213
-
214
- if (
215
- entries.length > 1 &&
216
- !isNested &&
217
- entries.every(([key]) => !this.isOperator(key) && !this.isCustomOperator(key))
218
- ) {
219
- return {
220
- must: entries.map(([key, value]) => this.translateNode(value, true, key)),
221
- };
222
- }
223
-
224
- return null;
225
- }
226
-
227
- private handleFieldConditions(
228
- entries: [string, any][],
229
- fieldKey?: string,
230
- ): { conditions: any[]; range: Record<string, any>; matchCondition: Record<string, any> | null } {
231
- const conditions = [];
232
- let range: Record<string, any> = {};
233
- let matchCondition: Record<string, any> | null = null;
234
-
235
- for (const [key, value] of entries) {
236
- if (this.isCustomOperator(key)) {
237
- const customOp = this.translateCustomOperator(key, value, fieldKey);
238
- conditions.push(customOp);
239
- } else if (this.isOperator(key)) {
240
- const opResult = this.translateOperatorValue(key, value);
241
- if (opResult.range) {
242
- Object.assign(range, opResult.range);
243
- } else {
244
- matchCondition = opResult;
245
- }
246
- } else {
247
- const nestedKey = fieldKey ? `${fieldKey}.${key}` : key;
248
- const nestedCondition = this.translateNode(value, true, nestedKey);
249
-
250
- if (nestedCondition.must) {
251
- conditions.push(...nestedCondition.must);
252
- } else if (!this.isEmpty(nestedCondition)) {
253
- conditions.push(nestedCondition);
254
- }
255
- }
256
- }
257
-
258
- return { conditions, range, matchCondition };
259
- }
260
-
261
- private translateCustomOperator(op: string, value: any, fieldKey?: string): any {
262
- switch (op) {
263
- case '$count':
264
- const countConditions = Object.entries(value).reduce(
265
- (acc, [k, v]) => ({
266
- ...acc,
267
- [k.replace('$', '')]: v,
268
- }),
269
- {},
270
- );
271
- return { key: fieldKey, values_count: countConditions };
272
- case '$geo':
273
- const geoOp = this.translateGeoFilter(value.type, value);
274
- return { key: fieldKey, ...geoOp };
275
- case '$hasId':
276
- return { has_id: Array.isArray(value) ? value : [value] };
277
- case '$nested':
278
- return {
279
- nested: {
280
- key: fieldKey,
281
- filter: this.translateNode(value),
282
- },
283
- };
284
- case '$hasVector':
285
- return { has_vector: value };
286
- case '$datetime':
287
- return {
288
- key: fieldKey,
289
- range: this.normalizeDatetimeRange(value.range),
290
- };
291
- case '$null':
292
- return { is_null: { key: fieldKey } };
293
- case '$empty':
294
- return { is_empty: { key: fieldKey } };
295
- default:
296
- throw new Error(`Unsupported custom operator: ${op}`);
297
- }
298
- }
299
-
300
- private getQdrantLogicalOp(op: string): string {
301
- switch (op) {
302
- case '$and':
303
- return 'must';
304
- case '$or':
305
- return 'should';
306
- case '$not':
307
- return 'must_not';
308
- default:
309
- throw new Error(`Unsupported logical operator: ${op}`);
310
- }
311
- }
312
-
313
- private translateOperatorValue(operator: string, value: any): any {
314
- const normalizedValue = this.normalizeComparisonValue(value);
315
-
316
- switch (operator) {
317
- case '$eq':
318
- return { value: normalizedValue };
319
- case '$ne':
320
- return { except: [normalizedValue] };
321
- case '$gt':
322
- return { range: { gt: normalizedValue } };
323
- case '$gte':
324
- return { range: { gte: normalizedValue } };
325
- case '$lt':
326
- return { range: { lt: normalizedValue } };
327
- case '$lte':
328
- return { range: { lte: normalizedValue } };
329
- case '$in':
330
- return { any: this.normalizeArrayValues(value) };
331
- case '$nin':
332
- return { except: this.normalizeArrayValues(value) };
333
- case '$regex':
334
- return { text: value };
335
- case 'exists':
336
- return value
337
- ? {
338
- must_not: [{ is_null: { key: value } }, { is_empty: { key: value } }],
339
- }
340
- : {
341
- is_empty: { key: value },
342
- };
343
- default:
344
- throw new Error(`Unsupported operator: ${operator}`);
345
- }
346
- }
347
-
348
- private translateGeoFilter(type: string, value: any): any {
349
- switch (type) {
350
- case 'box':
351
- return {
352
- geo_bounding_box: {
353
- top_left: value.top_left,
354
- bottom_right: value.bottom_right,
355
- },
356
- };
357
- case 'radius':
358
- return {
359
- geo_radius: {
360
- center: value.center,
361
- radius: value.radius,
362
- },
363
- };
364
- case 'polygon':
365
- return {
366
- geo_polygon: {
367
- exterior: value.exterior,
368
- interiors: value.interiors,
369
- },
370
- };
371
- default:
372
- throw new Error(`Unsupported geo filter type: ${type}`);
373
- }
374
- }
375
-
376
- private normalizeDatetimeRange(value: any): any {
377
- const range: Record<string, string> = {};
378
- for (const [op, val] of Object.entries(value)) {
379
- if (val instanceof Date) {
380
- range[op] = val.toISOString();
381
- } else if (typeof val === 'string') {
382
- // Assume string is already in proper format
383
- range[op] = val;
384
- }
385
- }
386
- return range;
387
- }
388
- }