@mastra/qdrant 0.10.3 → 0.11.0-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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +26 -0
- package/dist/_tsup-dts-rollup.d.cts +80 -4
- package/dist/_tsup-dts-rollup.d.ts +80 -4
- package/dist/index.cjs +165 -57
- package/dist/index.js +160 -52
- package/package.json +4 -4
- package/src/vector/filter.test.ts +40 -40
- package/src/vector/filter.ts +103 -5
- package/src/vector/index.test.ts +32 -27
- package/src/vector/index.ts +172 -60
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
|
|
1
2
|
import { MastraVector } from '@mastra/core/vector';
|
|
2
3
|
import { QdrantClient } from '@qdrant/js-client-rest';
|
|
3
4
|
import { BaseFilterTranslator } from '@mastra/core/vector/filter';
|
|
@@ -26,7 +27,7 @@ var QdrantFilterTranslator = class extends BaseFilterTranslator {
|
|
|
26
27
|
return fieldKey ? { key: fieldKey, ...condition } : condition;
|
|
27
28
|
}
|
|
28
29
|
translateNode(node, isNested = false, fieldKey) {
|
|
29
|
-
if (!this.isEmpty(node) && typeof node === "object" && "must" in node) {
|
|
30
|
+
if (!this.isEmpty(node) && !!node && typeof node === "object" && "must" in node) {
|
|
30
31
|
return node;
|
|
31
32
|
}
|
|
32
33
|
if (this.isPrimitive(node)) {
|
|
@@ -265,22 +266,46 @@ var QdrantVector = class extends MastraVector {
|
|
|
265
266
|
vector,
|
|
266
267
|
payload: metadata?.[i] || {}
|
|
267
268
|
}));
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
try {
|
|
270
|
+
for (let i = 0; i < records.length; i += BATCH_SIZE) {
|
|
271
|
+
const batch = records.slice(i, i + BATCH_SIZE);
|
|
272
|
+
await this.client.upsert(indexName, {
|
|
273
|
+
// @ts-expect-error
|
|
274
|
+
points: batch,
|
|
275
|
+
wait: true
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return pointIds;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new MastraError(
|
|
281
|
+
{
|
|
282
|
+
id: "STORAGE_QDRANT_VECTOR_UPSERT_FAILED",
|
|
283
|
+
domain: ErrorDomain.STORAGE,
|
|
284
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
285
|
+
details: { indexName, vectorCount: vectors.length }
|
|
286
|
+
},
|
|
287
|
+
error
|
|
288
|
+
);
|
|
275
289
|
}
|
|
276
|
-
return pointIds;
|
|
277
290
|
}
|
|
278
291
|
async createIndex({ indexName, dimension, metric = "cosine" }) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
292
|
+
try {
|
|
293
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
294
|
+
throw new Error("Dimension must be a positive integer");
|
|
295
|
+
}
|
|
296
|
+
if (!DISTANCE_MAPPING[metric]) {
|
|
297
|
+
throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
|
|
298
|
+
}
|
|
299
|
+
} catch (validationError) {
|
|
300
|
+
throw new MastraError(
|
|
301
|
+
{
|
|
302
|
+
id: "STORAGE_QDRANT_VECTOR_CREATE_INDEX_INVALID_ARGS",
|
|
303
|
+
domain: ErrorDomain.STORAGE,
|
|
304
|
+
category: ErrorCategory.USER,
|
|
305
|
+
details: { indexName, dimension, metric }
|
|
306
|
+
},
|
|
307
|
+
validationError
|
|
308
|
+
);
|
|
284
309
|
}
|
|
285
310
|
try {
|
|
286
311
|
await this.client.createCollection(indexName, {
|
|
@@ -295,6 +320,15 @@ var QdrantVector = class extends MastraVector {
|
|
|
295
320
|
await this.validateExistingIndex(indexName, dimension, metric);
|
|
296
321
|
return;
|
|
297
322
|
}
|
|
323
|
+
throw new MastraError(
|
|
324
|
+
{
|
|
325
|
+
id: "STORAGE_QDRANT_VECTOR_CREATE_INDEX_FAILED",
|
|
326
|
+
domain: ErrorDomain.STORAGE,
|
|
327
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
328
|
+
details: { indexName, dimension, metric }
|
|
329
|
+
},
|
|
330
|
+
error
|
|
331
|
+
);
|
|
298
332
|
}
|
|
299
333
|
}
|
|
300
334
|
transformFilter(filter) {
|
|
@@ -309,33 +343,56 @@ var QdrantVector = class extends MastraVector {
|
|
|
309
343
|
includeVector = false
|
|
310
344
|
}) {
|
|
311
345
|
const translatedFilter = this.transformFilter(filter) ?? {};
|
|
312
|
-
|
|
313
|
-
query
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
346
|
+
try {
|
|
347
|
+
const results = (await this.client.query(indexName, {
|
|
348
|
+
query: queryVector,
|
|
349
|
+
limit: topK,
|
|
350
|
+
filter: translatedFilter,
|
|
351
|
+
with_payload: true,
|
|
352
|
+
with_vector: includeVector
|
|
353
|
+
})).points;
|
|
354
|
+
return results.map((match) => {
|
|
355
|
+
let vector = [];
|
|
356
|
+
if (includeVector) {
|
|
357
|
+
if (Array.isArray(match.vector)) {
|
|
358
|
+
vector = match.vector;
|
|
359
|
+
} else if (typeof match.vector === "object" && match.vector !== null) {
|
|
360
|
+
vector = Object.values(match.vector).filter((v) => typeof v === "number");
|
|
361
|
+
}
|
|
326
362
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
};
|
|
334
|
-
})
|
|
363
|
+
return {
|
|
364
|
+
id: match.id,
|
|
365
|
+
score: match.score || 0,
|
|
366
|
+
metadata: match.payload,
|
|
367
|
+
...includeVector && { vector }
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
throw new MastraError(
|
|
372
|
+
{
|
|
373
|
+
id: "STORAGE_QDRANT_VECTOR_QUERY_FAILED",
|
|
374
|
+
domain: ErrorDomain.STORAGE,
|
|
375
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
376
|
+
details: { indexName, topK }
|
|
377
|
+
},
|
|
378
|
+
error
|
|
379
|
+
);
|
|
380
|
+
}
|
|
335
381
|
}
|
|
336
382
|
async listIndexes() {
|
|
337
|
-
|
|
338
|
-
|
|
383
|
+
try {
|
|
384
|
+
const response = await this.client.getCollections();
|
|
385
|
+
return response.collections.map((collection) => collection.name) || [];
|
|
386
|
+
} catch (error) {
|
|
387
|
+
throw new MastraError(
|
|
388
|
+
{
|
|
389
|
+
id: "STORAGE_QDRANT_VECTOR_LIST_INDEXES_FAILED",
|
|
390
|
+
domain: ErrorDomain.STORAGE,
|
|
391
|
+
category: ErrorCategory.THIRD_PARTY
|
|
392
|
+
},
|
|
393
|
+
error
|
|
394
|
+
);
|
|
395
|
+
}
|
|
339
396
|
}
|
|
340
397
|
/**
|
|
341
398
|
* Retrieves statistics about a vector index.
|
|
@@ -344,17 +401,41 @@ var QdrantVector = class extends MastraVector {
|
|
|
344
401
|
* @returns A promise that resolves to the index statistics including dimension, count and metric
|
|
345
402
|
*/
|
|
346
403
|
async describeIndex({ indexName }) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
404
|
+
try {
|
|
405
|
+
const { config, points_count } = await this.client.getCollection(indexName);
|
|
406
|
+
const distance = config.params.vectors?.distance;
|
|
407
|
+
return {
|
|
408
|
+
dimension: config.params.vectors?.size,
|
|
409
|
+
count: points_count || 0,
|
|
410
|
+
// @ts-expect-error
|
|
411
|
+
metric: Object.keys(DISTANCE_MAPPING).find((key) => DISTANCE_MAPPING[key] === distance)
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new MastraError(
|
|
415
|
+
{
|
|
416
|
+
id: "STORAGE_QDRANT_VECTOR_DESCRIBE_INDEX_FAILED",
|
|
417
|
+
domain: ErrorDomain.STORAGE,
|
|
418
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
419
|
+
details: { indexName }
|
|
420
|
+
},
|
|
421
|
+
error
|
|
422
|
+
);
|
|
423
|
+
}
|
|
355
424
|
}
|
|
356
425
|
async deleteIndex({ indexName }) {
|
|
357
|
-
|
|
426
|
+
try {
|
|
427
|
+
await this.client.deleteCollection(indexName);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
throw new MastraError(
|
|
430
|
+
{
|
|
431
|
+
id: "STORAGE_QDRANT_VECTOR_DELETE_INDEX_FAILED",
|
|
432
|
+
domain: ErrorDomain.STORAGE,
|
|
433
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
434
|
+
details: { indexName }
|
|
435
|
+
},
|
|
436
|
+
error
|
|
437
|
+
);
|
|
438
|
+
}
|
|
358
439
|
}
|
|
359
440
|
/**
|
|
360
441
|
* Updates a vector by its ID with the provided vector and/or metadata.
|
|
@@ -367,8 +448,20 @@ var QdrantVector = class extends MastraVector {
|
|
|
367
448
|
* @throws Will throw an error if no updates are provided or if the update operation fails.
|
|
368
449
|
*/
|
|
369
450
|
async updateVector({ indexName, id, update }) {
|
|
370
|
-
|
|
371
|
-
|
|
451
|
+
try {
|
|
452
|
+
if (!update.vector && !update.metadata) {
|
|
453
|
+
throw new Error("No updates provided");
|
|
454
|
+
}
|
|
455
|
+
} catch (validationError) {
|
|
456
|
+
throw new MastraError(
|
|
457
|
+
{
|
|
458
|
+
id: "STORAGE_QDRANT_VECTOR_UPDATE_VECTOR_INVALID_ARGS",
|
|
459
|
+
domain: ErrorDomain.STORAGE,
|
|
460
|
+
category: ErrorCategory.USER,
|
|
461
|
+
details: { indexName, id }
|
|
462
|
+
},
|
|
463
|
+
validationError
|
|
464
|
+
);
|
|
372
465
|
}
|
|
373
466
|
const pointId = this.parsePointId(id);
|
|
374
467
|
try {
|
|
@@ -399,8 +492,15 @@ var QdrantVector = class extends MastraVector {
|
|
|
399
492
|
return;
|
|
400
493
|
}
|
|
401
494
|
} catch (error) {
|
|
402
|
-
|
|
403
|
-
|
|
495
|
+
throw new MastraError(
|
|
496
|
+
{
|
|
497
|
+
id: "STORAGE_QDRANT_VECTOR_UPDATE_VECTOR_FAILED",
|
|
498
|
+
domain: ErrorDomain.STORAGE,
|
|
499
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
500
|
+
details: { indexName, id }
|
|
501
|
+
},
|
|
502
|
+
error
|
|
503
|
+
);
|
|
404
504
|
}
|
|
405
505
|
}
|
|
406
506
|
/**
|
|
@@ -417,7 +517,15 @@ var QdrantVector = class extends MastraVector {
|
|
|
417
517
|
points: [pointId]
|
|
418
518
|
});
|
|
419
519
|
} catch (error) {
|
|
420
|
-
throw new
|
|
520
|
+
throw new MastraError(
|
|
521
|
+
{
|
|
522
|
+
id: "STORAGE_QDRANT_VECTOR_DELETE_VECTOR_FAILED",
|
|
523
|
+
domain: ErrorDomain.STORAGE,
|
|
524
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
525
|
+
details: { indexName, id }
|
|
526
|
+
},
|
|
527
|
+
error
|
|
528
|
+
);
|
|
421
529
|
}
|
|
422
530
|
}
|
|
423
531
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/qdrant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-alpha.1",
|
|
4
4
|
"description": "Qdrant vector store provider for Mastra",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@microsoft/api-extractor": "^7.52.8",
|
|
27
27
|
"@types/node": "^20.19.0",
|
|
28
|
-
"eslint": "^9.
|
|
28
|
+
"eslint": "^9.29.0",
|
|
29
29
|
"tsup": "^8.5.0",
|
|
30
30
|
"typescript": "^5.8.3",
|
|
31
31
|
"vitest": "^3.2.3",
|
|
32
32
|
"@internal/lint": "0.0.13",
|
|
33
|
-
"@mastra/core": "0.10.
|
|
33
|
+
"@mastra/core": "0.10.7-alpha.3"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@mastra/core": ">=0.10.
|
|
36
|
+
"@mastra/core": ">=0.10.7-0 <0.11.0-0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
2
|
+
import type { QdrantVectorFilter } from './filter';
|
|
3
3
|
import { QdrantFilterTranslator } from './filter';
|
|
4
4
|
|
|
5
5
|
describe('QdrantFilterTranslator', () => {
|
|
@@ -7,7 +7,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
7
7
|
|
|
8
8
|
describe('Basic Operators', () => {
|
|
9
9
|
it('should translate direct value match', () => {
|
|
10
|
-
const filter = { field: 'value' };
|
|
10
|
+
const filter: QdrantVectorFilter = { field: 'value' };
|
|
11
11
|
const expected = { must: [{ key: 'field', match: { value: 'value' } }] };
|
|
12
12
|
expect(translator.translate(filter)).toEqual(expected);
|
|
13
13
|
});
|
|
@@ -63,7 +63,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it('should handle multiple comparison operators on same field', () => {
|
|
66
|
-
const filter = { field: { $gt: 10, $lt: 20 } };
|
|
66
|
+
const filter: QdrantVectorFilter = { field: { $gt: 10, $lt: 20 } };
|
|
67
67
|
const expected = { must: [{ key: 'field', range: { gt: 10, lt: 20 } }] };
|
|
68
68
|
expect(translator.translate(filter)).toEqual(expected);
|
|
69
69
|
});
|
|
@@ -71,7 +71,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
71
71
|
|
|
72
72
|
describe('Logical Operators', () => {
|
|
73
73
|
it('should translate $and operator', () => {
|
|
74
|
-
const filter = {
|
|
74
|
+
const filter: QdrantVectorFilter = {
|
|
75
75
|
$and: [{ field1: 'value1' }, { field2: { $gt: 100 } }],
|
|
76
76
|
};
|
|
77
77
|
|
|
@@ -86,7 +86,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('should translate $or operator', () => {
|
|
89
|
-
const filter = {
|
|
89
|
+
const filter: QdrantVectorFilter = {
|
|
90
90
|
$or: [{ field1: 'value1' }, { field2: { $lt: 100 } }],
|
|
91
91
|
};
|
|
92
92
|
|
|
@@ -101,7 +101,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
it('should translate $not operator', () => {
|
|
104
|
-
const filter = {
|
|
104
|
+
const filter: QdrantVectorFilter = {
|
|
105
105
|
$not: { field: 'value' },
|
|
106
106
|
};
|
|
107
107
|
|
|
@@ -113,7 +113,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('should handle nested logical operators', () => {
|
|
116
|
-
const filter = {
|
|
116
|
+
const filter: QdrantVectorFilter = {
|
|
117
117
|
$and: [
|
|
118
118
|
{ 'user.age': { $gte: 18 } },
|
|
119
119
|
{
|
|
@@ -155,7 +155,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
it('should handle nested must_not operators', () => {
|
|
158
|
-
const filter = {
|
|
158
|
+
const filter: QdrantVectorFilter = {
|
|
159
159
|
$not: {
|
|
160
160
|
$not: { field: 'value' },
|
|
161
161
|
},
|
|
@@ -171,7 +171,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
it('should handle complex logical combinations with ranges', () => {
|
|
174
|
-
const filter = {
|
|
174
|
+
const filter: QdrantVectorFilter = {
|
|
175
175
|
$or: [
|
|
176
176
|
{ $and: [{ price: { $gte: 100, $lt: 200 } }, { stock: { $gt: 0 } }] },
|
|
177
177
|
{ $and: [{ price: { $lt: 100 } }, { featured: true }] },
|
|
@@ -199,13 +199,13 @@ describe('QdrantFilterTranslator', () => {
|
|
|
199
199
|
|
|
200
200
|
describe('Custom Operators', () => {
|
|
201
201
|
it('should translate $count operator', () => {
|
|
202
|
-
const filter = { field: { $count: { $gt: 5 } } };
|
|
202
|
+
const filter: QdrantVectorFilter = { field: { $count: { $gt: 5 } } };
|
|
203
203
|
const expected = { must: [{ key: 'field', values_count: { gt: 5 } }] };
|
|
204
204
|
expect(translator.translate(filter)).toEqual(expected);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
207
|
it('should translate $geo operator with radius', () => {
|
|
208
|
-
const filter = {
|
|
208
|
+
const filter: QdrantVectorFilter = {
|
|
209
209
|
location: {
|
|
210
210
|
$geo: {
|
|
211
211
|
type: 'radius',
|
|
@@ -229,7 +229,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
it('should translate $geo operator with bounding box', () => {
|
|
232
|
-
const filter = {
|
|
232
|
+
const filter: QdrantVectorFilter = {
|
|
233
233
|
location: {
|
|
234
234
|
$geo: {
|
|
235
235
|
type: 'box',
|
|
@@ -255,7 +255,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
it('should translate $geo operator with polygon', () => {
|
|
258
|
-
const filter = {
|
|
258
|
+
const filter: QdrantVectorFilter = {
|
|
259
259
|
location: {
|
|
260
260
|
$geo: {
|
|
261
261
|
type: 'polygon',
|
|
@@ -286,7 +286,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
it('should translate $nested operator', () => {
|
|
289
|
-
const filter = {
|
|
289
|
+
const filter: QdrantVectorFilter = {
|
|
290
290
|
diet: {
|
|
291
291
|
$nested: {
|
|
292
292
|
food: 'meat',
|
|
@@ -322,7 +322,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
322
322
|
|
|
323
323
|
it('should translate $datetime operator', () => {
|
|
324
324
|
const now = new Date();
|
|
325
|
-
const filter = {
|
|
325
|
+
const filter: QdrantVectorFilter = {
|
|
326
326
|
timestamp: {
|
|
327
327
|
$datetime: {
|
|
328
328
|
range: {
|
|
@@ -357,7 +357,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
357
357
|
});
|
|
358
358
|
|
|
359
359
|
it('should handle nested $count with multiple conditions', () => {
|
|
360
|
-
const filter = { 'array.items': { $count: { $gt: 5, $lt: 10 } } };
|
|
360
|
+
const filter: QdrantVectorFilter = { 'array.items': { $count: { $gt: 5, $lt: 10 } } };
|
|
361
361
|
const expected = {
|
|
362
362
|
must: [
|
|
363
363
|
{
|
|
@@ -370,7 +370,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
it('should translate $nested operator with complex conditions', () => {
|
|
373
|
-
const filter = {
|
|
373
|
+
const filter: QdrantVectorFilter = {
|
|
374
374
|
nested_field: {
|
|
375
375
|
$nested: {
|
|
376
376
|
inner_field: { $gt: 100 },
|
|
@@ -399,7 +399,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
399
399
|
|
|
400
400
|
it('should translate $datetime operator with multiple range conditions', () => {
|
|
401
401
|
const now = new Date();
|
|
402
|
-
const filter = {
|
|
402
|
+
const filter: QdrantVectorFilter = {
|
|
403
403
|
timestamp: {
|
|
404
404
|
$datetime: {
|
|
405
405
|
range: {
|
|
@@ -430,7 +430,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
it('should translate $datetime operator with string dates', () => {
|
|
433
|
-
const filter = {
|
|
433
|
+
const filter: QdrantVectorFilter = {
|
|
434
434
|
timestamp: {
|
|
435
435
|
$datetime: {
|
|
436
436
|
key: 'timestamp',
|
|
@@ -458,7 +458,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
458
458
|
});
|
|
459
459
|
|
|
460
460
|
it('should translate complex $nested operator', () => {
|
|
461
|
-
const filter = {
|
|
461
|
+
const filter: QdrantVectorFilter = {
|
|
462
462
|
diet: {
|
|
463
463
|
$nested: {
|
|
464
464
|
food: { $in: ['meat', 'fish'] },
|
|
@@ -491,19 +491,19 @@ describe('QdrantFilterTranslator', () => {
|
|
|
491
491
|
|
|
492
492
|
describe('Special Cases', () => {
|
|
493
493
|
it('should handle nested paths', () => {
|
|
494
|
-
const filter = { 'obj.field': 'value' };
|
|
494
|
+
const filter: QdrantVectorFilter = { 'obj.field': 'value' };
|
|
495
495
|
const expected = { must: [{ key: 'obj.field', match: { value: 'value' } }] };
|
|
496
496
|
expect(translator.translate(filter)).toEqual(expected);
|
|
497
497
|
});
|
|
498
498
|
|
|
499
499
|
it('should handle deep nested paths', () => {
|
|
500
|
-
const filter = { 'a.b.c.d': { $gt: 100 } };
|
|
500
|
+
const filter: QdrantVectorFilter = { 'a.b.c.d': { $gt: 100 } };
|
|
501
501
|
const expected = { must: [{ key: 'a.b.c.d', range: { gt: 100 } }] };
|
|
502
502
|
expect(translator.translate(filter)).toEqual(expected);
|
|
503
503
|
});
|
|
504
504
|
|
|
505
505
|
it('should handle complex combinations', () => {
|
|
506
|
-
const filter = {
|
|
506
|
+
const filter: QdrantVectorFilter = {
|
|
507
507
|
$and: [
|
|
508
508
|
{ 'user.age': { $gte: 18 } },
|
|
509
509
|
{
|
|
@@ -534,7 +534,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
534
534
|
});
|
|
535
535
|
|
|
536
536
|
it('should handle multiple nested paths with same prefix', () => {
|
|
537
|
-
const filter = {
|
|
537
|
+
const filter: QdrantVectorFilter = {
|
|
538
538
|
$and: [{ 'user.profile.age': { $gte: 18 } }, { 'user.profile.name': 'John' }],
|
|
539
539
|
};
|
|
540
540
|
|
|
@@ -549,7 +549,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
549
549
|
});
|
|
550
550
|
|
|
551
551
|
it('should handle mixed array and object paths', () => {
|
|
552
|
-
const filter = {
|
|
552
|
+
const filter: QdrantVectorFilter = {
|
|
553
553
|
'items[].category': { $in: ['A', 'B'] },
|
|
554
554
|
'items[].details.price': { $gt: 100 },
|
|
555
555
|
};
|
|
@@ -565,7 +565,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
565
565
|
});
|
|
566
566
|
|
|
567
567
|
it('should handle empty logical operators in combination', () => {
|
|
568
|
-
const filter = {
|
|
568
|
+
const filter: QdrantVectorFilter = {
|
|
569
569
|
$and: [{ $or: [] }, { field: 'value' }],
|
|
570
570
|
};
|
|
571
571
|
const expected = {
|
|
@@ -575,7 +575,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
575
575
|
});
|
|
576
576
|
|
|
577
577
|
it('should handle deeply nested paths with array notation', () => {
|
|
578
|
-
const filter = {
|
|
578
|
+
const filter: QdrantVectorFilter = {
|
|
579
579
|
'users[].addresses[].geo.location': {
|
|
580
580
|
$geo: {
|
|
581
581
|
type: 'radius',
|
|
@@ -599,7 +599,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
599
599
|
});
|
|
600
600
|
|
|
601
601
|
it('should handle array paths with multiple levels', () => {
|
|
602
|
-
const filter = {
|
|
602
|
+
const filter: QdrantVectorFilter = {
|
|
603
603
|
'users[].addresses[].location[].coordinates': { $gt: 100 },
|
|
604
604
|
};
|
|
605
605
|
const expected = {
|
|
@@ -614,7 +614,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
614
614
|
});
|
|
615
615
|
|
|
616
616
|
it('should handle combination of array and object paths', () => {
|
|
617
|
-
const filter = {
|
|
617
|
+
const filter: QdrantVectorFilter = {
|
|
618
618
|
'users[].profile.addresses[].location.coordinates': { $gt: 100 },
|
|
619
619
|
};
|
|
620
620
|
const expected = {
|
|
@@ -629,7 +629,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
629
629
|
});
|
|
630
630
|
|
|
631
631
|
it('should handle multiple conditions with same array path', () => {
|
|
632
|
-
const filter = {
|
|
632
|
+
const filter: QdrantVectorFilter = {
|
|
633
633
|
$and: [
|
|
634
634
|
{ 'items[].price': { $gt: 100 } },
|
|
635
635
|
{ 'items[].quantity': { $gt: 0 } },
|
|
@@ -654,7 +654,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
654
654
|
});
|
|
655
655
|
|
|
656
656
|
it('should throw error for invalid geo filter type', () => {
|
|
657
|
-
const filter = {
|
|
657
|
+
const filter: QdrantVectorFilter = {
|
|
658
658
|
location: {
|
|
659
659
|
$geo: {
|
|
660
660
|
type: 'invalid',
|
|
@@ -667,7 +667,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
667
667
|
});
|
|
668
668
|
|
|
669
669
|
it('should throw error for invalid custom operator', () => {
|
|
670
|
-
const filter = { $invalidCustom: 'value' };
|
|
670
|
+
const filter: QdrantVectorFilter = { $invalidCustom: 'value' };
|
|
671
671
|
expect(() => translator.translate(filter)).toThrow();
|
|
672
672
|
});
|
|
673
673
|
});
|
|
@@ -686,7 +686,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
686
686
|
});
|
|
687
687
|
|
|
688
688
|
it('should validate array operator values', () => {
|
|
689
|
-
const invalidFilters = [
|
|
689
|
+
const invalidFilters: any = [
|
|
690
690
|
{ field: { $in: 'not-an-array' } }, // Should be array
|
|
691
691
|
{ field: { $nin: 123 } }, // Should be array
|
|
692
692
|
{ field: { $in: {} } }, // Should be array
|
|
@@ -697,14 +697,14 @@ describe('QdrantFilterTranslator', () => {
|
|
|
697
697
|
});
|
|
698
698
|
});
|
|
699
699
|
it('throws error for non-logical operators at top level', () => {
|
|
700
|
-
const invalidFilters = [{ $gt: 100 }, { $in: ['value1', 'value2'] }, { $eq: true }];
|
|
700
|
+
const invalidFilters: any = [{ $gt: 100 }, { $in: ['value1', 'value2'] }, { $eq: true }];
|
|
701
701
|
|
|
702
702
|
invalidFilters.forEach(filter => {
|
|
703
703
|
expect(() => translator.translate(filter)).toThrow(/Invalid top-level operator/);
|
|
704
704
|
});
|
|
705
705
|
});
|
|
706
706
|
it('allows logical operators at top level', () => {
|
|
707
|
-
const validFilters = [{ $and: [{ field: 'value' }] }, { $or: [{ field: 'value' }] }];
|
|
707
|
+
const validFilters: QdrantVectorFilter[] = [{ $and: [{ field: 'value' }] }, { $or: [{ field: 'value' }] }];
|
|
708
708
|
|
|
709
709
|
validFilters.forEach(filter => {
|
|
710
710
|
expect(() => translator.translate(filter)).not.toThrow();
|
|
@@ -732,7 +732,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
732
732
|
});
|
|
733
733
|
|
|
734
734
|
it('should wrap multiple field conditions in single must', () => {
|
|
735
|
-
const filter = {
|
|
735
|
+
const filter: QdrantVectorFilter = {
|
|
736
736
|
field1: 'value1',
|
|
737
737
|
field2: 'value2',
|
|
738
738
|
};
|
|
@@ -746,7 +746,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
746
746
|
});
|
|
747
747
|
|
|
748
748
|
it('should wrap complex single field conditions', () => {
|
|
749
|
-
const filter = {
|
|
749
|
+
const filter: QdrantVectorFilter = {
|
|
750
750
|
field: {
|
|
751
751
|
$gt: 10,
|
|
752
752
|
$lt: 20,
|
|
@@ -771,7 +771,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
771
771
|
|
|
772
772
|
describe('No Must Wrapper Cases', () => {
|
|
773
773
|
it('should not wrap nested logical operators', () => {
|
|
774
|
-
const filter = {
|
|
774
|
+
const filter: QdrantVectorFilter = {
|
|
775
775
|
$and: [{ field1: 'value1' }, { field2: 'value2' }],
|
|
776
776
|
};
|
|
777
777
|
const expected = {
|
|
@@ -791,7 +791,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
791
791
|
});
|
|
792
792
|
|
|
793
793
|
it('should preserve existing logical structure', () => {
|
|
794
|
-
const filter = {
|
|
794
|
+
const filter: QdrantVectorFilter = {
|
|
795
795
|
$or: [{ field1: 'value1' }, { $and: [{ field2: 'value2' }, { field3: 'value3' }] }],
|
|
796
796
|
};
|
|
797
797
|
const expected = {
|
|
@@ -826,7 +826,7 @@ describe('QdrantFilterTranslator', () => {
|
|
|
826
826
|
});
|
|
827
827
|
|
|
828
828
|
it('should handle regex in nested conditions', () => {
|
|
829
|
-
const filter = {
|
|
829
|
+
const filter: QdrantVectorFilter = {
|
|
830
830
|
diet: {
|
|
831
831
|
$nested: {
|
|
832
832
|
food: { $regex: 'meat' },
|