@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.
@@ -1,5 +1,103 @@
1
1
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
2
- import type { FieldCondition, VectorFilter, LogicalOperator, OperatorSupport } 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
+ >;
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?: VectorFilter): VectorFilter {
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: VectorFilter | FieldCondition, isNested: boolean = false, fieldKey?: string): any {
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
 
@@ -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 = [{ price: { $gt: 500 } }, { tags: { $in: ['electronics'] } }, { 'stock.quantity': { $gt: 0 } }];
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 })),