@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/src/vector/filter.ts
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
import { BaseFilterTranslator } from '@mastra/core/vector/filter';
|
|
2
|
-
import type {
|
|
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
|
+
>;
|
|
3
101
|
|
|
4
102
|
/**
|
|
5
103
|
* Translates MongoDB-style filters to Qdrant compatible filters.
|
|
@@ -20,7 +118,7 @@ import type { FieldCondition, VectorFilter, LogicalOperator, OperatorSupport } f
|
|
|
20
118
|
* - $null -> is_null check
|
|
21
119
|
* - $empty -> is_empty check
|
|
22
120
|
*/
|
|
23
|
-
export class QdrantFilterTranslator extends BaseFilterTranslator {
|
|
121
|
+
export class QdrantFilterTranslator extends BaseFilterTranslator<QdrantVectorFilter> {
|
|
24
122
|
protected override isLogicalOperator(key: string): key is LogicalOperator {
|
|
25
123
|
return super.isLogicalOperator(key) || key === '$hasId' || key === '$hasVector';
|
|
26
124
|
}
|
|
@@ -35,7 +133,7 @@ export class QdrantFilterTranslator extends BaseFilterTranslator {
|
|
|
35
133
|
};
|
|
36
134
|
}
|
|
37
135
|
|
|
38
|
-
translate(filter?:
|
|
136
|
+
translate(filter?: QdrantVectorFilter): QdrantVectorFilter {
|
|
39
137
|
if (this.isEmpty(filter)) return filter;
|
|
40
138
|
this.validateFilter(filter);
|
|
41
139
|
return this.translateNode(filter);
|
|
@@ -46,8 +144,8 @@ export class QdrantFilterTranslator extends BaseFilterTranslator {
|
|
|
46
144
|
return fieldKey ? { key: fieldKey, ...condition } : condition;
|
|
47
145
|
}
|
|
48
146
|
|
|
49
|
-
private translateNode(node:
|
|
50
|
-
if (!this.isEmpty(node) && typeof node === 'object' && 'must' in node) {
|
|
147
|
+
private translateNode(node: QdrantVectorFilter, isNested: boolean = false, fieldKey?: string): any {
|
|
148
|
+
if (!this.isEmpty(node) && !!node && typeof node === 'object' && 'must' in node) {
|
|
51
149
|
return node;
|
|
52
150
|
}
|
|
53
151
|
|
package/src/vector/index.test.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { QueryResult } from '@mastra/core';
|
|
4
4
|
import { describe, it, expect, beforeAll, afterAll, afterEach, vi, beforeEach } from 'vitest';
|
|
5
5
|
|
|
6
|
+
import type { QdrantVectorFilter } from './filter';
|
|
6
7
|
import { QdrantVector } from './index';
|
|
7
8
|
|
|
8
9
|
const dimension = 3;
|
|
@@ -77,7 +78,7 @@ describe('QdrantVector', () => {
|
|
|
77
78
|
|
|
78
79
|
it('should query vectors with metadata filter', async () => {
|
|
79
80
|
const queryVector = [0.0, 1.0, 0.0];
|
|
80
|
-
const filter = {
|
|
81
|
+
const filter: QdrantVectorFilter = {
|
|
81
82
|
label: 'y-axis',
|
|
82
83
|
};
|
|
83
84
|
|
|
@@ -350,21 +351,21 @@ describe('QdrantVector', () => {
|
|
|
350
351
|
|
|
351
352
|
describe('Basic Operators', () => {
|
|
352
353
|
it('should filter by exact value match', async () => {
|
|
353
|
-
const filter = { name: 'item1' };
|
|
354
|
+
const filter: QdrantVectorFilter = { name: 'item1' };
|
|
354
355
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
355
356
|
expect(results).toHaveLength(1);
|
|
356
357
|
expect(results[0]?.metadata?.name).toBe('item1');
|
|
357
358
|
});
|
|
358
359
|
|
|
359
360
|
it('should filter using comparison operators', async () => {
|
|
360
|
-
const filter = { price: { $gt: 100, $lt: 600 } };
|
|
361
|
+
const filter: QdrantVectorFilter = { price: { $gt: 100, $lt: 600 } };
|
|
361
362
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
362
363
|
expect(results).toHaveLength(1);
|
|
363
364
|
expect(results[0]?.metadata?.price).toBe(500);
|
|
364
365
|
});
|
|
365
366
|
|
|
366
367
|
it('should filter using array operators', async () => {
|
|
367
|
-
const filter = { tags: { $in: ['premium', 'bestseller'] } };
|
|
368
|
+
const filter: QdrantVectorFilter = { tags: { $in: ['premium', 'bestseller'] } };
|
|
368
369
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
369
370
|
expect(results).toHaveLength(2);
|
|
370
371
|
const tags = results.flatMap(r => r.metadata?.tags || []);
|
|
@@ -373,14 +374,14 @@ describe('QdrantVector', () => {
|
|
|
373
374
|
});
|
|
374
375
|
|
|
375
376
|
it('should handle null values', async () => {
|
|
376
|
-
const filter = { price: null };
|
|
377
|
+
const filter: QdrantVectorFilter = { price: null };
|
|
377
378
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
378
379
|
expect(results).toHaveLength(1);
|
|
379
380
|
expect(results[0]?.metadata?.price).toBeNull();
|
|
380
381
|
});
|
|
381
382
|
|
|
382
383
|
it('should handle empty arrays', async () => {
|
|
383
|
-
const filter = {
|
|
384
|
+
const filter: QdrantVectorFilter = {
|
|
384
385
|
tags: [],
|
|
385
386
|
};
|
|
386
387
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -392,7 +393,7 @@ describe('QdrantVector', () => {
|
|
|
392
393
|
|
|
393
394
|
describe('Logical Operators', () => {
|
|
394
395
|
it('should combine conditions with $and', async () => {
|
|
395
|
-
const filter = {
|
|
396
|
+
const filter: QdrantVectorFilter = {
|
|
396
397
|
$and: [{ tags: { $in: ['electronics'] } }, { price: { $gt: 700 } }],
|
|
397
398
|
};
|
|
398
399
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -402,7 +403,7 @@ describe('QdrantVector', () => {
|
|
|
402
403
|
});
|
|
403
404
|
|
|
404
405
|
it('should combine conditions with $or', async () => {
|
|
405
|
-
const filter = {
|
|
406
|
+
const filter: QdrantVectorFilter = {
|
|
406
407
|
$or: [{ price: { $gt: 900 } }, { tags: { $in: ['bestseller'] } }],
|
|
407
408
|
};
|
|
408
409
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -413,7 +414,7 @@ describe('QdrantVector', () => {
|
|
|
413
414
|
});
|
|
414
415
|
|
|
415
416
|
it('should handle $not operator', async () => {
|
|
416
|
-
const filter = {
|
|
417
|
+
const filter: QdrantVectorFilter = {
|
|
417
418
|
$not: { tags: { $in: ['electronics'] } },
|
|
418
419
|
};
|
|
419
420
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -425,7 +426,7 @@ describe('QdrantVector', () => {
|
|
|
425
426
|
});
|
|
426
427
|
|
|
427
428
|
it('should handle nested logical operators', async () => {
|
|
428
|
-
const filter = {
|
|
429
|
+
const filter: QdrantVectorFilter = {
|
|
429
430
|
$and: [
|
|
430
431
|
{ 'details.weight': { $lt: 2.0 } },
|
|
431
432
|
{
|
|
@@ -442,7 +443,7 @@ describe('QdrantVector', () => {
|
|
|
442
443
|
});
|
|
443
444
|
|
|
444
445
|
it('should handle empty logical operators', async () => {
|
|
445
|
-
const filter = { $and: [] };
|
|
446
|
+
const filter: QdrantVectorFilter = { $and: [] };
|
|
446
447
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
447
448
|
expect(results.length).toBeGreaterThan(0);
|
|
448
449
|
});
|
|
@@ -450,7 +451,7 @@ describe('QdrantVector', () => {
|
|
|
450
451
|
|
|
451
452
|
describe('Custom Operators', () => {
|
|
452
453
|
it('should filter using $count operator', async () => {
|
|
453
|
-
const filter = { 'stock.locations': { $count: { $gt: 1 } } };
|
|
454
|
+
const filter: QdrantVectorFilter = { 'stock.locations': { $count: { $gt: 1 } } };
|
|
454
455
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
455
456
|
expect(results).toHaveLength(2);
|
|
456
457
|
results.forEach(result => {
|
|
@@ -459,7 +460,7 @@ describe('QdrantVector', () => {
|
|
|
459
460
|
});
|
|
460
461
|
|
|
461
462
|
it('should filter using $geo radius operator', async () => {
|
|
462
|
-
const filter = {
|
|
463
|
+
const filter: QdrantVectorFilter = {
|
|
463
464
|
location: {
|
|
464
465
|
$geo: {
|
|
465
466
|
type: 'radius',
|
|
@@ -475,7 +476,7 @@ describe('QdrantVector', () => {
|
|
|
475
476
|
});
|
|
476
477
|
|
|
477
478
|
it('should filter using $geo box operator', async () => {
|
|
478
|
-
const filter = {
|
|
479
|
+
const filter: QdrantVectorFilter = {
|
|
479
480
|
location: {
|
|
480
481
|
$geo: {
|
|
481
482
|
type: 'box',
|
|
@@ -491,7 +492,7 @@ describe('QdrantVector', () => {
|
|
|
491
492
|
});
|
|
492
493
|
|
|
493
494
|
it('should filter using $geo polygon operator', async () => {
|
|
494
|
-
const filter = {
|
|
495
|
+
const filter: QdrantVectorFilter = {
|
|
495
496
|
location: {
|
|
496
497
|
$geo: {
|
|
497
498
|
type: 'polygon',
|
|
@@ -518,7 +519,7 @@ describe('QdrantVector', () => {
|
|
|
518
519
|
const allResults = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], topK: 2 });
|
|
519
520
|
const targetIds = allResults.map(r => r.id);
|
|
520
521
|
|
|
521
|
-
const filter = { $hasId: targetIds };
|
|
522
|
+
const filter: QdrantVectorFilter = { $hasId: targetIds };
|
|
522
523
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
523
524
|
expect(results).toHaveLength(2);
|
|
524
525
|
results.forEach(result => {
|
|
@@ -527,7 +528,7 @@ describe('QdrantVector', () => {
|
|
|
527
528
|
});
|
|
528
529
|
|
|
529
530
|
it('should filter using $hasVector operator', async () => {
|
|
530
|
-
const filter = { $hasVector: '' };
|
|
531
|
+
const filter: QdrantVectorFilter = { $hasVector: '' };
|
|
531
532
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
532
533
|
expect(results.length).toBeGreaterThan(0);
|
|
533
534
|
});
|
|
@@ -543,7 +544,7 @@ describe('QdrantVector', () => {
|
|
|
543
544
|
};
|
|
544
545
|
await qdrant.upsert({ indexName: testCollectionName, vectors: [vector], metadata: [metadata] });
|
|
545
546
|
|
|
546
|
-
const filter = {
|
|
547
|
+
const filter: QdrantVectorFilter = {
|
|
547
548
|
created_at: {
|
|
548
549
|
$datetime: {
|
|
549
550
|
range: {
|
|
@@ -603,7 +604,7 @@ describe('QdrantVector', () => {
|
|
|
603
604
|
expect(results.length).toBe(2);
|
|
604
605
|
});
|
|
605
606
|
it('should handle nested paths', async () => {
|
|
606
|
-
const filter = {
|
|
607
|
+
const filter: QdrantVectorFilter = {
|
|
607
608
|
'details.color': 'red',
|
|
608
609
|
'stock.quantity': { $gt: 0 },
|
|
609
610
|
};
|
|
@@ -614,7 +615,7 @@ describe('QdrantVector', () => {
|
|
|
614
615
|
});
|
|
615
616
|
|
|
616
617
|
it('should handle multiple conditions on same field', async () => {
|
|
617
|
-
const filter = {
|
|
618
|
+
const filter: QdrantVectorFilter = {
|
|
618
619
|
price: { $gt: 20, $lt: 30 },
|
|
619
620
|
};
|
|
620
621
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -623,7 +624,7 @@ describe('QdrantVector', () => {
|
|
|
623
624
|
});
|
|
624
625
|
|
|
625
626
|
it('should handle complex combinations', async () => {
|
|
626
|
-
const filter = {
|
|
627
|
+
const filter: QdrantVectorFilter = {
|
|
627
628
|
$and: [
|
|
628
629
|
{ 'details.weight': { $lt: 3.0 } },
|
|
629
630
|
{
|
|
@@ -642,7 +643,7 @@ describe('QdrantVector', () => {
|
|
|
642
643
|
});
|
|
643
644
|
|
|
644
645
|
it('should handle array paths with nested objects', async () => {
|
|
645
|
-
const filter = {
|
|
646
|
+
const filter: QdrantVectorFilter = {
|
|
646
647
|
'stock.locations[].warehouse': { $in: ['A'] },
|
|
647
648
|
};
|
|
648
649
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -653,7 +654,7 @@ describe('QdrantVector', () => {
|
|
|
653
654
|
});
|
|
654
655
|
|
|
655
656
|
it('should handle multiple nested paths with array notation', async () => {
|
|
656
|
-
const filter = {
|
|
657
|
+
const filter: QdrantVectorFilter = {
|
|
657
658
|
$and: [{ 'stock.locations[].warehouse': { $in: ['A'] } }, { 'stock.locations[].count': { $gt: 20 } }],
|
|
658
659
|
};
|
|
659
660
|
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
@@ -677,7 +678,7 @@ describe('QdrantVector', () => {
|
|
|
677
678
|
};
|
|
678
679
|
await qdrant.upsert({ indexName: testCollectionName, vectors: [vector], metadata: [metadata] });
|
|
679
680
|
|
|
680
|
-
const filter = {
|
|
681
|
+
const filter: QdrantVectorFilter = {
|
|
681
682
|
$and: [
|
|
682
683
|
{
|
|
683
684
|
'timestamps.created': {
|
|
@@ -696,7 +697,7 @@ describe('QdrantVector', () => {
|
|
|
696
697
|
});
|
|
697
698
|
|
|
698
699
|
it('should handle complex combinations with custom operators', async () => {
|
|
699
|
-
const filter = {
|
|
700
|
+
const filter: QdrantVectorFilter = {
|
|
700
701
|
$and: [
|
|
701
702
|
{ 'stock.locations': { $count: { $gt: 0 } } },
|
|
702
703
|
{
|
|
@@ -731,7 +732,7 @@ describe('QdrantVector', () => {
|
|
|
731
732
|
describe('Performance Cases', () => {
|
|
732
733
|
it('should handle deep nesting efficiently', async () => {
|
|
733
734
|
const start = Date.now();
|
|
734
|
-
const filter = {
|
|
735
|
+
const filter: QdrantVectorFilter = {
|
|
735
736
|
$and: Array(5)
|
|
736
737
|
.fill(null)
|
|
737
738
|
.map(() => ({
|
|
@@ -745,7 +746,11 @@ describe('QdrantVector', () => {
|
|
|
745
746
|
});
|
|
746
747
|
|
|
747
748
|
it('should handle multiple concurrent filtered queries', async () => {
|
|
748
|
-
const filters
|
|
749
|
+
const filters: QdrantVectorFilter[] = [
|
|
750
|
+
{ price: { $gt: 500 } },
|
|
751
|
+
{ tags: { $in: ['electronics'] } },
|
|
752
|
+
{ 'stock.quantity': { $gt: 0 } },
|
|
753
|
+
];
|
|
749
754
|
const start = Date.now();
|
|
750
755
|
const results = await Promise.all(
|
|
751
756
|
filters.map(filter => qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter })),
|