@mastra/astra 0.1.0-alpha.28

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.
@@ -0,0 +1,853 @@
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: 3000000 });
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
+ describe('AstraVector Integration Tests', () => {
27
+ let vectorDB: AstraVector;
28
+ const testIndexName = 'testvectors1733728136118'; // Unique collection name
29
+ const testIndexName2 = 'testvectors1733728136119'; // Unique collection name
30
+
31
+ console.log('testIndexName:', testIndexName);
32
+
33
+ beforeAll(async () => {
34
+ // Ensure required environment variables are set
35
+ const token = process.env.ASTRA_DB_TOKEN;
36
+ const endpoint = process.env.ASTRA_DB_ENDPOINT;
37
+ const keyspace = process.env.ASTRA_DB_KEYSPACE;
38
+
39
+ if (!token || !endpoint) {
40
+ throw new Error('Please set ASTRA_DB_TOKEN and ASTRA_DB_ENDPOINT environment variables');
41
+ }
42
+
43
+ vectorDB = new AstraVector({
44
+ token,
45
+ endpoint,
46
+ keyspace,
47
+ });
48
+ try {
49
+ const collections = await vectorDB.listIndexes();
50
+ await Promise.all(collections.map(c => vectorDB.deleteIndex(c)));
51
+ } catch (error) {
52
+ console.error('Failed to delete test collections:', error);
53
+ }
54
+
55
+ await vectorDB.createIndex(testIndexName, 4, 'cosine');
56
+ await vectorDB.createIndex(testIndexName2, 4, 'cosine');
57
+ }, 500000);
58
+
59
+ afterAll(async () => {
60
+ // Cleanup: delete test collection
61
+ try {
62
+ await vectorDB.deleteIndex(testIndexName);
63
+ } catch (error) {
64
+ console.error('Failed to delete test collection:', error);
65
+ }
66
+ try {
67
+ await vectorDB.deleteIndex(testIndexName2);
68
+ } catch (error) {
69
+ console.error('Failed to delete test collection:', error);
70
+ }
71
+ });
72
+
73
+ test('full vector database workflow', async () => {
74
+ // Verify collection was created
75
+ const indexes = await vectorDB.listIndexes();
76
+ expect(indexes).toContain(testIndexName);
77
+
78
+ // 2. Get collection stats
79
+ const initialStats = await vectorDB.describeIndex(testIndexName);
80
+ expect(initialStats).toEqual({
81
+ dimension: 4,
82
+ metric: 'cosine',
83
+ count: 0,
84
+ });
85
+
86
+ // 3. Insert vectors with metadata
87
+ const vectors = [
88
+ [1, 0, 0, 0],
89
+ [0, 1, 0, 0],
90
+ [0, 0, 1, 0],
91
+ [0, 0, 0, 1],
92
+ ];
93
+
94
+ const metadata = [{ label: 'vector1' }, { label: 'vector2' }, { label: 'vector3' }, { label: 'vector4' }];
95
+
96
+ const ids = await vectorDB.upsert(testIndexName, vectors, metadata);
97
+ expect(ids).toHaveLength(4);
98
+
99
+ // Wait for document count to update (with timeout)
100
+ const countUpdated = await waitForCondition(
101
+ async () => {
102
+ const stats = await vectorDB.describeIndex(testIndexName);
103
+ console.log('Current count:', stats.count);
104
+ return stats.count === 4;
105
+ },
106
+ 15000, // 15 second timeout
107
+ 2000, // Check every 2 seconds
108
+ );
109
+
110
+ if (!countUpdated) {
111
+ console.warn('Document count did not update to expected value within timeout');
112
+ }
113
+
114
+ // 4. Query vectors
115
+ const queryVector = [1, 0, 0, 0];
116
+ const results = await vectorDB.query(testIndexName, queryVector, 2);
117
+
118
+ expect(results).toHaveLength(2);
119
+ expect(results?.[0]?.metadata).toEqual({ label: 'vector1' });
120
+ expect(results?.[0]?.score).toBeCloseTo(1, 4);
121
+
122
+ // 5. Query with filter
123
+ const filteredResults = await vectorDB.query(testIndexName, queryVector, 2, { 'metadata.label': 'vector2' });
124
+
125
+ expect(filteredResults).toHaveLength(1);
126
+ expect(filteredResults?.[0]?.metadata).toEqual({ label: 'vector2' });
127
+
128
+ // Get final stats
129
+ const finalStats = await vectorDB.describeIndex(testIndexName);
130
+ console.log('Final stats:', finalStats);
131
+
132
+ // More lenient assertion for document count
133
+ expect(finalStats.count).toBeGreaterThan(0);
134
+ if (finalStats.count !== 4) {
135
+ console.warn(`Expected count of 4, but got ${finalStats.count}. This may be due to eventual consistency.`);
136
+ }
137
+ });
138
+
139
+ test('gets vector results back from query', async () => {
140
+ const queryVector = [1, 0, 0, 0];
141
+ const results = await vectorDB.query(testIndexName, queryVector, 2, undefined, true);
142
+
143
+ expect(results).toHaveLength(2);
144
+ expect(results?.[0]?.metadata).toEqual({ label: 'vector1' });
145
+ expect(results?.[0]?.score).toBeCloseTo(1, 4);
146
+ expect(results?.[0]?.vector).toEqual([1, 0, 0, 0]);
147
+ });
148
+
149
+ test('handles different vector dimensions', async () => {
150
+ const highDimIndexName = 'high_dim_test_' + Date.now();
151
+
152
+ try {
153
+ // Create index with higher dimensions
154
+ await vectorDB.createIndex(highDimIndexName, 1536, 'cosine');
155
+
156
+ // Insert high-dimensional vectors
157
+ const vectors = [
158
+ Array(1536)
159
+ .fill(0)
160
+ .map((_, i) => i % 2), // Alternating 0s and 1s
161
+ Array(1536)
162
+ .fill(0)
163
+ .map((_, i) => (i + 1) % 2), // Opposite pattern
164
+ ];
165
+
166
+ const metadata = [{ label: 'even' }, { label: 'odd' }];
167
+
168
+ const ids = await vectorDB.upsert(highDimIndexName, vectors, metadata);
169
+ expect(ids).toHaveLength(2);
170
+
171
+ // Wait for indexing with more generous timeout
172
+ await new Promise(resolve => setTimeout(resolve, 2000));
173
+
174
+ // Query with same pattern as first vector
175
+ const queryVector = Array(1536)
176
+ .fill(0)
177
+ .map((_, i) => i % 2);
178
+ const results = await vectorDB.query(highDimIndexName, queryVector, 2);
179
+
180
+ expect(results).toHaveLength(2);
181
+ expect(results?.[0]?.metadata).toEqual({ label: 'even' });
182
+ expect(results?.[0]?.score).toBeCloseTo(1, 4);
183
+ } finally {
184
+ // Cleanup
185
+ await vectorDB.deleteIndex(highDimIndexName);
186
+ }
187
+ });
188
+
189
+ test('handles different distance metrics', async () => {
190
+ const metrics = ['cosine', 'euclidean', 'dotproduct'] as const;
191
+
192
+ for (const metric of metrics) {
193
+ const metricIndexName = `metrictest${metric}${Date.now()}`;
194
+
195
+ try {
196
+ // Create index with different metric
197
+ await vectorDB.createIndex(metricIndexName, 4, metric);
198
+
199
+ // Insert same vectors
200
+ const vectors = [
201
+ [1, 0, 0, 0],
202
+ [0.7071, 0.7071, 0, 0], // 45-degree angle from first vector
203
+ ];
204
+
205
+ await vectorDB.upsert(metricIndexName, vectors);
206
+
207
+ // Wait for indexing with more generous timeout
208
+ await new Promise(resolve => setTimeout(resolve, 2000));
209
+
210
+ // Query
211
+ const results = await vectorDB.query(metricIndexName, [1, 0, 0, 0], 2);
212
+
213
+ expect(results).toHaveLength(2);
214
+
215
+ // Scores will differ based on metric but order should be same
216
+ expect(results?.[0]?.score).toBeGreaterThan(results?.[1]?.score!);
217
+ } finally {
218
+ // Cleanup
219
+ await vectorDB.deleteIndex(metricIndexName);
220
+ }
221
+ }
222
+ }, 500000);
223
+
224
+ describe('Filter Validation in Queries', () => {
225
+ it('rejects invalid operator values', async () => {
226
+ await expect(
227
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
228
+ tags: { $all: 'not-an-array' },
229
+ }),
230
+ ).rejects.toThrow();
231
+ });
232
+
233
+ it('validates array operator values', async () => {
234
+ await expect(
235
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
236
+ tags: { $in: null },
237
+ }),
238
+ ).rejects.toThrow();
239
+
240
+ await expect(
241
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
242
+ tags: { $all: 'not-an-array' },
243
+ }),
244
+ ).rejects.toThrow();
245
+ });
246
+
247
+ it('validates $in operators', async () => {
248
+ const invalidValues = [123, 'string', true, { key: 'value' }, {}, null];
249
+ for (const val of invalidValues) {
250
+ console.log('val:', val);
251
+ await expect(
252
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
253
+ field: { $in: val },
254
+ }),
255
+ ).rejects.toThrow();
256
+ }
257
+ });
258
+
259
+ it('validates $nin operators', async () => {
260
+ const invalidValues = [123, 'string', true, { key: 'value' }, {}, null];
261
+ for (const val of invalidValues) {
262
+ console.log('val:', val);
263
+ await expect(
264
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
265
+ field: { $nin: val },
266
+ }),
267
+ ).rejects.toThrow();
268
+ }
269
+ });
270
+
271
+ it('validates $all operators', async () => {
272
+ const invalidValues = [123, 'string', true, { key: 'value' }, {}, [], null];
273
+ for (const val of invalidValues) {
274
+ await expect(
275
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
276
+ field: { $all: val },
277
+ }),
278
+ ).rejects.toThrow();
279
+ }
280
+ });
281
+
282
+ it('validates element operators', async () => {
283
+ const invalidValues = [123, 'string', { key: 'value' }, {}, [], null];
284
+ for (const val of invalidValues) {
285
+ await expect(
286
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
287
+ field: { $exists: val },
288
+ }),
289
+ ).rejects.toThrow();
290
+ }
291
+ });
292
+
293
+ it('validates comparison operators', async () => {
294
+ // Numeric comparisons require numbers
295
+ const numOps = ['$gt', '$gte', '$lt', '$lte'];
296
+ const invalidNumericValues = [[], {}, null];
297
+ for (const op of numOps) {
298
+ for (const val of invalidNumericValues) {
299
+ console.log('op:', op);
300
+ console.log('val:', val);
301
+ await expect(
302
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
303
+ field: { [op]: val },
304
+ }),
305
+ ).rejects.toThrow();
306
+ }
307
+ }
308
+ });
309
+
310
+ it('validates multiple invalid values', async () => {
311
+ await expect(
312
+ vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
313
+ field1: { $in: 'not-array' },
314
+ field2: { $exists: 'not-boolean' },
315
+ field3: { $gt: 'not-number' },
316
+ }),
317
+ ).rejects.toThrow();
318
+ });
319
+ });
320
+
321
+ describe('Metadata Filter Tests', () => {
322
+ // Set up test vectors and metadata
323
+ beforeAll(async () => {
324
+ const vectors = [
325
+ [1, 0, 0, 0], // Electronics
326
+ [0, 1, 0, 0], // Books
327
+ [0, 0, 1, 0], // Electronics
328
+ [0, 0, 0, 1], // Books
329
+ ];
330
+
331
+ const metadata = [
332
+ {
333
+ category: 'electronics',
334
+ price: 1000,
335
+ rating: 4.8,
336
+ tags: ['premium', 'new'],
337
+ inStock: true,
338
+ specs: {
339
+ color: 'black',
340
+ weight: 2.5,
341
+ },
342
+ },
343
+ {
344
+ category: 'books',
345
+ price: 25,
346
+ rating: 4.2,
347
+ tags: ['bestseller'],
348
+ inStock: true,
349
+ author: {
350
+ name: 'John Doe',
351
+ country: 'USA',
352
+ },
353
+ },
354
+ {
355
+ category: 'electronics',
356
+ price: 500,
357
+ rating: 4.5,
358
+ tags: ['refurbished', 'premium'],
359
+ inStock: false,
360
+ specs: {
361
+ color: 'silver',
362
+ weight: 1.8,
363
+ },
364
+ },
365
+ {
366
+ category: 'books',
367
+ price: 15,
368
+ rating: 4.9,
369
+ tags: ['bestseller', 'new'],
370
+ inStock: true,
371
+ author: {
372
+ name: 'Jane Smith',
373
+ country: 'UK',
374
+ },
375
+ },
376
+ ];
377
+
378
+ await vectorDB.upsert(testIndexName2, vectors, metadata);
379
+ // Wait for indexing
380
+ await new Promise(resolve => setTimeout(resolve, 2000));
381
+ });
382
+
383
+ describe('Basic Comparison Operators', () => {
384
+ it('filters with $eq operator', async () => {
385
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
386
+ 'metadata.category': { $eq: 'electronics' },
387
+ });
388
+ expect(results.length).toBe(2);
389
+ results.forEach(result => {
390
+ expect(result.metadata?.category).toBe('electronics');
391
+ });
392
+ });
393
+
394
+ it('filters with $gt operator', async () => {
395
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
396
+ 'metadata.price': { $gt: 500 },
397
+ });
398
+ expect(results.length).toBe(1);
399
+ results.forEach(result => {
400
+ expect(Number(result.metadata?.price)).toBeGreaterThan(500);
401
+ });
402
+ });
403
+
404
+ it('filters with $gte, $lt, $lte operators', async () => {
405
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
406
+ 'metadata.price': { $gte: 25, $lte: 500 },
407
+ });
408
+ expect(results.length).toBe(2);
409
+ results.forEach(result => {
410
+ expect(Number(result.metadata?.price)).toBeLessThanOrEqual(500);
411
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(25);
412
+ });
413
+ });
414
+
415
+ it('filters with $ne operator', async () => {
416
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
417
+ 'metadata.category': { $ne: 'books' },
418
+ });
419
+ expect(results.length).toBe(2);
420
+ results.forEach(result => {
421
+ expect(result.metadata?.category).not.toBe('books');
422
+ });
423
+ });
424
+ });
425
+
426
+ describe('Null/Undefined/Empty FIlters', () => {
427
+ it('should handle undefined filter', async () => {
428
+ const results1 = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, undefined);
429
+ const results2 = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10);
430
+ expect(results1).toEqual(results2);
431
+ expect(results1.length).toBeGreaterThan(0);
432
+ });
433
+
434
+ it('should handle empty object filter', async () => {
435
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {});
436
+ const results2 = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10);
437
+ expect(results).toEqual(results2);
438
+ expect(results.length).toBeGreaterThan(0);
439
+ });
440
+
441
+ it('should handle null filter', async () => {
442
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, null as any);
443
+ const results2 = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10);
444
+ expect(results).toEqual(results2);
445
+ expect(results.length).toBeGreaterThan(0);
446
+ });
447
+ });
448
+
449
+ describe('Array Operators', () => {
450
+ it('filters with $in operator', async () => {
451
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
452
+ 'metadata.tags': { $in: ['premium'] },
453
+ });
454
+ expect(results.length).toBe(2);
455
+ results.forEach(result => {
456
+ expect(result.metadata?.tags).toContain('premium');
457
+ });
458
+ });
459
+
460
+ it('filters with $nin operator', async () => {
461
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
462
+ 'metadata.tags': { $nin: ['bestseller'] },
463
+ });
464
+ expect(results.length).toBe(2);
465
+ results.forEach(result => {
466
+ expect(result.metadata?.tags).not.toContain('bestseller');
467
+ });
468
+ });
469
+
470
+ it('filters with $all operator', async () => {
471
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
472
+ 'metadata.tags': { $all: ['premium', 'new'] },
473
+ });
474
+ expect(results.length).toBe(1);
475
+ results.forEach(result => {
476
+ expect(result.metadata?.tags).toContain('premium');
477
+ expect(result.metadata?.tags).toContain('new');
478
+ });
479
+ });
480
+ });
481
+
482
+ describe('Logical Operators', () => {
483
+ it('filters with $and operator', async () => {
484
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
485
+ $and: [{ 'metadata.category': 'electronics' }, { 'metadata.price': { $gt: 500 } }],
486
+ });
487
+ expect(results.length).toBe(1);
488
+ expect(results[0]?.metadata?.category).toBe('electronics');
489
+ expect(Number(results[0]?.metadata?.price)).toBeGreaterThan(500);
490
+ });
491
+
492
+ it('filters with $or operator', async () => {
493
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
494
+ $or: [{ 'metadata.price': { $gt: 900 } }, { 'metadata.rating': { $gt: 4.8 } }],
495
+ });
496
+ expect(results.length).toBe(2);
497
+ results.forEach(result => {
498
+ expect(Number(result.metadata?.price) > 900 || Number(result.metadata?.rating) > 4.8).toBe(true);
499
+ });
500
+ });
501
+
502
+ it('filters with direct field comparison', async () => {
503
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
504
+ $not: { 'metadata.category': 'electronics' }, // Simple field equality
505
+ });
506
+ expect(results.length).toBe(2);
507
+ results.forEach(result => {
508
+ expect(result.metadata?.category).not.toBe('electronics');
509
+ });
510
+ });
511
+
512
+ it('filters with $eq operator', async () => {
513
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
514
+ $not: { 'metadata.category': { $eq: 'electronics' } },
515
+ });
516
+ expect(results.length).toBe(2);
517
+ results.forEach(result => {
518
+ expect(result.metadata?.category).not.toBe('electronics');
519
+ });
520
+ });
521
+
522
+ it('filters with multiple conditions on same field using implicit $and', async () => {
523
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
524
+ 'metadata.category': 'electronics',
525
+ 'metadata.price': 1000,
526
+ });
527
+ expect(results.length).toBe(1);
528
+ });
529
+
530
+ it('filters with multiple fields', async () => {
531
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
532
+ $not: {
533
+ 'metadata.category': 'electronics',
534
+ 'metadata.price': 100,
535
+ },
536
+ });
537
+ expect(results.length).toBeGreaterThan(0);
538
+ results.forEach(result => {
539
+ expect(result.metadata?.category === 'electronics' && result.metadata?.price === 100).toBe(false);
540
+ });
541
+ });
542
+
543
+ it('uses $not within $or', async () => {
544
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
545
+ $or: [{ $not: { 'metadata.category': 'electronics' } }, { 'metadata.price': { $gt: 100 } }],
546
+ });
547
+ expect(results.length).toBeGreaterThan(0);
548
+ });
549
+
550
+ // Test $not with $exists
551
+ it('filters with $exists', async () => {
552
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
553
+ $not: { 'metadata.optional_field': { $exists: true } },
554
+ });
555
+ expect(results.length).toBeGreaterThan(0);
556
+ });
557
+
558
+ it('filters with nested logical operators', async () => {
559
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
560
+ $and: [
561
+ { 'metadata.category': 'electronics' },
562
+ {
563
+ $or: [{ 'metadata.price': { $gt: 900 } }, { 'metadata.tags': { $all: ['refurbished'] } }],
564
+ },
565
+ ],
566
+ });
567
+ expect(results.length).toBe(2);
568
+ results.forEach(result => {
569
+ expect(result.metadata?.category).toBe('electronics');
570
+ expect(Number(result.metadata?.price) > 900 || result.metadata?.tags?.includes('refurbished')).toBe(true);
571
+ });
572
+ });
573
+ });
574
+
575
+ describe('Nested Field Queries', () => {
576
+ it('filters on nested object fields', async () => {
577
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
578
+ 'metadata.specs.color': 'black',
579
+ });
580
+ expect(results.length).toBe(1);
581
+ expect(results[0]?.metadata?.specs?.color).toBe('black');
582
+ });
583
+
584
+ it('combines nested field queries with logical operators', async () => {
585
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
586
+ $or: [{ 'metadata.specs.weight': { $lt: 2.0 } }, { 'metadata.author.country': 'UK' }],
587
+ });
588
+ expect(results.length).toBe(2);
589
+ results.forEach(result => {
590
+ expect(result.metadata?.specs?.weight < 2.0 || result.metadata?.author?.country === 'UK').toBe(true);
591
+ });
592
+ });
593
+ });
594
+
595
+ describe('Complex Filter Combinations', () => {
596
+ it('combines multiple operators and conditions', async () => {
597
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
598
+ $and: [
599
+ { 'metadata.price': { $gt: 20 } },
600
+ { 'metadata.inStock': true },
601
+ {
602
+ $or: [{ 'metadata.tags': { $in: ['premium'] } }, { 'metadata.rating': { $gt: 4.5 } }],
603
+ },
604
+ ],
605
+ });
606
+ expect(results.length).toBeGreaterThan(0);
607
+ results.forEach(result => {
608
+ expect(Number(result.metadata?.price)).toBeGreaterThan(20);
609
+ expect(result.metadata?.inStock).toBe(true);
610
+ expect(result.metadata?.tags?.includes('premium') || Number(result.metadata?.rating) > 4.5).toBe(true);
611
+ });
612
+ });
613
+
614
+ it('handles complex nested conditions', async () => {
615
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
616
+ $or: [
617
+ {
618
+ $and: [
619
+ { 'metadata.category': 'electronics' },
620
+ { 'metadata.specs.weight': { $lt: 2.0 } },
621
+ { 'metadata.tags': { $in: ['premium'] } },
622
+ ],
623
+ },
624
+ {
625
+ $and: [
626
+ { 'metadata.category': 'books' },
627
+ { 'metadata.price': { $lt: 20 } },
628
+ { 'metadata.author.country': 'UK' },
629
+ ],
630
+ },
631
+ ],
632
+ });
633
+ expect(results.length).toBeGreaterThan(0);
634
+ results.forEach(result => {
635
+ if (result.metadata?.category === 'electronics') {
636
+ expect(result.metadata?.specs?.weight).toBeLessThan(2.0);
637
+ expect(result.metadata?.tags).toContain('premium');
638
+ } else {
639
+ expect(Number(result.metadata?.price)).toBeLessThan(20);
640
+ expect(result.metadata?.author?.country).toBe('UK');
641
+ }
642
+ });
643
+ });
644
+ });
645
+
646
+ describe('Field Existence and Null Checks', () => {
647
+ beforeAll(async () => {
648
+ // Add some vectors with special metadata cases
649
+ const vectors = [
650
+ [0.5, 0.5, 0.5, 0.5],
651
+ [0.3, 0.3, 0.3, 0.3],
652
+ ];
653
+
654
+ const metadata = [
655
+ {
656
+ category: 'special',
657
+ optionalField: null,
658
+ emptyArray: [],
659
+ nested: {
660
+ existingField: 'value',
661
+ nullField: null,
662
+ },
663
+ },
664
+ {
665
+ category: 'special',
666
+ // optionalField intentionally missing
667
+ emptyArray: ['single'],
668
+ nested: {
669
+ // existingField intentionally missing
670
+ otherField: 'value',
671
+ },
672
+ },
673
+ ];
674
+
675
+ await vectorDB.upsert(testIndexName2, vectors, metadata);
676
+ await new Promise(resolve => setTimeout(resolve, 2000));
677
+ });
678
+
679
+ it('filters based on field existence', async () => {
680
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
681
+ 'metadata.optionalField': { $exists: true },
682
+ });
683
+ expect(results.length).toBe(1);
684
+ expect('optionalField' in results[0]!.metadata!).toBe(true);
685
+ });
686
+
687
+ it('filters for null values', async () => {
688
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
689
+ 'metadata.nested.nullField': null,
690
+ });
691
+ expect(results.length).toBe(1);
692
+ expect(results[0]!.metadata!.nested.nullField).toBeNull();
693
+ });
694
+
695
+ it('combines existence checks with other operators', async () => {
696
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
697
+ $and: [{ 'metadata.category': 'special' }, { 'metadata.optionalField': { $exists: false } }],
698
+ });
699
+ expect(results.length).toBe(1);
700
+ expect(results[0]!.metadata!.category).toBe('special');
701
+ expect('optionalField' in results[0]!.metadata!).toBe(false);
702
+ });
703
+
704
+ it('handles empty array edge cases', async () => {
705
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
706
+ 'metadata.emptyArray': { $size: 0 },
707
+ });
708
+ expect(results.length).toBe(1);
709
+ expect(results[0]!.metadata!.emptyArray).toHaveLength(0);
710
+ });
711
+ });
712
+
713
+ describe('Date and Numeric Edge Cases', () => {
714
+ beforeAll(async () => {
715
+ const vectors = [
716
+ [0.1, 0.1, 0.1, 0.1],
717
+ [0.2, 0.2, 0.2, 0.2],
718
+ ];
719
+
720
+ const metadata = [
721
+ {
722
+ numericFields: {
723
+ zero: 0,
724
+ negativeZero: -0,
725
+ infinity: Infinity,
726
+ negativeInfinity: -Infinity,
727
+ decimal: 0.1,
728
+ negativeDecimal: -0.1,
729
+ },
730
+ dateFields: {
731
+ current: new Date().toISOString(),
732
+ epoch: new Date(0).toISOString(),
733
+ future: new Date('2100-01-01').toISOString(),
734
+ },
735
+ },
736
+ {
737
+ numericFields: {
738
+ maxInt: Number.MAX_SAFE_INTEGER,
739
+ minInt: Number.MIN_SAFE_INTEGER,
740
+ maxFloat: Number.MAX_VALUE,
741
+ minFloat: Number.MIN_VALUE,
742
+ },
743
+ dateFields: {
744
+ past: new Date('1900-01-01').toISOString(),
745
+ current: new Date().toISOString(),
746
+ },
747
+ },
748
+ ];
749
+
750
+ await vectorDB.upsert(testIndexName2, vectors, metadata);
751
+ await new Promise(resolve => setTimeout(resolve, 2000));
752
+ });
753
+
754
+ it('handles special numeric values', async () => {
755
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
756
+ $or: [{ 'metadata.numericFields.zero': 0 }, { 'metadata.numericFields.negativeZero': 0 }],
757
+ });
758
+ expect(results.length).toBeGreaterThan(0);
759
+ results.forEach(result => {
760
+ const value = result.metadata?.numericFields?.zero ?? result.metadata?.numericFields?.negativeZero;
761
+ expect(value).toBe(0);
762
+ });
763
+ });
764
+
765
+ it('compares dates correctly', async () => {
766
+ const now = new Date().toISOString();
767
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
768
+ $and: [
769
+ { 'metadata.dateFields.current': { $lte: now } },
770
+ { 'metadata.dateFields.current': { $gt: new Date(0).toISOString() } },
771
+ ],
772
+ });
773
+ expect(results.length).toBeGreaterThan(0);
774
+ });
775
+
776
+ it('handles extreme numeric values', async () => {
777
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
778
+ $or: [
779
+ { 'metadata.numericFields.maxInt': { $gte: Number.MAX_SAFE_INTEGER } },
780
+ { 'metadata.numericFields.minInt': { $lte: Number.MIN_SAFE_INTEGER } },
781
+ ],
782
+ });
783
+ expect(results.length).toBe(1);
784
+ });
785
+ });
786
+
787
+ describe('Advanced Array Operations', () => {
788
+ beforeAll(async () => {
789
+ const vectors = [
790
+ [0.7, 0.7, 0.7, 0.7],
791
+ [0.8, 0.8, 0.8, 0.8],
792
+ [0.9, 0.9, 0.9, 0.9],
793
+ ];
794
+
795
+ const metadata = [
796
+ {
797
+ arrays: {
798
+ empty: [],
799
+ single: ['one'],
800
+ multiple: ['one', 'two', 'three'],
801
+ nested: [['inner']],
802
+ },
803
+ },
804
+ {
805
+ arrays: {
806
+ empty: [],
807
+ single: ['two'],
808
+ multiple: ['two', 'three'],
809
+ nested: [['inner'], ['outer']],
810
+ },
811
+ },
812
+ {
813
+ arrays: {
814
+ single: ['three'],
815
+ multiple: ['three', 'four', 'five'],
816
+ nested: [],
817
+ },
818
+ },
819
+ ];
820
+
821
+ await vectorDB.upsert(testIndexName2, vectors, metadata);
822
+ await new Promise(resolve => setTimeout(resolve, 2000));
823
+ });
824
+
825
+ it('handles $in with empty array input', async () => {
826
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
827
+ 'metadata.arrays.single': { $in: [] },
828
+ });
829
+ expect(results.length).toBe(0);
830
+ });
831
+
832
+ it('combines $size with $exists for array fields', async () => {
833
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
834
+ $and: [{ 'metadata.arrays.empty': { $exists: true } }, { 'metadata.arrays.empty': { $size: 0 } }],
835
+ });
836
+ expect(results.length).toBe(2);
837
+ results.forEach(result => {
838
+ expect(result.metadata?.arrays?.empty).toBeDefined();
839
+ expect(result.metadata?.arrays?.empty).toHaveLength(0);
840
+ });
841
+ });
842
+
843
+ it('filters arrays by exact size matching', async () => {
844
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0, 0], 10, {
845
+ $and: [{ 'metadata.arrays.multiple': { $size: 3 } }, { 'metadata.arrays.multiple': { $in: ['two'] } }],
846
+ });
847
+ expect(results.length).toBe(1);
848
+ expect(results[0]?.metadata?.arrays?.multiple).toContain('two');
849
+ expect(results[0]?.metadata?.arrays?.multiple).toHaveLength(3);
850
+ });
851
+ });
852
+ });
853
+ });