@mastra/astra 0.11.6 → 0.11.7-alpha.0

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,1278 +0,0 @@
1
- import { vi, describe, it, expect, beforeAll, afterAll, test } from 'vitest';
2
-
3
- import { AstraVector } from './';
4
-
5
- // Give tests enough time to complete database operations
6
- vi.setConfig({ testTimeout: 300000, hookTimeout: 300000 });
7
-
8
- // Helper function to wait for condition with timeout
9
- async function waitForCondition(
10
- condition: () => Promise<boolean>,
11
- timeout: number = 10000,
12
- interval: number = 1000,
13
- ): Promise<boolean> {
14
- const startTime = Date.now();
15
-
16
- while (Date.now() - startTime < timeout) {
17
- if (await condition()) {
18
- return true;
19
- }
20
- await new Promise(resolve => setTimeout(resolve, interval));
21
- }
22
-
23
- return false;
24
- }
25
-
26
- async function createIndexAndWait(
27
- vectorDB: AstraVector,
28
- indexName: string,
29
- dimension: number,
30
- metric: 'cosine' | 'euclidean' | 'dotproduct',
31
- ) {
32
- await vectorDB.createIndex({ indexName, dimension, metric });
33
- const created = await waitForCondition(
34
- async () => {
35
- const newCollections = await vectorDB.listIndexes();
36
- return newCollections.includes(indexName);
37
- },
38
- 30000,
39
- 2000,
40
- );
41
- if (!created) {
42
- throw new Error('Timed out waiting for collection to be created');
43
- }
44
- }
45
-
46
- async function deleteIndexAndWait(vectorDB: AstraVector, indexName: string) {
47
- await vectorDB.deleteIndex({ indexName });
48
- const deleted = await waitForCondition(
49
- async () => {
50
- const newCollections = await vectorDB.listIndexes();
51
- return !newCollections.includes(indexName);
52
- },
53
- 30000,
54
- 2000,
55
- );
56
- if (!deleted) {
57
- throw new Error('Timed out waiting for collection to be deleted');
58
- }
59
- }
60
-
61
- describe.skip('AstraVector Integration Tests', () => {
62
- let vectorDB: AstraVector;
63
- const testIndexName = 'testvectors1733728136118'; // Unique collection name
64
- const testIndexName2 = 'testvectors1733728136119'; // Unique collection name
65
-
66
- beforeAll(async () => {
67
- // Ensure required environment variables are set
68
- const token = process.env.ASTRA_DB_TOKEN;
69
- const endpoint = process.env.ASTRA_DB_ENDPOINT;
70
- const keyspace = process.env.ASTRA_DB_KEYSPACE;
71
-
72
- if (!token || !endpoint) {
73
- throw new Error('Please set ASTRA_DB_TOKEN and ASTRA_DB_ENDPOINT environment variables');
74
- }
75
-
76
- vectorDB = new AstraVector({
77
- token,
78
- endpoint,
79
- keyspace,
80
- });
81
- try {
82
- const collections = await vectorDB.listIndexes();
83
- await Promise.all(collections.map(c => vectorDB.deleteIndex({ indexName: c })));
84
- const deleted = await waitForCondition(
85
- async () => {
86
- const remainingCollections = await vectorDB.listIndexes();
87
- return remainingCollections.length === 0;
88
- },
89
- 30000,
90
- 2000,
91
- );
92
- if (!deleted) {
93
- throw new Error('Timed out waiting for collections to be deleted');
94
- }
95
- } catch (error) {
96
- console.error('Failed to delete test collections:', error);
97
- throw error;
98
- }
99
-
100
- await createIndexAndWait(vectorDB, testIndexName, 4, 'cosine');
101
- await createIndexAndWait(vectorDB, testIndexName2, 4, 'cosine');
102
- }, 500000);
103
-
104
- afterAll(async () => {
105
- // Cleanup: delete test collection
106
- try {
107
- await vectorDB.deleteIndex({ indexName: testIndexName });
108
- } catch (error) {
109
- console.error('Failed to delete test collection:', error);
110
- }
111
- try {
112
- await vectorDB.deleteIndex({ indexName: testIndexName2 });
113
- } catch (error) {
114
- console.error('Failed to delete test collection:', error);
115
- }
116
- });
117
-
118
- test('full vector database workflow', async () => {
119
- // Verify collection was created
120
- const indexes = await vectorDB.listIndexes();
121
- expect(indexes).toContain(testIndexName);
122
-
123
- // 2. Get collection stats
124
- const initialStats = await vectorDB.describeIndex({ indexName: testIndexName });
125
- expect(initialStats).toEqual({
126
- dimension: 4,
127
- metric: 'cosine',
128
- count: 0,
129
- });
130
-
131
- // 3. Insert vectors with metadata
132
- const vectors = [
133
- [1, 0, 0, 0],
134
- [0, 1, 0, 0],
135
- [0, 0, 1, 0],
136
- [0, 0, 0, 1],
137
- ];
138
-
139
- const metadata = [{ label: 'vector1' }, { label: 'vector2' }, { label: 'vector3' }, { label: 'vector4' }];
140
-
141
- const ids = await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
142
- expect(ids).toHaveLength(4);
143
-
144
- // Wait for document count to update (with timeout)
145
- const countUpdated = await waitForCondition(
146
- async () => {
147
- const stats = await vectorDB.describeIndex({ indexName: testIndexName });
148
- console.log('Current count:', stats.count);
149
- return stats.count === 4;
150
- },
151
- 15000, // 15 second timeout
152
- 2000, // Check every 2 seconds
153
- );
154
-
155
- if (!countUpdated) {
156
- console.warn('Document count did not update to expected value within timeout');
157
- }
158
-
159
- // 4. Query vectors
160
- const queryVector = [1, 0, 0, 0];
161
- const results = await vectorDB.query({ indexName: testIndexName, queryVector, topK: 2 });
162
-
163
- expect(results).toHaveLength(2);
164
- expect(results?.[0]?.metadata).toEqual({ label: 'vector1' });
165
- expect(results?.[0]?.score).toBeCloseTo(1, 4);
166
-
167
- // 5. Query with filter
168
- const filteredResults = await vectorDB.query({
169
- indexName: testIndexName,
170
- queryVector,
171
- topK: 2,
172
- filter: { 'metadata.label': 'vector2' },
173
- });
174
-
175
- expect(filteredResults).toHaveLength(1);
176
- expect(filteredResults?.[0]?.metadata).toEqual({ label: 'vector2' });
177
-
178
- // Get final stats
179
- const finalStats = await vectorDB.describeIndex({ indexName: testIndexName });
180
- console.log('Final stats:', finalStats);
181
-
182
- // More lenient assertion for document count
183
- expect(finalStats.count).toBeGreaterThan(0);
184
- if (finalStats.count !== 4) {
185
- console.warn(`Expected count of 4, but got ${finalStats.count}. This may be due to eventual consistency.`);
186
- }
187
- });
188
-
189
- test('gets vector results back from query', async () => {
190
- const queryVector = [1, 0, 0, 0];
191
- const results = await vectorDB.query({
192
- indexName: testIndexName,
193
- queryVector,
194
- topK: 2,
195
- includeVector: true,
196
- });
197
-
198
- expect(results).toHaveLength(2);
199
- expect(results?.[0]?.metadata).toEqual({ label: 'vector1' });
200
- expect(results?.[0]?.score).toBeCloseTo(1, 4);
201
- expect(results?.[0]?.vector).toEqual([1, 0, 0, 0]);
202
- });
203
-
204
- test('handles different vector dimensions', async () => {
205
- const highDimIndexName = 'high_dim_test_' + Date.now();
206
-
207
- try {
208
- // Create index with higher dimensions
209
- await createIndexAndWait(vectorDB, highDimIndexName, 1536, 'cosine');
210
-
211
- // Insert high-dimensional vectors
212
- const vectors = [
213
- Array(1536)
214
- .fill(0)
215
- .map((_, i) => i % 2), // Alternating 0s and 1s
216
- Array(1536)
217
- .fill(0)
218
- .map((_, i) => (i + 1) % 2), // Opposite pattern
219
- ];
220
-
221
- const metadata = [{ label: 'even' }, { label: 'odd' }];
222
-
223
- const ids = await vectorDB.upsert({
224
- indexName: highDimIndexName,
225
- vectors,
226
- metadata,
227
- });
228
- expect(ids).toHaveLength(2);
229
-
230
- // Wait for indexing with more generous timeout
231
- await new Promise(resolve => setTimeout(resolve, 2000));
232
-
233
- // Query with same pattern as first vector
234
- const queryVector = Array(1536)
235
- .fill(0)
236
- .map((_, i) => i % 2);
237
- const results = await vectorDB.query({
238
- indexName: highDimIndexName,
239
- queryVector,
240
- topK: 2,
241
- });
242
-
243
- expect(results).toHaveLength(2);
244
- expect(results?.[0]?.metadata).toEqual({ label: 'even' });
245
- expect(results?.[0]?.score).toBeCloseTo(1, 4);
246
- } finally {
247
- // Cleanup
248
- await deleteIndexAndWait(vectorDB, highDimIndexName);
249
- }
250
- });
251
-
252
- test('handles different distance metrics', async () => {
253
- const metrics = ['cosine', 'euclidean', 'dotproduct'] as const;
254
-
255
- for (const metric of metrics) {
256
- const metricIndexName = `metrictest${metric}${Date.now()}`;
257
-
258
- try {
259
- // Create index with different metric
260
- await createIndexAndWait(vectorDB, metricIndexName, 4, metric);
261
-
262
- // Insert same vectors
263
- const vectors = [
264
- [1, 0, 0, 0],
265
- [0.7071, 0.7071, 0, 0], // 45-degree angle from first vector
266
- ];
267
-
268
- await vectorDB.upsert({
269
- indexName: metricIndexName,
270
- vectors,
271
- });
272
-
273
- // Wait for indexing with more generous timeout
274
- await new Promise(resolve => setTimeout(resolve, 2000));
275
-
276
- // Query
277
- const results = await vectorDB.query({
278
- indexName: metricIndexName,
279
- queryVector: [1, 0, 0, 0],
280
- topK: 2,
281
- });
282
-
283
- expect(results).toHaveLength(2);
284
-
285
- // Scores will differ based on metric but order should be same
286
- expect(results?.[0]?.score).toBeGreaterThan(results?.[1]?.score!);
287
- } finally {
288
- // Cleanup
289
- await deleteIndexAndWait(vectorDB, metricIndexName);
290
- }
291
- }
292
- }, 500000);
293
-
294
- describe('Filter Validation in Queries', () => {
295
- it('rejects invalid operator values', async () => {
296
- await expect(
297
- vectorDB.query({
298
- indexName: testIndexName2,
299
- queryVector: [1, 0, 0, 0],
300
- filter: { tags: { $all: 'not-an-array' as any } },
301
- }),
302
- ).rejects.toThrow();
303
- });
304
-
305
- it('validates array operator values', async () => {
306
- await expect(
307
- vectorDB.query({
308
- indexName: testIndexName2,
309
- queryVector: [1, 0, 0, 0],
310
- filter: { tags: { $in: null as any } },
311
- }),
312
- ).rejects.toThrow();
313
- });
314
-
315
- it('validates $in operators', async () => {
316
- const invalidValues = [123, 'string', true, { key: 'value' }, {}, null];
317
- for (const val of invalidValues) {
318
- await expect(
319
- vectorDB.query({
320
- indexName: testIndexName2,
321
- queryVector: [1, 0, 0, 0],
322
- filter: { field: { $in: val as any } },
323
- }),
324
- ).rejects.toThrow();
325
- }
326
- });
327
-
328
- it('validates $nin operators', async () => {
329
- const invalidValues = [123, 'string', true, { key: 'value' }, {}, null];
330
- for (const val of invalidValues) {
331
- await expect(
332
- vectorDB.query({
333
- indexName: testIndexName2,
334
- queryVector: [1, 0, 0, 0],
335
- filter: { field: { $nin: val as any } },
336
- }),
337
- ).rejects.toThrow();
338
- }
339
- });
340
-
341
- it('validates $all operators', async () => {
342
- const invalidValues = [123, 'string', true, { key: 'value' }, {}, [], null];
343
- for (const val of invalidValues) {
344
- await expect(
345
- vectorDB.query({
346
- indexName: testIndexName2,
347
- queryVector: [1, 0, 0, 0],
348
- filter: { field: { $all: val as any } },
349
- }),
350
- ).rejects.toThrow();
351
- }
352
- });
353
-
354
- it('validates element operators', async () => {
355
- const invalidValues = [123, 'string', { key: 'value' }, {}, [], null];
356
- for (const val of invalidValues) {
357
- await expect(
358
- vectorDB.query({
359
- indexName: testIndexName2,
360
- queryVector: [1, 0, 0, 0],
361
- filter: { field: { $exists: val as any } },
362
- }),
363
- ).rejects.toThrow();
364
- }
365
- });
366
-
367
- it('validates comparison operators', async () => {
368
- // Numeric comparisons require numbers
369
- const numOps = ['$gt', '$gte', '$lt', '$lte'];
370
- const invalidNumericValues = [[], {}, null];
371
- for (const op of numOps) {
372
- for (const val of invalidNumericValues) {
373
- await expect(
374
- vectorDB.query({
375
- indexName: testIndexName2,
376
- queryVector: [1, 0, 0, 0],
377
- filter: { field: { [op]: val } },
378
- }),
379
- ).rejects.toThrow();
380
- }
381
- }
382
- });
383
-
384
- it('validates multiple invalid values', async () => {
385
- await expect(
386
- vectorDB.query({
387
- indexName: testIndexName2,
388
- queryVector: [1, 0, 0, 0],
389
- filter: {
390
- field1: { $in: 'not-array' as any },
391
- field2: { $exists: 'not-boolean' as any },
392
- field3: { $gt: 'not-number' as any },
393
- },
394
- }),
395
- ).rejects.toThrow();
396
- });
397
- });
398
-
399
- describe('Metadata Filter Tests', () => {
400
- // Set up test vectors and metadata
401
- beforeAll(async () => {
402
- const vectors = [
403
- [1, 0, 0, 0], // Electronics
404
- [0, 1, 0, 0], // Books
405
- [0, 0, 1, 0], // Electronics
406
- [0, 0, 0, 1], // Books
407
- ];
408
-
409
- const metadata = [
410
- {
411
- category: 'electronics',
412
- price: 1000,
413
- rating: 4.8,
414
- tags: ['premium', 'new'],
415
- inStock: true,
416
- specs: {
417
- color: 'black',
418
- weight: 2.5,
419
- },
420
- },
421
- {
422
- category: 'books',
423
- price: 25,
424
- rating: 4.2,
425
- tags: ['bestseller'],
426
- inStock: true,
427
- author: {
428
- name: 'John Doe',
429
- country: 'USA',
430
- },
431
- },
432
- {
433
- category: 'electronics',
434
- price: 500,
435
- rating: 4.5,
436
- tags: ['refurbished', 'premium'],
437
- inStock: false,
438
- specs: {
439
- color: 'silver',
440
- weight: 1.8,
441
- },
442
- },
443
- {
444
- category: 'books',
445
- price: 15,
446
- rating: 4.9,
447
- tags: ['bestseller', 'new'],
448
- inStock: true,
449
- author: {
450
- name: 'Jane Smith',
451
- country: 'UK',
452
- },
453
- },
454
- ];
455
-
456
- await vectorDB.upsert({
457
- indexName: testIndexName2,
458
- vectors,
459
- metadata,
460
- });
461
- // Wait for indexing
462
- await new Promise(resolve => setTimeout(resolve, 2000));
463
- });
464
-
465
- describe('Basic Comparison Operators', () => {
466
- it('filters with $eq operator', async () => {
467
- const results = await vectorDB.query({
468
- indexName: testIndexName2,
469
- queryVector: [1, 0, 0, 0],
470
- filter: {
471
- 'metadata.category': { $eq: 'electronics' },
472
- },
473
- });
474
- expect(results.length).toBe(2);
475
- results.forEach(result => {
476
- expect(result.metadata?.category).toBe('electronics');
477
- });
478
- });
479
-
480
- it('filters with $gt operator', async () => {
481
- const results = await vectorDB.query({
482
- indexName: testIndexName2,
483
- queryVector: [1, 0, 0, 0],
484
- filter: {
485
- 'metadata.price': { $gt: 500 },
486
- },
487
- });
488
- expect(results.length).toBe(1);
489
- results.forEach(result => {
490
- expect(Number(result.metadata?.price)).toBeGreaterThan(500);
491
- });
492
- });
493
-
494
- it('filters with $gte, $lt, $lte operators', async () => {
495
- const results = await vectorDB.query({
496
- indexName: testIndexName2,
497
- queryVector: [1, 0, 0, 0],
498
- filter: {
499
- 'metadata.price': { $gte: 25, $lte: 500 },
500
- },
501
- });
502
- expect(results.length).toBe(2);
503
- results.forEach(result => {
504
- expect(Number(result.metadata?.price)).toBeLessThanOrEqual(500);
505
- expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(25);
506
- });
507
- });
508
-
509
- it('filters with $ne operator', async () => {
510
- const results = await vectorDB.query({
511
- indexName: testIndexName2,
512
- queryVector: [1, 0, 0, 0],
513
- filter: {
514
- 'metadata.category': { $ne: 'books' },
515
- },
516
- });
517
- expect(results.length).toBe(2);
518
- results.forEach(result => {
519
- expect(result.metadata?.category).not.toBe('books');
520
- });
521
- });
522
- });
523
-
524
- describe('Null/Undefined/Empty FIlters', () => {
525
- it('should handle undefined filter', async () => {
526
- const results1 = await vectorDB.query({
527
- indexName: testIndexName2,
528
- queryVector: [1, 0, 0, 0],
529
- filter: undefined,
530
- });
531
- const results2 = await vectorDB.query({
532
- indexName: testIndexName2,
533
- queryVector: [1, 0, 0, 0],
534
- });
535
- expect(results1).toEqual(results2);
536
- expect(results1.length).toBeGreaterThan(0);
537
- });
538
-
539
- it('should handle empty object filter', async () => {
540
- const results = await vectorDB.query({
541
- indexName: testIndexName2,
542
- queryVector: [1, 0, 0, 0],
543
- filter: {},
544
- });
545
- const results2 = await vectorDB.query({
546
- indexName: testIndexName2,
547
- queryVector: [1, 0, 0, 0],
548
- });
549
- expect(results).toEqual(results2);
550
- expect(results.length).toBeGreaterThan(0);
551
- });
552
-
553
- it('should handle null filter', async () => {
554
- const results = await vectorDB.query({
555
- indexName: testIndexName2,
556
- queryVector: [1, 0, 0, 0],
557
- filter: null,
558
- });
559
- const results2 = await vectorDB.query({
560
- indexName: testIndexName2,
561
- queryVector: [1, 0, 0, 0],
562
- });
563
- expect(results).toEqual(results2);
564
- expect(results.length).toBeGreaterThan(0);
565
- });
566
- });
567
-
568
- describe('Array Operators', () => {
569
- it('filters with $in operator', async () => {
570
- const results = await vectorDB.query({
571
- indexName: testIndexName2,
572
- queryVector: [1, 0, 0, 0],
573
- filter: {
574
- 'metadata.tags': { $in: ['premium'] },
575
- },
576
- });
577
- expect(results.length).toBe(2);
578
- results.forEach(result => {
579
- expect(result.metadata?.tags).toContain('premium');
580
- });
581
- });
582
-
583
- it('filters with $nin operator', async () => {
584
- const results = await vectorDB.query({
585
- indexName: testIndexName2,
586
- queryVector: [1, 0, 0, 0],
587
- filter: {
588
- 'metadata.tags': { $nin: ['bestseller'] },
589
- },
590
- });
591
- expect(results.length).toBe(2);
592
- results.forEach(result => {
593
- expect(result.metadata?.tags).not.toContain('bestseller');
594
- });
595
- });
596
-
597
- it('filters with $all operator', async () => {
598
- const results = await vectorDB.query({
599
- indexName: testIndexName2,
600
- queryVector: [1, 0, 0, 0],
601
- filter: {
602
- 'metadata.tags': { $all: ['premium', 'new'] },
603
- },
604
- });
605
- expect(results.length).toBe(1);
606
- results.forEach(result => {
607
- expect(result.metadata?.tags).toContain('premium');
608
- expect(result.metadata?.tags).toContain('new');
609
- });
610
- });
611
- });
612
-
613
- describe('Logical Operators', () => {
614
- it('filters with $and operator', async () => {
615
- const results = await vectorDB.query({
616
- indexName: testIndexName2,
617
- queryVector: [1, 0, 0, 0],
618
- filter: {
619
- $and: [{ 'metadata.category': 'electronics' }, { 'metadata.price': { $gt: 500 } }],
620
- },
621
- });
622
- expect(results.length).toBe(1);
623
- expect(results[0]?.metadata?.category).toBe('electronics');
624
- expect(Number(results[0]?.metadata?.price)).toBeGreaterThan(500);
625
- });
626
-
627
- it('filters with $or operator', async () => {
628
- const results = await vectorDB.query({
629
- indexName: testIndexName2,
630
- queryVector: [1, 0, 0, 0],
631
- filter: {
632
- $or: [{ 'metadata.price': { $gt: 900 } }, { 'metadata.rating': { $gt: 4.8 } }],
633
- },
634
- });
635
- expect(results.length).toBe(2);
636
- results.forEach(result => {
637
- expect(Number(result.metadata?.price) > 900 || Number(result.metadata?.rating) > 4.8).toBe(true);
638
- });
639
- });
640
-
641
- it('filters with direct field comparison', async () => {
642
- const results = await vectorDB.query({
643
- indexName: testIndexName2,
644
- queryVector: [1, 0, 0, 0],
645
- filter: {
646
- $not: { 'metadata.category': 'electronics' }, // Simple field equality
647
- },
648
- });
649
- expect(results.length).toBe(2);
650
- results.forEach(result => {
651
- expect(result.metadata?.category).not.toBe('electronics');
652
- });
653
- });
654
-
655
- it('filters with $eq operator', async () => {
656
- const results = await vectorDB.query({
657
- indexName: testIndexName2,
658
- queryVector: [1, 0, 0, 0],
659
- filter: {
660
- $not: { 'metadata.category': { $eq: 'electronics' } },
661
- },
662
- });
663
- expect(results.length).toBe(2);
664
- results.forEach(result => {
665
- expect(result.metadata?.category).not.toBe('electronics');
666
- });
667
- });
668
-
669
- it('filters with multiple conditions on same field using implicit $and', async () => {
670
- const results = await vectorDB.query({
671
- indexName: testIndexName2,
672
- queryVector: [1, 0, 0, 0],
673
- filter: {
674
- 'metadata.category': 'electronics',
675
- 'metadata.price': 1000,
676
- },
677
- });
678
- expect(results.length).toBe(1);
679
- });
680
-
681
- it('filters with multiple fields', async () => {
682
- const results = await vectorDB.query({
683
- indexName: testIndexName2,
684
- queryVector: [1, 0, 0, 0],
685
- filter: {
686
- $not: {
687
- 'metadata.category': 'electronics',
688
- 'metadata.price': 100,
689
- },
690
- },
691
- });
692
- expect(results.length).toBeGreaterThan(0);
693
- results.forEach(result => {
694
- expect(result.metadata?.category === 'electronics' && result.metadata?.price === 100).toBe(false);
695
- });
696
- });
697
-
698
- it('uses $not within $or', async () => {
699
- const results = await vectorDB.query({
700
- indexName: testIndexName2,
701
- queryVector: [1, 0, 0, 0],
702
- filter: {
703
- $or: [{ $not: { 'metadata.category': 'electronics' } }, { 'metadata.price': { $gt: 100 } }],
704
- },
705
- });
706
- expect(results.length).toBeGreaterThan(0);
707
- });
708
-
709
- // Test $not with $exists
710
- it('filters with $exists', async () => {
711
- const results = await vectorDB.query({
712
- indexName: testIndexName2,
713
- queryVector: [1, 0, 0, 0],
714
- filter: {
715
- $not: { 'metadata.optional_field': { $exists: true } },
716
- },
717
- });
718
- expect(results.length).toBeGreaterThan(0);
719
- });
720
-
721
- it('filters with nested logical operators', async () => {
722
- const results = await vectorDB.query({
723
- indexName: testIndexName2,
724
- queryVector: [1, 0, 0, 0],
725
- filter: {
726
- $and: [
727
- { 'metadata.category': 'electronics' },
728
- {
729
- $or: [{ 'metadata.price': { $gt: 900 } }, { 'metadata.tags': { $all: ['refurbished'] } }],
730
- },
731
- ],
732
- },
733
- });
734
- expect(results.length).toBe(2);
735
- results.forEach(result => {
736
- expect(result.metadata?.category).toBe('electronics');
737
- expect(Number(result.metadata?.price) > 900 || result.metadata?.tags?.includes('refurbished')).toBe(true);
738
- });
739
- });
740
- });
741
-
742
- describe('Nested Field Queries', () => {
743
- it('filters on nested object fields', async () => {
744
- const results = await vectorDB.query({
745
- indexName: testIndexName2,
746
- queryVector: [1, 0, 0, 0],
747
- filter: {
748
- 'metadata.specs.color': 'black',
749
- },
750
- });
751
- expect(results.length).toBe(1);
752
- expect(results[0]?.metadata?.specs?.color).toBe('black');
753
- });
754
-
755
- it('combines nested field queries with logical operators', async () => {
756
- const results = await vectorDB.query({
757
- indexName: testIndexName2,
758
- queryVector: [1, 0, 0, 0],
759
- filter: {
760
- $or: [{ 'metadata.specs.weight': { $lt: 2.0 } }, { 'metadata.author.country': 'UK' }],
761
- },
762
- });
763
- expect(results.length).toBe(2);
764
- results.forEach(result => {
765
- expect(result.metadata?.specs?.weight < 2.0 || result.metadata?.author?.country === 'UK').toBe(true);
766
- });
767
- });
768
- });
769
-
770
- describe('Complex Filter Combinations', () => {
771
- it('combines multiple operators and conditions', async () => {
772
- const results = await vectorDB.query({
773
- indexName: testIndexName2,
774
- queryVector: [1, 0, 0, 0],
775
- filter: {
776
- $and: [
777
- { 'metadata.price': { $gt: 20 } },
778
- { 'metadata.inStock': true },
779
- {
780
- $or: [{ 'metadata.tags': { $in: ['premium'] } }, { 'metadata.rating': { $gt: 4.5 } }],
781
- },
782
- ],
783
- },
784
- });
785
- expect(results.length).toBeGreaterThan(0);
786
- results.forEach(result => {
787
- expect(Number(result.metadata?.price)).toBeGreaterThan(20);
788
- expect(result.metadata?.inStock).toBe(true);
789
- expect(result.metadata?.tags?.includes('premium') || Number(result.metadata?.rating) > 4.5).toBe(true);
790
- });
791
- });
792
-
793
- it('handles complex nested conditions', async () => {
794
- const results = await vectorDB.query({
795
- indexName: testIndexName2,
796
- queryVector: [1, 0, 0, 0],
797
- filter: {
798
- $or: [
799
- {
800
- $and: [
801
- { 'metadata.category': 'electronics' },
802
- { 'metadata.specs.weight': { $lt: 2.0 } },
803
- { 'metadata.tags': { $in: ['premium'] } },
804
- ],
805
- },
806
- {
807
- $and: [
808
- { 'metadata.category': 'books' },
809
- { 'metadata.price': { $lt: 20 } },
810
- { 'metadata.author.country': 'UK' },
811
- ],
812
- },
813
- ],
814
- },
815
- });
816
- expect(results.length).toBeGreaterThan(0);
817
- results.forEach(result => {
818
- if (result.metadata?.category === 'electronics') {
819
- expect(result.metadata?.specs?.weight).toBeLessThan(2.0);
820
- expect(result.metadata?.tags).toContain('premium');
821
- } else {
822
- expect(Number(result.metadata?.price)).toBeLessThan(20);
823
- expect(result.metadata?.author?.country).toBe('UK');
824
- }
825
- });
826
- });
827
- });
828
-
829
- describe('Field Existence and Null Checks', () => {
830
- beforeAll(async () => {
831
- // Add some vectors with special metadata cases
832
- const vectors = [
833
- [0.5, 0.5, 0.5, 0.5],
834
- [0.3, 0.3, 0.3, 0.3],
835
- ];
836
-
837
- const metadata = [
838
- {
839
- category: 'special',
840
- optionalField: null,
841
- emptyArray: [],
842
- nested: {
843
- existingField: 'value',
844
- nullField: null,
845
- },
846
- },
847
- {
848
- category: 'special',
849
- // optionalField intentionally missing
850
- emptyArray: ['single'],
851
- nested: {
852
- // existingField intentionally missing
853
- otherField: 'value',
854
- },
855
- },
856
- ];
857
-
858
- await vectorDB.upsert({
859
- indexName: testIndexName2,
860
- vectors,
861
- metadata,
862
- });
863
- await new Promise(resolve => setTimeout(resolve, 2000));
864
- });
865
-
866
- it('filters based on field existence', async () => {
867
- const results = await vectorDB.query({
868
- indexName: testIndexName2,
869
- queryVector: [1, 0, 0, 0],
870
- filter: {
871
- 'metadata.optionalField': { $exists: true },
872
- },
873
- });
874
- expect(results.length).toBe(1);
875
- expect('optionalField' in results[0]!.metadata!).toBe(true);
876
- });
877
-
878
- it('filters for null values', async () => {
879
- const results = await vectorDB.query({
880
- indexName: testIndexName2,
881
- queryVector: [1, 0, 0, 0],
882
- filter: {
883
- 'metadata.nested.nullField': null,
884
- },
885
- });
886
- expect(results.length).toBe(1);
887
- expect(results[0]!.metadata!.nested.nullField).toBeNull();
888
- });
889
-
890
- it('combines existence checks with other operators', async () => {
891
- const results = await vectorDB.query({
892
- indexName: testIndexName2,
893
- queryVector: [1, 0, 0, 0],
894
- filter: {
895
- $and: [{ 'metadata.category': 'special' }, { 'metadata.optionalField': { $exists: false } }],
896
- },
897
- });
898
- expect(results.length).toBe(1);
899
- expect(results[0]!.metadata!.category).toBe('special');
900
- expect('optionalField' in results[0]!.metadata!).toBe(false);
901
- });
902
-
903
- it('handles empty array edge cases', async () => {
904
- const results = await vectorDB.query({
905
- indexName: testIndexName2,
906
- queryVector: [1, 0, 0, 0],
907
- filter: {
908
- 'metadata.emptyArray': { $size: 0 },
909
- },
910
- });
911
- expect(results.length).toBe(1);
912
- expect(results[0]!.metadata!.emptyArray).toHaveLength(0);
913
- });
914
- });
915
-
916
- describe('Date and Numeric Edge Cases', () => {
917
- beforeAll(async () => {
918
- const vectors = [
919
- [0.1, 0.1, 0.1, 0.1],
920
- [0.2, 0.2, 0.2, 0.2],
921
- ];
922
-
923
- const metadata = [
924
- {
925
- numericFields: {
926
- zero: 0,
927
- negativeZero: -0,
928
- infinity: Infinity,
929
- negativeInfinity: -Infinity,
930
- decimal: 0.1,
931
- negativeDecimal: -0.1,
932
- },
933
- dateFields: {
934
- current: new Date().toISOString(),
935
- epoch: new Date(0).toISOString(),
936
- future: new Date('2100-01-01').toISOString(),
937
- },
938
- },
939
- {
940
- numericFields: {
941
- maxInt: Number.MAX_SAFE_INTEGER,
942
- minInt: Number.MIN_SAFE_INTEGER,
943
- maxFloat: Number.MAX_VALUE,
944
- minFloat: Number.MIN_VALUE,
945
- },
946
- dateFields: {
947
- past: new Date('1900-01-01').toISOString(),
948
- current: new Date().toISOString(),
949
- },
950
- },
951
- ];
952
-
953
- await vectorDB.upsert({
954
- indexName: testIndexName2,
955
- vectors,
956
- metadata,
957
- });
958
- await new Promise(resolve => setTimeout(resolve, 2000));
959
- });
960
-
961
- it('handles special numeric values', async () => {
962
- const results = await vectorDB.query({
963
- indexName: testIndexName2,
964
- queryVector: [1, 0, 0, 0],
965
- filter: {
966
- $or: [{ 'metadata.numericFields.zero': 0 }, { 'metadata.numericFields.negativeZero': 0 }],
967
- },
968
- });
969
- expect(results.length).toBeGreaterThan(0);
970
- results.forEach(result => {
971
- const value = result.metadata?.numericFields?.zero ?? result.metadata?.numericFields?.negativeZero;
972
- expect(value).toBe(0);
973
- });
974
- });
975
-
976
- it('compares dates correctly', async () => {
977
- const now = new Date().toISOString();
978
- const results = await vectorDB.query({
979
- indexName: testIndexName2,
980
- queryVector: [1, 0, 0, 0],
981
- filter: {
982
- $and: [
983
- { 'metadata.dateFields.current': { $lte: now } },
984
- { 'metadata.dateFields.current': { $gt: new Date(0).toISOString() } },
985
- ],
986
- },
987
- });
988
- expect(results.length).toBeGreaterThan(0);
989
- });
990
-
991
- it('handles extreme numeric values', async () => {
992
- const results = await vectorDB.query({
993
- indexName: testIndexName2,
994
- queryVector: [1, 0, 0, 0],
995
- filter: {
996
- $or: [
997
- { 'metadata.numericFields.maxInt': { $gte: Number.MAX_SAFE_INTEGER } },
998
- { 'metadata.numericFields.minInt': { $lte: Number.MIN_SAFE_INTEGER } },
999
- ],
1000
- },
1001
- });
1002
- expect(results.length).toBe(1);
1003
- });
1004
- });
1005
-
1006
- describe('Advanced Array Operations', () => {
1007
- beforeAll(async () => {
1008
- const vectors = [
1009
- [0.7, 0.7, 0.7, 0.7],
1010
- [0.8, 0.8, 0.8, 0.8],
1011
- [0.9, 0.9, 0.9, 0.9],
1012
- ];
1013
-
1014
- const metadata = [
1015
- {
1016
- arrays: {
1017
- empty: [],
1018
- single: ['one'],
1019
- multiple: ['one', 'two', 'three'],
1020
- nested: [['inner']],
1021
- },
1022
- },
1023
- {
1024
- arrays: {
1025
- empty: [],
1026
- single: ['two'],
1027
- multiple: ['two', 'three'],
1028
- nested: [['inner'], ['outer']],
1029
- },
1030
- },
1031
- {
1032
- arrays: {
1033
- single: ['three'],
1034
- multiple: ['three', 'four', 'five'],
1035
- nested: [],
1036
- },
1037
- },
1038
- ];
1039
-
1040
- await vectorDB.upsert({
1041
- indexName: testIndexName2,
1042
- vectors,
1043
- metadata,
1044
- });
1045
- await new Promise(resolve => setTimeout(resolve, 2000));
1046
- });
1047
-
1048
- it('handles $in with empty array input', async () => {
1049
- const results = await vectorDB.query({
1050
- indexName: testIndexName2,
1051
- queryVector: [1, 0, 0, 0],
1052
- filter: {
1053
- 'metadata.arrays.single': { $in: [] },
1054
- },
1055
- });
1056
- expect(results.length).toBe(0);
1057
- });
1058
-
1059
- it('combines $size with $exists for array fields', async () => {
1060
- const results = await vectorDB.query({
1061
- indexName: testIndexName2,
1062
- queryVector: [1, 0, 0, 0],
1063
- filter: {
1064
- $and: [{ 'metadata.arrays.empty': { $exists: true } }, { 'metadata.arrays.empty': { $size: 0 } }],
1065
- },
1066
- });
1067
- expect(results.length).toBe(2);
1068
- results.forEach(result => {
1069
- expect(result.metadata?.arrays?.empty).toBeDefined();
1070
- expect(result.metadata?.arrays?.empty).toHaveLength(0);
1071
- });
1072
- });
1073
-
1074
- it('filters arrays by exact size matching', async () => {
1075
- const results = await vectorDB.query({
1076
- indexName: testIndexName2,
1077
- queryVector: [1, 0, 0, 0],
1078
- filter: {
1079
- $and: [{ 'metadata.arrays.multiple': { $size: 3 } }, { 'metadata.arrays.multiple': { $in: ['two'] } }],
1080
- },
1081
- });
1082
- expect(results.length).toBe(1);
1083
- expect(results[0]?.metadata?.arrays?.multiple).toContain('two');
1084
- expect(results[0]?.metadata?.arrays?.multiple).toHaveLength(3);
1085
- });
1086
- });
1087
- });
1088
-
1089
- describe('Basic vector operations', () => {
1090
- const indexName = 'testbasicvectoroperations';
1091
-
1092
- beforeAll(async () => {
1093
- await createIndexAndWait(vectorDB, indexName, 4, 'cosine');
1094
- });
1095
-
1096
- afterAll(async () => {
1097
- await deleteIndexAndWait(vectorDB, indexName);
1098
- });
1099
-
1100
- const testVectors = [
1101
- [1, 0, 0, 0],
1102
- [0, 1, 0, 0],
1103
- [0, 0, 1, 0],
1104
- [0, 0, 0, 1],
1105
- ];
1106
-
1107
- it('should update the vector by id', async () => {
1108
- const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
1109
- expect(ids).toHaveLength(4);
1110
-
1111
- const idToBeUpdated = ids[0];
1112
- const newVector = [1, 2, 3, 4];
1113
- const newMetaData = {
1114
- test: 'updates',
1115
- };
1116
-
1117
- const update = {
1118
- vector: newVector,
1119
- metadata: newMetaData,
1120
- };
1121
-
1122
- await vectorDB.updateVector({ indexName, id: idToBeUpdated, update });
1123
-
1124
- await new Promise(resolve => setTimeout(resolve, 2000));
1125
-
1126
- const results = await vectorDB.query({
1127
- indexName,
1128
- queryVector: newVector,
1129
- topK: 2,
1130
- includeVector: true,
1131
- });
1132
-
1133
- expect(results).toHaveLength(2);
1134
- const updatedResult = results.find(result => result.id === idToBeUpdated);
1135
- expect(updatedResult).toBeDefined();
1136
- expect(updatedResult?.id).toEqual(idToBeUpdated);
1137
- expect(updatedResult?.vector).toEqual(newVector);
1138
- expect(updatedResult?.metadata).toEqual(newMetaData);
1139
- });
1140
-
1141
- it('should only update the metadata by id', async () => {
1142
- const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
1143
- expect(ids).toHaveLength(4);
1144
-
1145
- const idToBeUpdated = ids[0];
1146
- const newMetaData = {
1147
- test: 'updates',
1148
- };
1149
-
1150
- const update = {
1151
- metadata: newMetaData,
1152
- };
1153
-
1154
- await vectorDB.updateVector({ indexName, id: idToBeUpdated, update });
1155
- await new Promise(resolve => setTimeout(resolve, 2000));
1156
-
1157
- const results = await vectorDB.query({
1158
- indexName,
1159
- queryVector: testVectors[0],
1160
- topK: 2,
1161
- includeVector: true,
1162
- });
1163
-
1164
- expect(results).toHaveLength(2);
1165
- const updatedResult = results.find(result => result.id === idToBeUpdated);
1166
- expect(updatedResult).toBeDefined();
1167
- expect(updatedResult?.id).toEqual(idToBeUpdated);
1168
- expect(updatedResult?.vector).toEqual(testVectors[0]);
1169
- expect(updatedResult?.metadata).toEqual(newMetaData);
1170
- });
1171
-
1172
- it('should only update vector embeddings by id', async () => {
1173
- const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
1174
- expect(ids).toHaveLength(4);
1175
-
1176
- const idToBeUpdated = ids[0];
1177
- const newVector = [1, 2, 3, 4];
1178
-
1179
- const update = {
1180
- vector: newVector,
1181
- };
1182
-
1183
- await vectorDB.updateVector({ indexName, id: idToBeUpdated, update });
1184
- await new Promise(resolve => setTimeout(resolve, 2000));
1185
-
1186
- const results = await vectorDB.query({
1187
- indexName,
1188
- queryVector: newVector,
1189
- topK: 2,
1190
- includeVector: true,
1191
- });
1192
-
1193
- expect(results).toHaveLength(2);
1194
- const updatedResult = results.find(result => result.id === idToBeUpdated);
1195
- expect(updatedResult).toBeDefined();
1196
- expect(updatedResult?.id).toEqual(idToBeUpdated);
1197
- expect(updatedResult?.vector).toEqual(newVector);
1198
- });
1199
-
1200
- it('should throw exception when no updates are given', async () => {
1201
- await expect(vectorDB.updateVector({ indexName, id: 'id', update: {} })).rejects.toThrow('No updates provided');
1202
- });
1203
-
1204
- it('should delete the vector by id', async () => {
1205
- const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
1206
- expect(ids).toHaveLength(4);
1207
-
1208
- const idToBeDeleted = ids[0];
1209
- await vectorDB.deleteVector({ indexName, id: idToBeDeleted });
1210
-
1211
- const results = await vectorDB.query({
1212
- indexName: indexName,
1213
- queryVector: [1, 0, 0, 0],
1214
- topK: 2,
1215
- });
1216
-
1217
- expect(results).toHaveLength(2);
1218
- expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
1219
- });
1220
- });
1221
-
1222
- describe('Error Handling', () => {
1223
- const testIndexName = 'test_index_error';
1224
- beforeAll(async () => {
1225
- await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
1226
- });
1227
-
1228
- afterAll(async () => {
1229
- await vectorDB.deleteIndex({ indexName: testIndexName });
1230
- });
1231
-
1232
- it('should handle non-existent index queries', async () => {
1233
- await expect(vectorDB.query({ indexName: 'non-existent-index', queryVector: [1, 2, 3] })).rejects.toThrow();
1234
- });
1235
-
1236
- it('should handle invalid dimension vectors', async () => {
1237
- const invalidVector = [1, 2, 3, 4]; // 4D vector for 3D index
1238
- await expect(vectorDB.upsert({ indexName: testIndexName, vectors: [invalidVector] })).rejects.toThrow();
1239
- });
1240
-
1241
- it('should handle duplicate index creation gracefully', async () => {
1242
- const duplicateIndexName = `duplicate_test`;
1243
- const dimension = 768;
1244
-
1245
- try {
1246
- // Create index first time
1247
- await vectorDB.createIndex({
1248
- indexName: duplicateIndexName,
1249
- dimension,
1250
- metric: 'cosine',
1251
- });
1252
-
1253
- // Try to create with same dimensions - should not throw
1254
- await expect(
1255
- vectorDB.createIndex({
1256
- indexName: duplicateIndexName,
1257
- dimension,
1258
- metric: 'cosine',
1259
- }),
1260
- ).resolves.not.toThrow();
1261
-
1262
- // Try to create with different dimensions - should throw
1263
- await expect(
1264
- vectorDB.createIndex({
1265
- indexName: duplicateIndexName,
1266
- dimension: dimension + 1,
1267
- metric: 'cosine',
1268
- }),
1269
- ).rejects.toThrow(
1270
- `Collection already exists: trying to create Collection ('${duplicateIndexName}') with different settings`,
1271
- );
1272
- } finally {
1273
- // Cleanup
1274
- await vectorDB.deleteIndex({ indexName: duplicateIndexName });
1275
- }
1276
- });
1277
- });
1278
- });