@mastra/chroma 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,889 @@
1
+ import { QueryResult, IndexStats } from '@mastra/core/vector';
2
+ import { describe, expect, beforeEach, afterEach, it, beforeAll, afterAll } from 'vitest';
3
+
4
+ import { ChromaVector } from './';
5
+
6
+ describe('ChromaVector Integration Tests', () => {
7
+ let vectorDB = new ChromaVector({
8
+ path: 'http://localhost:8000',
9
+ });
10
+
11
+ const testIndexName = 'test-index';
12
+ const testIndexName2 = 'test-index-2';
13
+ const dimension = 3;
14
+
15
+ beforeEach(async () => {
16
+ // Clean up any existing test index
17
+ try {
18
+ await vectorDB.deleteIndex(testIndexName);
19
+ } catch (error) {
20
+ // Ignore errors if index doesn't exist
21
+ }
22
+ await vectorDB.createIndex(testIndexName, dimension);
23
+ }, 5000);
24
+
25
+ afterEach(async () => {
26
+ // Cleanup after tests
27
+ try {
28
+ await vectorDB.deleteIndex(testIndexName);
29
+ } catch (error) {
30
+ // Ignore cleanup errors
31
+ }
32
+ }, 5000);
33
+
34
+ describe('Index Management', () => {
35
+ it('should create and list indexes', async () => {
36
+ const indexes = await vectorDB.listIndexes();
37
+ expect(indexes).toContain(testIndexName);
38
+ });
39
+
40
+ it('should describe index correctly', async () => {
41
+ const stats: IndexStats = await vectorDB.describeIndex(testIndexName);
42
+ expect(stats.dimension).toBe(dimension);
43
+ expect(stats.count).toBe(0);
44
+ expect(stats.metric).toBe('cosine');
45
+ });
46
+
47
+ it('should delete index', async () => {
48
+ await vectorDB.deleteIndex(testIndexName);
49
+ const indexes = await vectorDB.listIndexes();
50
+ expect(indexes).not.toContain(testIndexName);
51
+ });
52
+
53
+ it('should create index with different metrics', async () => {
54
+ const metricsToTest: Array<'cosine' | 'euclidean' | 'dotproduct'> = ['euclidean', 'dotproduct'];
55
+
56
+ for (const metric of metricsToTest) {
57
+ const testIndex = `test-index-${metric}`;
58
+ await vectorDB.createIndex(testIndex, dimension, metric);
59
+
60
+ const stats = await vectorDB.describeIndex(testIndex);
61
+ expect(stats.metric).toBe(metric);
62
+
63
+ await vectorDB.deleteIndex(testIndex);
64
+ }
65
+ });
66
+ });
67
+
68
+ describe('Basic Vector Operations', () => {
69
+ const testVectors = [
70
+ [1.0, 0.0, 0.0],
71
+ [0.0, 1.0, 0.0],
72
+ [0.0, 0.0, 1.0],
73
+ ];
74
+ const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
75
+ const testIds = ['vec1', 'vec2', 'vec3'];
76
+
77
+ it('should upsert vectors with generated ids', async () => {
78
+ const ids = await vectorDB.upsert(testIndexName, testVectors);
79
+ expect(ids).toHaveLength(testVectors.length);
80
+ ids.forEach(id => expect(typeof id).toBe('string'));
81
+
82
+ const stats = await vectorDB.describeIndex(testIndexName);
83
+ expect(stats.count).toBe(testVectors.length);
84
+ });
85
+
86
+ it('should upsert vectors with provided ids and metadata', async () => {
87
+ await vectorDB.upsert(testIndexName, testVectors, testMetadata, testIds);
88
+
89
+ const stats = await vectorDB.describeIndex(testIndexName);
90
+ expect(stats.count).toBe(testVectors.length);
91
+
92
+ // Query each vector to verify metadata
93
+ for (let i = 0; i < testVectors.length; i++) {
94
+ const results = await vectorDB.query(testIndexName, testVectors?.[i]!, 1);
95
+ expect(results?.[0]?.id).toBe(testIds[i]);
96
+ expect(results?.[0]?.metadata).toEqual(testMetadata[i]);
97
+ }
98
+ });
99
+
100
+ it('should update existing vectors', async () => {
101
+ // Initial upsert
102
+ await vectorDB.upsert(testIndexName, testVectors, testMetadata, testIds);
103
+
104
+ // Update first vector
105
+ const updatedVector = [[0.5, 0.5, 0.0]];
106
+ const updatedMetadata = [{ label: 'updated-x-axis' }];
107
+ await vectorDB.upsert(testIndexName, updatedVector, updatedMetadata, [testIds?.[0]!]);
108
+
109
+ // Verify update
110
+ const results = await vectorDB.query(testIndexName, updatedVector?.[0]!, 1);
111
+ expect(results?.[0]?.id).toBe(testIds[0]);
112
+ expect(results?.[0]?.metadata).toEqual(updatedMetadata[0]);
113
+ });
114
+ });
115
+
116
+ describe('Query Operations', () => {
117
+ const testVectors = [
118
+ [1.0, 0.0, 0.0],
119
+ [0.0, 1.0, 0.0],
120
+ [0.0, 0.0, 1.0],
121
+ ];
122
+ const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
123
+ const testIds = ['vec1', 'vec2', 'vec3'];
124
+
125
+ beforeEach(async () => {
126
+ await vectorDB.upsert(testIndexName, testVectors, testMetadata, testIds);
127
+ });
128
+
129
+ describe('Basic Queries', () => {
130
+ it('should perform vector search with topK', async () => {
131
+ const queryVector = [1.0, 0.1, 0.1];
132
+ const topK = 2;
133
+
134
+ const results: QueryResult[] = await vectorDB.query(testIndexName, queryVector, topK);
135
+
136
+ expect(results).toHaveLength(topK);
137
+ expect(results?.[0]?.id).toBe(testIds[0]); // Should match x-axis vector most closely
138
+ });
139
+ });
140
+
141
+ describe('Filter Queries', () => {
142
+ it('should filter query results', async () => {
143
+ const queryVector = [1.0, 1.0, 1.0];
144
+ const filter = { label: 'x-axis' };
145
+
146
+ const results = await vectorDB.query(testIndexName, queryVector, 3, filter);
147
+
148
+ expect(results).toHaveLength(1);
149
+ expect(results?.[0]?.metadata?.label).toBe('x-axis');
150
+ });
151
+ });
152
+
153
+ describe('Vector Inclusion', () => {
154
+ it('should include vector in query results', async () => {
155
+ const queryVector = [1.0, 0.1, 0.1];
156
+ const topK = 1;
157
+
158
+ const results = await vectorDB.query(testIndexName, queryVector, topK, undefined, true);
159
+
160
+ expect(results).toHaveLength(topK);
161
+ expect(results?.[0]?.vector).toEqual(testVectors[0]);
162
+ });
163
+ });
164
+ });
165
+
166
+ describe('Error Handling', () => {
167
+ it('should handle non-existent index queries', async () => {
168
+ await expect(vectorDB.query('non-existent-index-yu', [1, 2, 3])).rejects.toThrow();
169
+ });
170
+
171
+ it('should handle invalid dimension vectors', async () => {
172
+ const invalidVector = [1, 2, 3, 4]; // 4D vector for 3D index
173
+ await expect(vectorDB.upsert(testIndexName, [invalidVector])).rejects.toThrow();
174
+ });
175
+
176
+ it('should handle mismatched metadata and vectors length', async () => {
177
+ const vectors = [[1, 2, 3]];
178
+ const metadata = [{}, {}]; // More metadata than vectors
179
+ await expect(vectorDB.upsert(testIndexName, vectors, metadata)).rejects.toThrow();
180
+ });
181
+ });
182
+
183
+ describe('Filter Validation in Queries', () => {
184
+ it('rejects queries with null values', async () => {
185
+ await expect(
186
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
187
+ field: null,
188
+ }),
189
+ ).rejects.toThrow();
190
+
191
+ await expect(
192
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
193
+ other: { $eq: null },
194
+ }),
195
+ ).rejects.toThrow();
196
+ });
197
+
198
+ it('validates array operator values', async () => {
199
+ await expect(
200
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
201
+ tags: { $in: null },
202
+ }),
203
+ ).rejects.toThrow();
204
+ });
205
+
206
+ it('validates numeric values for comparison operators', async () => {
207
+ await expect(
208
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
209
+ price: { $gt: 'not-a-number' },
210
+ }),
211
+ ).rejects.toThrow();
212
+ });
213
+
214
+ it('validates value types', async () => {
215
+ await expect(
216
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
217
+ date: { $gt: 'not-a-date' },
218
+ }),
219
+ ).rejects.toThrow();
220
+
221
+ await expect(
222
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
223
+ number: { $lt: 'not-a-number' },
224
+ }),
225
+ ).rejects.toThrow();
226
+ });
227
+
228
+ it('validates array operators', async () => {
229
+ const invalidValues = [123, 'string', true, { key: 'value' }, null, undefined];
230
+ for (const op of ['$in', '$nin']) {
231
+ for (const val of invalidValues) {
232
+ await expect(
233
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
234
+ field: { [op]: val },
235
+ }),
236
+ ).rejects.toThrow();
237
+ }
238
+ }
239
+ });
240
+
241
+ it('rejects invalid array operator values', async () => {
242
+ // Test non-undefined values
243
+ const invalidValues = [123, 'string', true, { key: 'value' }, null];
244
+ for (const op of ['$in', '$nin']) {
245
+ for (const val of invalidValues) {
246
+ await expect(
247
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
248
+ field: { [op]: val },
249
+ }),
250
+ ).rejects.toThrow();
251
+ }
252
+ }
253
+ });
254
+
255
+ it('validates comparison operators', async () => {
256
+ // Basic equality can accept any non-undefined value
257
+ for (const op of ['$eq', '$ne']) {
258
+ await expect(
259
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
260
+ field: { [op]: undefined },
261
+ }),
262
+ ).rejects.toThrow();
263
+ }
264
+
265
+ // Numeric comparisons require numbers
266
+ const numOps = ['$gt', '$gte', '$lt', '$lte'];
267
+ const invalidNumericValues = ['not-a-number', true, [], {}, null, undefined];
268
+ for (const op of numOps) {
269
+ for (const val of invalidNumericValues) {
270
+ await expect(
271
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
272
+ field: { [op]: val },
273
+ }),
274
+ ).rejects.toThrow();
275
+ }
276
+ }
277
+ });
278
+
279
+ it('validates multiple invalid values', async () => {
280
+ await expect(
281
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
282
+ field1: { $in: 'not-array' },
283
+ field2: { $exists: 'not-boolean' },
284
+ field3: { $gt: 'not-number' },
285
+ }),
286
+ ).rejects.toThrow();
287
+ });
288
+
289
+ it('handles empty object filters', async () => {
290
+ // Test empty object at top level
291
+ await expect(vectorDB.query(testIndexName, [1, 0, 0], 10, { field: { $eq: {} } })).rejects.toThrow();
292
+ });
293
+
294
+ it('handles empty/undefined filters by returning all results', async () => {
295
+ const noFilterCases = [{ field: {} }, { field: undefined }, { field: { $in: undefined } }];
296
+
297
+ for (const filter of noFilterCases) {
298
+ await expect(vectorDB.query(testIndexName, [1, 0, 0], 10, filter)).rejects.toThrow();
299
+ }
300
+ });
301
+ it('handles empty object filters', async () => {
302
+ // Test empty object at top level
303
+ await expect(vectorDB.query(testIndexName, [1, 0, 0], 10, {})).rejects.toThrow();
304
+ });
305
+ });
306
+
307
+ describe('Metadata Filter Tests', () => {
308
+ // Set up test vectors and metadata
309
+ beforeAll(async () => {
310
+ try {
311
+ await vectorDB.deleteIndex(testIndexName2);
312
+ } catch (error) {
313
+ // Ignore errors if index doesn't exist
314
+ }
315
+ await vectorDB.createIndex(testIndexName2, dimension);
316
+
317
+ const vectors = [
318
+ [1, 0, 0], // Electronics
319
+ [0, 1, 0], // Books
320
+ [0, 0, 1], // Electronics
321
+ [0, 0, 0.1], // Books
322
+ ];
323
+
324
+ const metadata = [
325
+ {
326
+ category: 'electronics',
327
+ price: 1000,
328
+ rating: 4.8,
329
+ inStock: true,
330
+ },
331
+ {
332
+ category: 'books',
333
+ price: 25,
334
+ rating: 4.2,
335
+ inStock: true,
336
+ },
337
+ {
338
+ category: 'electronics',
339
+ price: 500,
340
+ rating: 4.5,
341
+ inStock: false,
342
+ },
343
+ {
344
+ category: 'books',
345
+ price: 15,
346
+ rating: 4.9,
347
+ inStock: true,
348
+ },
349
+ ];
350
+
351
+ await vectorDB.upsert(testIndexName2, vectors, metadata);
352
+ // Wait for indexing
353
+ await new Promise(resolve => setTimeout(resolve, 2000));
354
+ });
355
+
356
+ afterAll(async () => {
357
+ // Cleanup after tests
358
+ try {
359
+ await vectorDB.deleteIndex(testIndexName2);
360
+ } catch (error) {
361
+ // Ignore cleanup errors
362
+ }
363
+ });
364
+
365
+ describe('Basic Comparison Operators', () => {
366
+ it('filters with $eq operator', async () => {
367
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
368
+ category: { $eq: 'electronics' },
369
+ });
370
+ expect(results.length).toBe(2);
371
+ results.forEach(result => {
372
+ expect(result.metadata?.category).toBe('electronics');
373
+ });
374
+ });
375
+
376
+ it('filters with implicit $eq', async () => {
377
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
378
+ category: 'electronics', // implicit $eq
379
+ });
380
+ expect(results.length).toBe(2);
381
+ results.forEach(result => {
382
+ expect(result.metadata?.category).toBe('electronics');
383
+ });
384
+ });
385
+ it('filters with $gt operator', async () => {
386
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
387
+ price: { $gt: 500 },
388
+ });
389
+ expect(results.length).toBe(1);
390
+ results.forEach(result => {
391
+ expect(Number(result.metadata?.price)).toBeGreaterThan(500);
392
+ });
393
+ });
394
+
395
+ it('filters with $gte operator', async () => {
396
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
397
+ price: { $gte: 500 },
398
+ });
399
+ expect(results.length).toBe(2);
400
+ results.forEach(result => {
401
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
402
+ });
403
+ });
404
+
405
+ it('filters with $lt operator', async () => {
406
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
407
+ price: { $lt: 100 },
408
+ });
409
+ expect(results.length).toBe(2);
410
+ results.forEach(result => {
411
+ expect(Number(result.metadata?.price)).toBeLessThan(100);
412
+ });
413
+ });
414
+
415
+ it('filters with $lte operator', async () => {
416
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
417
+ price: { $lte: 500 },
418
+ });
419
+ expect(results.length).toBeGreaterThan(0);
420
+ results.forEach(result => {
421
+ expect(Number(result.metadata?.price)).toBeLessThanOrEqual(500);
422
+ });
423
+ });
424
+
425
+ it('filters with $gte, $lt, $lte operators', async () => {
426
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
427
+ price: { $gte: 25, $lte: 500 },
428
+ });
429
+ expect(results.length).toBe(2);
430
+ results.forEach(result => {
431
+ expect(Number(result.metadata?.price)).toBeLessThanOrEqual(500);
432
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(25);
433
+ });
434
+ });
435
+
436
+ it('filters with $ne operator', async () => {
437
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
438
+ category: { $ne: 'electronics' },
439
+ });
440
+ expect(results.length).toBe(2);
441
+ results.forEach(result => {
442
+ expect(result.metadata?.category).not.toBe('electronics');
443
+ });
444
+ });
445
+
446
+ it('filters with boolean values', async () => {
447
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
448
+ inStock: true, // test both implicit
449
+ });
450
+ expect(results.length).toBe(3);
451
+ results.forEach(result => {
452
+ expect(result.metadata?.inStock).toBe(true);
453
+ });
454
+ });
455
+
456
+ it('filters with multiple fields', async () => {
457
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
458
+ category: 'electronics',
459
+ price: 1000,
460
+ });
461
+ expect(results.length).toBeGreaterThan(0);
462
+ results.forEach(result => {
463
+ expect(result.metadata?.category === 'electronics' && result.metadata?.price === 1000).toBe(true);
464
+ });
465
+ });
466
+ });
467
+
468
+ describe('Array Operators', () => {
469
+ it('filters with $in operator', async () => {
470
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
471
+ category: { $in: ['electronics', 'books'] },
472
+ });
473
+ expect(results.length).toBeGreaterThan(0);
474
+ results.forEach(result => {
475
+ expect(['electronics', 'books']).toContain(result.metadata?.category);
476
+ });
477
+ });
478
+
479
+ it('should filter with $in operator for numbers', async () => {
480
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
481
+ price: { $in: [50, 75, 1000] },
482
+ });
483
+ expect(results.length).toBeGreaterThan(0);
484
+ results.forEach(result => {
485
+ expect([50, 75, 1000]).toContain(result.metadata?.price);
486
+ });
487
+ });
488
+
489
+ it('filters with $in operator for booleans', async () => {
490
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
491
+ inStock: { $in: [true] },
492
+ });
493
+ expect(results.length).toBeGreaterThan(0);
494
+ results.forEach(result => {
495
+ expect(result.metadata?.inStock).toBe(true);
496
+ });
497
+ });
498
+ });
499
+
500
+ describe('Logical Operators', () => {
501
+ it('filters with $and operator', async () => {
502
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
503
+ $and: [{ category: 'electronics' }, { price: { $gt: 500 } }],
504
+ });
505
+ expect(results.length).toBe(1);
506
+ expect(results[0]?.metadata?.category).toBe('electronics');
507
+ expect(Number(results[0]?.metadata?.price)).toBeGreaterThan(500);
508
+ });
509
+
510
+ it('should filter with $and operator', async () => {
511
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
512
+ $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { inStock: true }],
513
+ });
514
+ expect(results.length).toBeGreaterThan(0);
515
+ results.forEach(result => {
516
+ expect(result.metadata?.category).toBe('electronics');
517
+ expect(Number(result.metadata?.price)).toBeGreaterThan(700);
518
+ expect(result.metadata?.inStock).toBe(true);
519
+ });
520
+ });
521
+
522
+ it('filters with $or operator', async () => {
523
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
524
+ $or: [{ price: { $gt: 900 } }, { rating: { $gt: 4.8 } }],
525
+ });
526
+ expect(results.length).toBe(2);
527
+ results.forEach(result => {
528
+ expect(Number(result.metadata?.price) > 900 || Number(result.metadata?.rating) > 4.8).toBe(true);
529
+ });
530
+ });
531
+
532
+ it('should filter with $or operator', async () => {
533
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
534
+ $or: [{ price: { $gt: 900 } }, { category: { $in: ['electronics', 'books'] } }],
535
+ });
536
+ expect(results.length).toBeGreaterThan(0);
537
+ results.forEach(result => {
538
+ const condition1 = Number(result.metadata?.price) > 900;
539
+ const condition2 = ['electronics', 'books'].includes(result.metadata?.category);
540
+ expect(condition1 || condition2).toBe(true);
541
+ });
542
+ });
543
+
544
+ it('should handle nested logical operators', async () => {
545
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
546
+ $and: [
547
+ {
548
+ $or: [{ category: 'electronics' }, { category: 'books' }],
549
+ },
550
+ { price: { $lt: 100 } },
551
+ { inStock: true },
552
+ ],
553
+ });
554
+ expect(results.length).toBeGreaterThan(0);
555
+ results.forEach(result => {
556
+ expect(['electronics', 'books']).toContain(result.metadata?.category);
557
+ expect(Number(result.metadata?.price)).toBeLessThan(100);
558
+ expect(result.metadata?.inStock).toBe(true);
559
+ });
560
+ });
561
+
562
+ it('uses implicit $eq within $or', async () => {
563
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
564
+ $or: [{ category: 'electronics' }, { price: { $gt: 100 } }],
565
+ });
566
+ expect(results.length).toBeGreaterThan(0);
567
+ });
568
+
569
+ it('requires multiple conditions in logical operators', async () => {
570
+ await expect(
571
+ vectorDB.query(testIndexName2, [1, 0, 0], 10, {
572
+ $and: [{ category: 'electronics' }],
573
+ }),
574
+ ).rejects.toThrow();
575
+
576
+ await expect(
577
+ vectorDB.query(testIndexName2, [1, 0, 0], 10, {
578
+ $or: [{ price: { $gt: 900 } }],
579
+ }),
580
+ ).rejects.toThrow();
581
+ });
582
+ });
583
+
584
+ describe('Complex Filter Combinations', () => {
585
+ it('combines multiple operators and conditions', async () => {
586
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
587
+ $and: [
588
+ { price: { $gt: 20 } },
589
+ { inStock: true },
590
+ {
591
+ $or: [{ category: { $in: ['books'] } }, { rating: { $gt: 4.5 } }],
592
+ },
593
+ ],
594
+ });
595
+ expect(results.length).toBeGreaterThan(0);
596
+ results.forEach(result => {
597
+ expect(Number(result.metadata?.price)).toBeGreaterThan(20);
598
+ expect(result.metadata?.inStock).toBe(true);
599
+ expect(result.metadata?.category === 'books' || Number(result.metadata?.rating) > 4.5).toBe(true);
600
+ });
601
+ });
602
+
603
+ it('handles complex nested conditions', async () => {
604
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
605
+ $or: [
606
+ {
607
+ $and: [{ category: 'electronics' }, { price: { $gt: 700 } }],
608
+ },
609
+ {
610
+ $and: [{ category: 'books' }, { price: { $lt: 20 } }],
611
+ },
612
+ ],
613
+ });
614
+ expect(results.length).toBeGreaterThan(0);
615
+ results.forEach(result => {
616
+ if (result.metadata?.category === 'electronics') {
617
+ expect(Number(result.metadata?.price)).toBeGreaterThan(700);
618
+ } else {
619
+ expect(Number(result.metadata?.price)).toBeLessThan(20);
620
+ }
621
+ });
622
+ });
623
+
624
+ it('should combine comparison and array operators', async () => {
625
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
626
+ $and: [{ price: { $gte: 500 } }, { rating: { $gt: 4.5 } }],
627
+ });
628
+ expect(results.length).toBeGreaterThan(0);
629
+ results.forEach(result => {
630
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
631
+ expect(Number(result.metadata?.rating)).toBeGreaterThan(4.5);
632
+ });
633
+ });
634
+
635
+ it('should handle multiple conditions on same field', async () => {
636
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
637
+ $and: [{ price: { $gte: 30 } }, { price: { $lte: 800 } }],
638
+ });
639
+ expect(results.length).toBeGreaterThan(0);
640
+ results.forEach(result => {
641
+ const price = Number(result.metadata?.price);
642
+ expect(price).toBeGreaterThanOrEqual(30);
643
+ expect(price).toBeLessThanOrEqual(800);
644
+ });
645
+ });
646
+
647
+ it('should handle deeply nested logical operators', async () => {
648
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
649
+ $or: [
650
+ {
651
+ $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { rating: { $gt: 4.5 } }],
652
+ },
653
+ {
654
+ $and: [{ category: 'books' }, { price: { $lt: 50 } }, { rating: { $gt: 4.0 } }],
655
+ },
656
+ ],
657
+ });
658
+ expect(results.length).toBeGreaterThan(0);
659
+ results.forEach(result => {
660
+ const isExpensiveElectronics =
661
+ result.metadata?.category === 'electronics' &&
662
+ Number(result.metadata?.price) > 700 &&
663
+ Number(result.metadata?.rating) > 4.5;
664
+
665
+ const isCheapBook =
666
+ result.metadata?.category === 'books' &&
667
+ Number(result.metadata?.price) < 50 &&
668
+ Number(result.metadata?.rating) > 4.0;
669
+
670
+ expect(isExpensiveElectronics || isCheapBook).toBe(true);
671
+ });
672
+ });
673
+ });
674
+
675
+ describe('Date and Numeric Edge Cases', () => {
676
+ beforeAll(async () => {
677
+ const vectors = [
678
+ [0.1, 0.1, 0.1],
679
+ [0.2, 0.2, 0.2],
680
+ ];
681
+
682
+ const metadata = [
683
+ {
684
+ zero: 0,
685
+ negativeZero: -0,
686
+ infinity: Infinity,
687
+ negativeInfinity: -Infinity,
688
+ decimal: 0.1,
689
+ negativeDecimal: -0.1,
690
+ currentDate: new Date().toISOString(),
691
+ epochDate: new Date(0).toISOString(),
692
+ futureDate: new Date('2100-01-01').toISOString(),
693
+ },
694
+ {
695
+ maxInt: Number.MAX_SAFE_INTEGER,
696
+ minInt: Number.MIN_SAFE_INTEGER,
697
+ maxFloat: Number.MAX_VALUE,
698
+ minFloat: Number.MIN_VALUE,
699
+ pastDate: new Date('1900-01-01').toISOString(),
700
+ currentDate: new Date().toISOString(),
701
+ },
702
+ ];
703
+
704
+ await vectorDB.upsert(testIndexName2, vectors, metadata);
705
+ await new Promise(resolve => setTimeout(resolve, 2000));
706
+ });
707
+
708
+ it('handles special numeric values', async () => {
709
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
710
+ $or: [{ zero: 0 }, { negativeZero: 0 }],
711
+ });
712
+ expect(results.length).toBeGreaterThan(0);
713
+ results.forEach(result => {
714
+ const value = result.metadata?.zero ?? result.metadata?.negativeZero;
715
+ expect(value).toBe(0);
716
+ });
717
+ });
718
+
719
+ it('handles extreme numeric values', async () => {
720
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
721
+ $or: [{ maxInt: { $gte: Number.MAX_SAFE_INTEGER } }, { minInt: { $lte: Number.MIN_SAFE_INTEGER } }],
722
+ });
723
+ expect(results.length).toBe(1);
724
+ });
725
+
726
+ it('should handle numeric comparisons with decimals', async () => {
727
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
728
+ rating: { $gt: 4.5 },
729
+ });
730
+ expect(results.length).toBeGreaterThan(0);
731
+ results.forEach(result => {
732
+ expect(Number(result.metadata?.rating)).toBeGreaterThan(4.5);
733
+ });
734
+ });
735
+
736
+ it('should handle boolean values', async () => {
737
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
738
+ inStock: { $eq: false },
739
+ });
740
+ expect(results.length).toBeGreaterThan(0);
741
+ results.forEach(result => {
742
+ expect(result.metadata?.inStock).toBe(false);
743
+ });
744
+ });
745
+ });
746
+
747
+ describe('Additional Validation Tests', () => {
748
+ it('should throw error as date is not supported', async () => {
749
+ await expect(
750
+ vectorDB.query(testIndexName2, [1, 0, 0], 10, {
751
+ $and: [
752
+ { currentDate: { $lte: new Date().toISOString() } },
753
+ { currentDate: { $gt: new Date(0).toISOString() } },
754
+ ],
755
+ }),
756
+ ).rejects.toThrow();
757
+ });
758
+ it('should throw error as empty array in $in operator is not supported', async () => {
759
+ await expect(
760
+ vectorDB.query(testIndexName2, [1, 0, 0], 10, {
761
+ category: { $in: [] },
762
+ }),
763
+ ).rejects.toThrow();
764
+ });
765
+ it('should reject non-numeric values in numeric comparisons', async () => {
766
+ await expect(
767
+ vectorDB.query(testIndexName2, [1, 0, 0], 10, {
768
+ price: { $gt: '500' }, // string instead of number
769
+ }),
770
+ ).rejects.toThrow();
771
+ });
772
+
773
+ it('should reject mixed types in $in operator', async () => {
774
+ await expect(
775
+ vectorDB.query(testIndexName2, [1, 0, 0], 10, {
776
+ field: { $in: ['string', 123] }, // mixed string and number
777
+ }),
778
+ ).rejects.toThrow();
779
+ });
780
+ it('should handle undefined filter', async () => {
781
+ const results1 = await vectorDB.query(testIndexName2, [1, 0, 0], 10, undefined);
782
+ const results2 = await vectorDB.query(testIndexName2, [1, 0, 0], 10);
783
+ expect(results1).toEqual(results2);
784
+ expect(results1.length).toBeGreaterThan(0);
785
+ });
786
+
787
+ it('should handle null filter', async () => {
788
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, null as any);
789
+ const results2 = await vectorDB.query(testIndexName2, [1, 0, 0], 10);
790
+ expect(results).toEqual(results2);
791
+ expect(results.length).toBeGreaterThan(0);
792
+ });
793
+ });
794
+
795
+ describe('Additional Edge Cases', () => {
796
+ it('should handle exact boundary conditions', async () => {
797
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
798
+ $and: [{ price: { $gte: 25 } }, { price: { $lte: 1000 } }],
799
+ });
800
+ expect(results.length).toBeGreaterThan(0);
801
+ expect(results.some(r => r.metadata?.price === 25)).toBe(true);
802
+ expect(results.some(r => r.metadata?.price === 1000)).toBe(true);
803
+ });
804
+ });
805
+
806
+ describe('Additional Complex Logical Combinations', () => {
807
+ it('should handle deeply nested $or conditions', async () => {
808
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
809
+ $or: [
810
+ {
811
+ $and: [{ category: 'electronics' }, { $or: [{ price: { $gt: 900 } }, { rating: { $gt: 4.8 } }] }],
812
+ },
813
+ {
814
+ $and: [{ category: 'books' }, { $or: [{ price: { $lt: 30 } }, { rating: { $gt: 4.5 } }] }],
815
+ },
816
+ ],
817
+ });
818
+ expect(results.length).toBeGreaterThan(0);
819
+ results.forEach(result => {
820
+ if (result.metadata?.category === 'electronics') {
821
+ expect(Number(result.metadata?.price) > 900 || Number(result.metadata?.rating) > 4.8).toBe(true);
822
+ } else if (result.metadata?.category === 'books') {
823
+ expect(Number(result.metadata?.price) < 30 || Number(result.metadata?.rating) > 4.5).toBe(true);
824
+ }
825
+ });
826
+ });
827
+
828
+ it('should handle multiple field comparisons with same value', async () => {
829
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
830
+ $or: [{ price: { $gt: 500 } }, { rating: { $gt: 4.5 } }],
831
+ });
832
+ expect(results.length).toBeGreaterThan(0);
833
+ results.forEach(result => {
834
+ expect(Number(result.metadata?.price) > 500 || Number(result.metadata?.rating) > 4.5).toBe(true);
835
+ });
836
+ });
837
+ });
838
+
839
+ describe('Performance Edge Cases', () => {
840
+ it('should handle filters with many conditions', async () => {
841
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
842
+ $and: Array(10)
843
+ .fill(null)
844
+ .map(() => ({
845
+ $or: [{ price: { $gt: 100 } }, { rating: { $gt: 4.0 } }],
846
+ })),
847
+ });
848
+ expect(results.length).toBeGreaterThan(0);
849
+ results.forEach(result => {
850
+ expect(Number(result.metadata?.price) > 100 || Number(result.metadata?.rating) > 4.0).toBe(true);
851
+ });
852
+ });
853
+
854
+ it('should handle deeply nested conditions efficiently', async () => {
855
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
856
+ $or: Array(5)
857
+ .fill(null)
858
+ .map(() => ({
859
+ $and: [{ category: { $in: ['electronics', 'books'] } }, { price: { $gt: 50 } }, { rating: { $gt: 4.0 } }],
860
+ })),
861
+ });
862
+ expect(results.length).toBeGreaterThan(0);
863
+ results.forEach(result => {
864
+ expect(['electronics', 'books']).toContain(result.metadata?.category);
865
+ expect(Number(result.metadata?.price)).toBeGreaterThan(50);
866
+ expect(Number(result.metadata?.rating)).toBeGreaterThan(4.0);
867
+ });
868
+ });
869
+
870
+ it('should handle large number of $or conditions', async () => {
871
+ const results = await vectorDB.query(testIndexName2, [1, 0, 0], 10, {
872
+ $or: [
873
+ ...Array(5)
874
+ .fill(null)
875
+ .map((_, i) => ({
876
+ price: { $gt: i * 100 },
877
+ })),
878
+ ...Array(5)
879
+ .fill(null)
880
+ .map((_, i) => ({
881
+ rating: { $gt: 4.0 + i * 0.1 },
882
+ })),
883
+ ],
884
+ });
885
+ expect(results.length).toBeGreaterThan(0);
886
+ });
887
+ });
888
+ });
889
+ });