@mastra/chroma 0.11.4 → 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,1672 +0,0 @@
1
- import { createVectorTestSuite } from '@internal/storage-test-utils';
2
- import type { QueryResult, IndexStats } from '@mastra/core/vector';
3
- import { describe, expect, beforeEach, afterEach, it, beforeAll, afterAll, vi } from 'vitest';
4
-
5
- import { ChromaVector } from './';
6
-
7
- describe('ChromaVector Integration Tests', () => {
8
- let vectorDB = new ChromaVector();
9
-
10
- const testIndexName = 'test-index';
11
- const testIndexName2 = 'test-index-2';
12
- const testIndexName3 = 'test-index-3';
13
- const dimension = 3;
14
-
15
- beforeEach(async () => {
16
- // Clean up any existing test index
17
- try {
18
- await vectorDB.deleteIndex({ indexName: testIndexName });
19
- } catch {
20
- // Ignore errors if index doesn't exist
21
- }
22
- await vectorDB.createIndex({ indexName: testIndexName, dimension });
23
- }, 5000);
24
-
25
- afterEach(async () => {
26
- // Cleanup after tests
27
- try {
28
- await vectorDB.deleteIndex({ indexName: testIndexName });
29
- } catch {
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({ indexName: 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({ indexName: 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({ indexName: testIndex, dimension, metric });
59
-
60
- const stats = await vectorDB.describeIndex({ indexName: testIndex });
61
- expect(stats.metric).toBe(metric);
62
-
63
- await vectorDB.deleteIndex({ indexName: 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({ indexName: testIndexName, vectors: testVectors });
79
- expect(ids).toHaveLength(testVectors.length);
80
- ids.forEach(id => expect(typeof id).toBe('string'));
81
-
82
- const stats = await vectorDB.describeIndex({ indexName: 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({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata, ids: testIds });
88
-
89
- const stats = await vectorDB.describeIndex({ indexName: 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({ indexName: testIndexName, queryVector: testVectors?.[i]!, topK: 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({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata, ids: 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({
108
- indexName: testIndexName,
109
- vectors: updatedVector,
110
- metadata: updatedMetadata,
111
- ids: [testIds?.[0]!],
112
- });
113
-
114
- // Verify update
115
- const results = await vectorDB.query({ indexName: testIndexName, queryVector: updatedVector?.[0]!, topK: 1 });
116
- expect(results?.[0]?.id).toBe(testIds[0]);
117
- expect(results?.[0]?.metadata).toEqual(updatedMetadata[0]);
118
- });
119
-
120
- it('should update the vector by id', async () => {
121
- const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
122
- expect(ids).toHaveLength(3);
123
-
124
- const idToBeUpdated = ids[0] as string;
125
- const newVector = [1, 2, 3];
126
- const newMetaData = {
127
- test: 'updates',
128
- };
129
-
130
- const update = {
131
- vector: newVector,
132
- metadata: newMetaData,
133
- };
134
-
135
- await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
136
-
137
- const results: QueryResult[] = await vectorDB.query({
138
- indexName: testIndexName,
139
- queryVector: newVector,
140
- topK: 2,
141
- includeVector: true,
142
- });
143
- expect(results[0]?.id).toBe(idToBeUpdated);
144
- expect(results[0]?.vector).toEqual(newVector);
145
- expect(results[0]?.metadata).toEqual(newMetaData);
146
- });
147
-
148
- it('should only update the metadata by id', async () => {
149
- const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
150
- expect(ids).toHaveLength(3);
151
-
152
- const idToBeUpdated = ids[0] as string;
153
- const newMetaData = {
154
- test: 'updates',
155
- };
156
-
157
- const update = {
158
- metadata: newMetaData,
159
- };
160
-
161
- await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
162
-
163
- const results: QueryResult[] = await vectorDB.query({
164
- indexName: testIndexName,
165
- queryVector: testVectors[0] as number[],
166
- topK: 2,
167
- includeVector: true,
168
- });
169
- expect(results[0]?.id).toBe(idToBeUpdated);
170
- expect(results[0]?.vector).toEqual(testVectors[0]);
171
- expect(results[0]?.metadata).toEqual(newMetaData);
172
- });
173
-
174
- it('should only update vector embeddings by id', async () => {
175
- const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
176
- expect(ids).toHaveLength(3);
177
-
178
- const idToBeUpdated = ids[0] as string;
179
- const newVector = [1, 2, 3];
180
-
181
- const update = {
182
- vector: newVector,
183
- };
184
-
185
- await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
186
-
187
- const results: QueryResult[] = await vectorDB.query({
188
- indexName: testIndexName,
189
- queryVector: newVector,
190
- topK: 2,
191
- includeVector: true,
192
- });
193
- expect(results[0]?.id).toBe(idToBeUpdated);
194
- expect(results[0]?.vector).toEqual(newVector);
195
- });
196
-
197
- it('should throw exception when no updates are given', async () => {
198
- await expect(vectorDB.updateVector({ indexName: testIndexName, id: 'id', update: {} })).rejects.toThrow(
199
- 'No updates provided',
200
- );
201
- });
202
-
203
- it('should delete the vector by id', async () => {
204
- const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
205
- expect(ids).toHaveLength(3);
206
- const idToBeDeleted = ids[0] as string;
207
-
208
- await vectorDB.deleteVector({ indexName: testIndexName, id: idToBeDeleted });
209
-
210
- const results: QueryResult[] = await vectorDB.query({
211
- indexName: testIndexName,
212
- queryVector: [1.0, 0.0, 0.0],
213
- topK: 2,
214
- });
215
-
216
- expect(results).toHaveLength(2);
217
- expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
218
- });
219
- });
220
-
221
- describe('Query Operations', () => {
222
- const testVectors = [
223
- [1.0, 0.0, 0.0],
224
- [0.0, 1.0, 0.0],
225
- [0.0, 0.0, 1.0],
226
- ];
227
- const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
228
- const testIds = ['vec1', 'vec2', 'vec3'];
229
-
230
- beforeEach(async () => {
231
- await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata, ids: testIds });
232
- });
233
-
234
- describe('Basic Queries', () => {
235
- it('should perform vector search with topK', async () => {
236
- const queryVector = [1.0, 0.1, 0.1];
237
- const topK = 2;
238
-
239
- const results: QueryResult[] = await vectorDB.query({ indexName: testIndexName, queryVector, topK });
240
-
241
- expect(results).toHaveLength(topK);
242
- expect(results?.[0]?.id).toBe(testIds[0]); // Should match x-axis vector most closely
243
- });
244
- });
245
-
246
- describe('Filter Queries', () => {
247
- it('should filter query results', async () => {
248
- const queryVector = [1.0, 1.0, 1.0];
249
- const filter = { label: 'x-axis' };
250
-
251
- const results = await vectorDB.query({ indexName: testIndexName, queryVector, topK: 3, filter });
252
-
253
- expect(results).toHaveLength(1);
254
- expect(results?.[0]?.metadata?.label).toBe('x-axis');
255
- });
256
- });
257
-
258
- describe('Vector Inclusion', () => {
259
- it('should include vector in query results', async () => {
260
- const queryVector = [1.0, 0.1, 0.1];
261
- const topK = 1;
262
-
263
- const results = await vectorDB.query({
264
- indexName: testIndexName,
265
- queryVector,
266
- topK,
267
- includeVector: true,
268
- });
269
-
270
- expect(results).toHaveLength(topK);
271
- expect(results?.[0]?.vector).toEqual(testVectors[0]);
272
- });
273
- });
274
- });
275
-
276
- describe('Error Handling', () => {
277
- const testIndexName = 'test_index_error';
278
- beforeAll(async () => {
279
- await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
280
- });
281
-
282
- afterAll(async () => {
283
- await vectorDB.deleteIndex({ indexName: testIndexName });
284
- });
285
-
286
- it('should handle non-existent index queries', async () => {
287
- await expect(vectorDB.query({ indexName: 'non-existent-index', queryVector: [1, 2, 3] })).rejects.toThrow();
288
- });
289
-
290
- it('should handle invalid dimension vectors', async () => {
291
- const invalidVector = [1, 2, 3, 4]; // 4D vector for 3D index
292
- await expect(vectorDB.upsert({ indexName: testIndexName, vectors: [invalidVector] })).rejects.toThrow();
293
- });
294
-
295
- it('should handle duplicate index creation gracefully', async () => {
296
- const infoSpy = vi.spyOn(vectorDB['logger'], 'info');
297
- const warnSpy = vi.spyOn(vectorDB['logger'], 'warn');
298
-
299
- const duplicateIndexName = `duplicate-test`;
300
- const dimension = 768;
301
-
302
- try {
303
- // Create index first time
304
- await vectorDB.createIndex({
305
- indexName: duplicateIndexName,
306
- dimension,
307
- metric: 'cosine',
308
- });
309
-
310
- // Try to create with same dimensions - should not throw
311
- await expect(
312
- vectorDB.createIndex({
313
- indexName: duplicateIndexName,
314
- dimension,
315
- metric: 'cosine',
316
- }),
317
- ).resolves.not.toThrow();
318
-
319
- expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('already exists with'));
320
-
321
- // Try to create with same dimensions and different metric - should not throw
322
- await expect(
323
- vectorDB.createIndex({
324
- indexName: duplicateIndexName,
325
- dimension,
326
- metric: 'euclidean',
327
- }),
328
- ).resolves.not.toThrow();
329
-
330
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Attempted to create index with metric'));
331
-
332
- // Try to create with different dimensions - should throw
333
- await expect(
334
- vectorDB.createIndex({
335
- indexName: duplicateIndexName,
336
- dimension: dimension + 1,
337
- metric: 'cosine',
338
- }),
339
- ).rejects.toThrow(
340
- `Index "${duplicateIndexName}" already exists with ${dimension} dimensions, but ${dimension + 1} dimensions were requested`,
341
- );
342
- } finally {
343
- infoSpy.mockRestore();
344
- warnSpy.mockRestore();
345
- // Cleanup
346
- await vectorDB.deleteIndex({ indexName: duplicateIndexName });
347
- }
348
- });
349
- });
350
-
351
- describe('Filter Validation in Queries', () => {
352
- it('rejects queries with null values', async () => {
353
- await expect(
354
- vectorDB.query({
355
- indexName: testIndexName,
356
- queryVector: [1, 0, 0],
357
- filter: {
358
- field: null,
359
- },
360
- }),
361
- ).rejects.toThrow();
362
-
363
- await expect(
364
- vectorDB.query({
365
- indexName: testIndexName,
366
- queryVector: [1, 0, 0],
367
- filter: {
368
- other: { $eq: null },
369
- },
370
- }),
371
- ).rejects.toThrow();
372
- });
373
-
374
- it('validates array operator values', async () => {
375
- await expect(
376
- vectorDB.query({
377
- indexName: testIndexName,
378
- queryVector: [1, 0, 0],
379
- filter: {
380
- tags: { $in: null } as any,
381
- },
382
- }),
383
- ).rejects.toThrow();
384
- });
385
-
386
- it('validates numeric values for comparison operators', async () => {
387
- await expect(
388
- vectorDB.query({
389
- indexName: testIndexName,
390
- queryVector: [1, 0, 0],
391
- filter: {
392
- price: { $gt: 'not-a-number' },
393
- },
394
- }),
395
- ).rejects.toThrow();
396
- });
397
-
398
- it('validates value types', async () => {
399
- await expect(
400
- vectorDB.query({
401
- indexName: testIndexName,
402
- queryVector: [1, 0, 0],
403
- filter: { date: { $gt: 'not-a-date' } },
404
- }),
405
- ).rejects.toThrow();
406
-
407
- await expect(
408
- vectorDB.query({
409
- indexName: testIndexName,
410
- queryVector: [1, 0, 0],
411
- filter: { number: { $lt: 'not-a-number' } },
412
- }),
413
- ).rejects.toThrow();
414
- });
415
-
416
- it('validates array operators', async () => {
417
- const invalidValues = [123, 'string', true, { key: 'value' }, null];
418
- for (const op of ['$in', '$nin']) {
419
- for (const val of invalidValues) {
420
- await expect(
421
- vectorDB.query({
422
- indexName: testIndexName,
423
- queryVector: [1, 0, 0],
424
- filter: { field: { [op]: val } },
425
- }),
426
- ).rejects.toThrow();
427
- }
428
- }
429
- });
430
-
431
- it('rejects invalid array operator values', async () => {
432
- // Test non-undefined values
433
- const invalidValues = [123, 'string', true, { key: 'value' }, null];
434
- for (const op of ['$in', '$nin']) {
435
- for (const val of invalidValues) {
436
- await expect(
437
- vectorDB.query({
438
- indexName: testIndexName,
439
- queryVector: [1, 0, 0],
440
- filter: { field: { [op]: val } },
441
- }),
442
- ).rejects.toThrow();
443
- }
444
- }
445
- });
446
-
447
- it('validates comparison operators', async () => {
448
- // Basic equality can accept any non-undefined value
449
- for (const op of ['$eq', '$ne']) {
450
- await expect(
451
- vectorDB.query({
452
- indexName: testIndexName,
453
- queryVector: [1, 0, 0],
454
- filter: { field: { [op]: undefined } },
455
- }),
456
- ).rejects.toThrow();
457
- }
458
-
459
- // Numeric comparisons require numbers
460
- const numOps = ['$gt', '$gte', '$lt', '$lte'];
461
- const invalidNumericValues = ['not-a-number', true, [], {}, null, undefined];
462
- for (const op of numOps) {
463
- for (const val of invalidNumericValues) {
464
- await expect(
465
- vectorDB.query({
466
- indexName: testIndexName,
467
- queryVector: [1, 0, 0],
468
- filter: { field: { [op]: val } },
469
- }),
470
- ).rejects.toThrow();
471
- }
472
- }
473
- });
474
-
475
- it('validates multiple invalid values', async () => {
476
- await expect(
477
- vectorDB.query({
478
- indexName: testIndexName,
479
- queryVector: [1, 0, 0],
480
- filter: {
481
- field1: { $in: 'not-array' } as any,
482
- field2: { $exists: 'not-boolean' } as any,
483
- field3: { $gt: 'not-number' } as any,
484
- },
485
- }),
486
- ).rejects.toThrow();
487
- });
488
-
489
- it('handles empty object filters', async () => {
490
- // Test empty object at top level
491
- await expect(
492
- vectorDB.query({
493
- indexName: testIndexName,
494
- queryVector: [1, 0, 0],
495
- filter: { field: { $eq: {} } },
496
- }),
497
- ).rejects.toThrow();
498
- });
499
-
500
- it('handles empty/undefined filters by returning all results', async () => {
501
- const noFilterCases = [{ field: {} }, { field: undefined }, { field: { $in: undefined } }];
502
-
503
- for (const filter of noFilterCases) {
504
- await expect(vectorDB.query({ indexName: testIndexName, queryVector: [1, 0, 0], filter })).rejects.toThrow();
505
- }
506
- });
507
- it('handles empty object filters', async () => {
508
- // Test empty object at top level
509
- await expect(
510
- vectorDB.query({
511
- indexName: testIndexName,
512
- queryVector: [1, 0, 0],
513
- filter: {},
514
- }),
515
- ).rejects.toThrow();
516
- });
517
- });
518
-
519
- describe('Metadata Filter Tests', () => {
520
- // Set up test vectors and metadata
521
- beforeAll(async () => {
522
- try {
523
- await vectorDB.deleteIndex({ indexName: testIndexName2 });
524
- } catch {
525
- // Ignore errors if index doesn't exist
526
- }
527
- await vectorDB.createIndex({ indexName: testIndexName2, dimension });
528
-
529
- const vectors = [
530
- [1, 0, 0], // Electronics
531
- [0, 1, 0], // Books
532
- [0, 0, 1], // Electronics
533
- [0, 0, 0.1], // Books
534
- ];
535
-
536
- const metadata = [
537
- {
538
- category: 'electronics',
539
- price: 1000,
540
- rating: 4.8,
541
- inStock: true,
542
- },
543
- {
544
- category: 'books',
545
- price: 25,
546
- rating: 4.2,
547
- inStock: true,
548
- },
549
- {
550
- category: 'electronics',
551
- price: 500,
552
- rating: 4.5,
553
- inStock: false,
554
- },
555
- {
556
- category: 'books',
557
- price: 15,
558
- rating: 4.9,
559
- inStock: true,
560
- },
561
- ];
562
-
563
- await vectorDB.upsert({ indexName: testIndexName2, vectors, metadata });
564
- // Wait for indexing
565
- await new Promise(resolve => setTimeout(resolve, 2000));
566
- });
567
-
568
- afterAll(async () => {
569
- // Cleanup after tests
570
- try {
571
- await vectorDB.deleteIndex({ indexName: testIndexName2 });
572
- } catch {
573
- // Ignore cleanup errors
574
- }
575
- });
576
-
577
- describe('Basic Comparison Operators', () => {
578
- it('filters with $eq operator', async () => {
579
- const results = await vectorDB.query({
580
- indexName: testIndexName2,
581
- queryVector: [1, 0, 0],
582
- filter: { category: { $eq: 'electronics' } },
583
- });
584
- expect(results.length).toBe(2);
585
- results.forEach(result => {
586
- expect(result.metadata?.category).toBe('electronics');
587
- });
588
- });
589
-
590
- it('filters with implicit $eq', async () => {
591
- const results = await vectorDB.query({
592
- indexName: testIndexName2,
593
- queryVector: [1, 0, 0],
594
- filter: { category: 'electronics' }, // implicit $eq
595
- });
596
- expect(results.length).toBe(2);
597
- results.forEach(result => {
598
- expect(result.metadata?.category).toBe('electronics');
599
- });
600
- });
601
- it('filters with $gt operator', async () => {
602
- const results = await vectorDB.query({
603
- indexName: testIndexName2,
604
- queryVector: [1, 0, 0],
605
- filter: { price: { $gt: 500 } },
606
- });
607
- expect(results.length).toBe(1);
608
- results.forEach(result => {
609
- expect(Number(result.metadata?.price)).toBeGreaterThan(500);
610
- });
611
- });
612
-
613
- it('filters with $gte operator', async () => {
614
- const results = await vectorDB.query({
615
- indexName: testIndexName2,
616
- queryVector: [1, 0, 0],
617
- filter: { price: { $gte: 500 } },
618
- });
619
- expect(results.length).toBe(2);
620
- results.forEach(result => {
621
- expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
622
- });
623
- });
624
-
625
- it('filters with $lt operator', async () => {
626
- const results = await vectorDB.query({
627
- indexName: testIndexName2,
628
- queryVector: [1, 0, 0],
629
- filter: { price: { $lt: 100 } },
630
- });
631
- expect(results.length).toBe(2);
632
- results.forEach(result => {
633
- expect(Number(result.metadata?.price)).toBeLessThan(100);
634
- });
635
- });
636
-
637
- it('filters with $lte operator', async () => {
638
- const results = await vectorDB.query({
639
- indexName: testIndexName2,
640
- queryVector: [1, 0, 0],
641
- filter: { price: { $lte: 500 } },
642
- });
643
- expect(results.length).toBeGreaterThan(0);
644
- results.forEach(result => {
645
- expect(Number(result.metadata?.price)).toBeLessThanOrEqual(500);
646
- });
647
- });
648
-
649
- it('filters with $gte, $lt, $lte operators', async () => {
650
- const results = await vectorDB.query({
651
- indexName: testIndexName2,
652
- queryVector: [1, 0, 0],
653
- filter: { price: { $gte: 25, $lte: 500 } },
654
- });
655
- expect(results.length).toBe(2);
656
- results.forEach(result => {
657
- expect(Number(result.metadata?.price)).toBeLessThanOrEqual(500);
658
- expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(25);
659
- });
660
- });
661
-
662
- it('filters with $ne operator', async () => {
663
- const results = await vectorDB.query({
664
- indexName: testIndexName2,
665
- queryVector: [1, 0, 0],
666
- filter: { category: { $ne: 'electronics' } },
667
- });
668
- expect(results.length).toBe(2);
669
- results.forEach(result => {
670
- expect(result.metadata?.category).not.toBe('electronics');
671
- });
672
- });
673
-
674
- it('filters with boolean values', async () => {
675
- const results = await vectorDB.query({
676
- indexName: testIndexName2,
677
- queryVector: [1, 0, 0],
678
- filter: { inStock: true }, // test both implicit
679
- });
680
- expect(results.length).toBe(3);
681
- results.forEach(result => {
682
- expect(result.metadata?.inStock).toBe(true);
683
- });
684
- });
685
-
686
- it('filters with multiple fields', async () => {
687
- const results = await vectorDB.query({
688
- indexName: testIndexName2,
689
- queryVector: [1, 0, 0],
690
- filter: { category: 'electronics', price: 1000 },
691
- });
692
- expect(results.length).toBeGreaterThan(0);
693
- results.forEach(result => {
694
- expect(result.metadata?.category === 'electronics' && result.metadata?.price === 1000).toBe(true);
695
- });
696
- });
697
- });
698
-
699
- describe('Array Operators', () => {
700
- it('filters with $in operator', async () => {
701
- const results = await vectorDB.query({
702
- indexName: testIndexName2,
703
- queryVector: [1, 0, 0],
704
- filter: { category: { $in: ['electronics', 'books'] } },
705
- });
706
- expect(results.length).toBeGreaterThan(0);
707
- results.forEach(result => {
708
- expect(['electronics', 'books']).toContain(result.metadata?.category);
709
- });
710
- });
711
-
712
- it('should filter with $in operator for numbers', async () => {
713
- const results = await vectorDB.query({
714
- indexName: testIndexName2,
715
- queryVector: [1, 0, 0],
716
- filter: { price: { $in: [50, 75, 1000] } },
717
- });
718
- expect(results.length).toBeGreaterThan(0);
719
- results.forEach(result => {
720
- expect([50, 75, 1000]).toContain(result.metadata?.price);
721
- });
722
- });
723
-
724
- it('filters with $in operator for booleans', async () => {
725
- const results = await vectorDB.query({
726
- indexName: testIndexName2,
727
- queryVector: [1, 0, 0],
728
- filter: { inStock: { $in: [true] } },
729
- });
730
- expect(results.length).toBeGreaterThan(0);
731
- results.forEach(result => {
732
- expect(result.metadata?.inStock).toBe(true);
733
- });
734
- });
735
- });
736
-
737
- describe('Logical Operators', () => {
738
- it('filters with $and operator', async () => {
739
- const results = await vectorDB.query({
740
- indexName: testIndexName2,
741
- queryVector: [1, 0, 0],
742
- filter: { $and: [{ category: 'electronics' }, { price: { $gt: 500 } }] },
743
- });
744
- expect(results.length).toBe(1);
745
- expect(results[0]?.metadata?.category).toBe('electronics');
746
- expect(Number(results[0]?.metadata?.price)).toBeGreaterThan(500);
747
- });
748
-
749
- it('should filter with $and operator', async () => {
750
- const results = await vectorDB.query({
751
- indexName: testIndexName2,
752
- queryVector: [1, 0, 0],
753
- filter: { $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { inStock: true }] },
754
- });
755
- expect(results.length).toBeGreaterThan(0);
756
- results.forEach(result => {
757
- expect(result.metadata?.category).toBe('electronics');
758
- expect(Number(result.metadata?.price)).toBeGreaterThan(700);
759
- expect(result.metadata?.inStock).toBe(true);
760
- });
761
- });
762
-
763
- it('filters with $or operator', async () => {
764
- const results = await vectorDB.query({
765
- indexName: testIndexName2,
766
- queryVector: [1, 0, 0],
767
- filter: { $or: [{ price: { $gt: 900 } }, { rating: { $gt: 4.8 } }] },
768
- });
769
- expect(results.length).toBe(2);
770
- results.forEach(result => {
771
- expect(Number(result.metadata?.price) > 900 || Number(result.metadata?.rating) > 4.8).toBe(true);
772
- });
773
- });
774
-
775
- it('should filter with $or operator', async () => {
776
- const results = await vectorDB.query({
777
- indexName: testIndexName2,
778
- queryVector: [1, 0, 0],
779
- filter: { $or: [{ price: { $gt: 900 } }, { category: { $in: ['electronics', 'books'] } }] },
780
- });
781
- expect(results.length).toBeGreaterThan(0);
782
- results.forEach(result => {
783
- const condition1 = Number(result.metadata?.price) > 900;
784
- const condition2 = ['electronics', 'books'].includes(result.metadata?.category);
785
- expect(condition1 || condition2).toBe(true);
786
- });
787
- });
788
-
789
- it('should handle nested logical operators', async () => {
790
- const results = await vectorDB.query({
791
- indexName: testIndexName2,
792
- queryVector: [1, 0, 0],
793
- filter: {
794
- $and: [
795
- {
796
- $or: [{ category: 'electronics' }, { category: 'books' }],
797
- },
798
- { price: { $lt: 100 } },
799
- { inStock: true },
800
- ],
801
- },
802
- });
803
- expect(results.length).toBeGreaterThan(0);
804
- results.forEach(result => {
805
- expect(['electronics', 'books']).toContain(result.metadata?.category);
806
- expect(Number(result.metadata?.price)).toBeLessThan(100);
807
- expect(result.metadata?.inStock).toBe(true);
808
- });
809
- });
810
-
811
- it('uses implicit $eq within $or', async () => {
812
- const results = await vectorDB.query({
813
- indexName: testIndexName2,
814
- queryVector: [1, 0, 0],
815
- filter: { $or: [{ category: 'electronics' }, { price: { $gt: 100 } }] },
816
- });
817
- expect(results.length).toBeGreaterThan(0);
818
- });
819
-
820
- it('accepts single conditions in logical operators', async () => {
821
- const results = await vectorDB.query({
822
- indexName: testIndexName2,
823
- queryVector: [1, 0, 0],
824
- filter: { $and: [{ category: 'electronics' }] },
825
- });
826
- expect(results.length).toBeGreaterThan(0);
827
-
828
- const results2 = await vectorDB.query({
829
- indexName: testIndexName2,
830
- queryVector: [1, 0, 0],
831
- filter: { $or: [{ price: { $gt: 900 } }] },
832
- });
833
- expect(results2.length).toBeGreaterThan(0);
834
- });
835
- });
836
-
837
- describe('Complex Filter Combinations', () => {
838
- it('combines multiple operators and conditions', async () => {
839
- const results = await vectorDB.query({
840
- indexName: testIndexName2,
841
- queryVector: [1, 0, 0],
842
- filter: {
843
- $and: [
844
- { price: { $gt: 20 } },
845
- { inStock: true },
846
- {
847
- $or: [{ category: { $in: ['books'] } }, { rating: { $gt: 4.5 } }],
848
- },
849
- ],
850
- },
851
- });
852
- expect(results.length).toBeGreaterThan(0);
853
- results.forEach(result => {
854
- expect(Number(result.metadata?.price)).toBeGreaterThan(20);
855
- expect(result.metadata?.inStock).toBe(true);
856
- expect(result.metadata?.category === 'books' || Number(result.metadata?.rating) > 4.5).toBe(true);
857
- });
858
- });
859
-
860
- it('handles complex nested conditions', async () => {
861
- const results = await vectorDB.query({
862
- indexName: testIndexName2,
863
- queryVector: [1, 0, 0],
864
- filter: {
865
- $or: [
866
- {
867
- $and: [{ category: 'electronics' }, { price: { $gt: 700 } }],
868
- },
869
- {
870
- $and: [{ category: 'books' }, { price: { $lt: 20 } }],
871
- },
872
- ],
873
- },
874
- });
875
- expect(results.length).toBeGreaterThan(0);
876
- results.forEach(result => {
877
- if (result.metadata?.category === 'electronics') {
878
- expect(Number(result.metadata?.price)).toBeGreaterThan(700);
879
- } else {
880
- expect(Number(result.metadata?.price)).toBeLessThan(20);
881
- }
882
- });
883
- });
884
-
885
- it('should combine comparison and array operators', async () => {
886
- const results = await vectorDB.query({
887
- indexName: testIndexName2,
888
- queryVector: [1, 0, 0],
889
- filter: { $and: [{ price: { $gte: 500 } }, { rating: { $gt: 4.5 } }] },
890
- });
891
- expect(results.length).toBeGreaterThan(0);
892
- results.forEach(result => {
893
- expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
894
- expect(Number(result.metadata?.rating)).toBeGreaterThan(4.5);
895
- });
896
- });
897
-
898
- it('should handle multiple conditions on same field', async () => {
899
- const results = await vectorDB.query({
900
- indexName: testIndexName2,
901
- queryVector: [1, 0, 0],
902
- filter: { $and: [{ price: { $gte: 30 } }, { price: { $lte: 800 } }] },
903
- });
904
- expect(results.length).toBeGreaterThan(0);
905
- results.forEach(result => {
906
- const price = Number(result.metadata?.price);
907
- expect(price).toBeGreaterThanOrEqual(30);
908
- expect(price).toBeLessThanOrEqual(800);
909
- });
910
- });
911
-
912
- it('should handle deeply nested logical operators', async () => {
913
- const results = await vectorDB.query({
914
- indexName: testIndexName2,
915
- queryVector: [1, 0, 0],
916
- filter: {
917
- $or: [
918
- {
919
- $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { rating: { $gt: 4.5 } }],
920
- },
921
- {
922
- $and: [{ category: 'books' }, { price: { $lt: 50 } }, { rating: { $gt: 4.0 } }],
923
- },
924
- ],
925
- },
926
- });
927
- expect(results.length).toBeGreaterThan(0);
928
- results.forEach(result => {
929
- const isExpensiveElectronics =
930
- result.metadata?.category === 'electronics' &&
931
- Number(result.metadata?.price) > 700 &&
932
- Number(result.metadata?.rating) > 4.5;
933
-
934
- const isCheapBook =
935
- result.metadata?.category === 'books' &&
936
- Number(result.metadata?.price) < 50 &&
937
- Number(result.metadata?.rating) > 4.0;
938
-
939
- expect(isExpensiveElectronics || isCheapBook).toBe(true);
940
- });
941
- });
942
- });
943
-
944
- describe('Date and Numeric Edge Cases', () => {
945
- beforeAll(async () => {
946
- const vectors = [
947
- [0.1, 0.1, 0.1],
948
- [0.2, 0.2, 0.2],
949
- ];
950
-
951
- const metadata = [
952
- {
953
- zero: 0,
954
- negativeZero: -0,
955
- infinity: Infinity,
956
- negativeInfinity: -Infinity,
957
- decimal: 0.1,
958
- negativeDecimal: -0.1,
959
- currentDate: new Date().toISOString(),
960
- epochDate: new Date(0).toISOString(),
961
- futureDate: new Date('2100-01-01').toISOString(),
962
- },
963
- {
964
- maxInt: Number.MAX_SAFE_INTEGER,
965
- minInt: Number.MIN_SAFE_INTEGER,
966
- maxFloat: Number.MAX_VALUE,
967
- minFloat: Number.MIN_VALUE,
968
- pastDate: new Date('1900-01-01').toISOString(),
969
- currentDate: new Date().toISOString(),
970
- },
971
- ];
972
-
973
- await vectorDB.upsert({
974
- indexName: testIndexName2,
975
- vectors,
976
- metadata,
977
- });
978
- await new Promise(resolve => setTimeout(resolve, 2000));
979
- });
980
-
981
- it('handles special numeric values', async () => {
982
- const results = await vectorDB.query({
983
- indexName: testIndexName2,
984
- queryVector: [1, 0, 0],
985
- filter: { $or: [{ zero: 0 }, { negativeZero: 0 }] },
986
- });
987
- expect(results.length).toBeGreaterThan(0);
988
- results.forEach(result => {
989
- const value = result.metadata?.zero ?? result.metadata?.negativeZero;
990
- expect(value).toBe(0);
991
- });
992
- });
993
-
994
- it('handles extreme numeric values', async () => {
995
- const results = await vectorDB.query({
996
- indexName: testIndexName2,
997
- queryVector: [1, 0, 0],
998
- filter: {
999
- $or: [{ maxInt: { $gte: Number.MAX_SAFE_INTEGER } }, { minInt: { $lte: Number.MIN_SAFE_INTEGER } }],
1000
- },
1001
- });
1002
- expect(results.length).toBe(1);
1003
- });
1004
-
1005
- it('should handle numeric comparisons with decimals', async () => {
1006
- const results = await vectorDB.query({
1007
- indexName: testIndexName2,
1008
- queryVector: [1, 0, 0],
1009
- filter: {
1010
- rating: { $gt: 4.5 },
1011
- },
1012
- });
1013
- expect(results.length).toBeGreaterThan(0);
1014
- results.forEach(result => {
1015
- expect(Number(result.metadata?.rating)).toBeGreaterThan(4.5);
1016
- });
1017
- });
1018
-
1019
- it('should handle boolean values', async () => {
1020
- const results = await vectorDB.query({
1021
- indexName: testIndexName2,
1022
- queryVector: [1, 0, 0],
1023
- filter: { inStock: { $eq: false } },
1024
- });
1025
- expect(results.length).toBeGreaterThan(0);
1026
- results.forEach(result => {
1027
- expect(result.metadata?.inStock).toBe(false);
1028
- });
1029
- });
1030
- });
1031
-
1032
- describe('Additional Validation Tests', () => {
1033
- it('should throw error as date is not supported', async () => {
1034
- await expect(
1035
- vectorDB.query({
1036
- indexName: testIndexName2,
1037
- queryVector: [1, 0, 0],
1038
- filter: {
1039
- $and: [
1040
- { currentDate: { $lte: new Date().toISOString() } },
1041
- { currentDate: { $gt: new Date(0).toISOString() } },
1042
- ],
1043
- },
1044
- }),
1045
- ).rejects.toThrow();
1046
- });
1047
- it('should throw error as empty array in $in operator is not supported', async () => {
1048
- await expect(
1049
- vectorDB.query({
1050
- indexName: testIndexName2,
1051
- queryVector: [1, 0, 0],
1052
- filter: {
1053
- category: { $in: [] },
1054
- },
1055
- }),
1056
- ).rejects.toThrow();
1057
- });
1058
- it('should reject non-numeric values in numeric comparisons', async () => {
1059
- await expect(
1060
- vectorDB.query({
1061
- indexName: testIndexName2,
1062
- queryVector: [1, 0, 0],
1063
- filter: {
1064
- price: { $gt: '500' }, // string instead of number
1065
- },
1066
- }),
1067
- ).rejects.toThrow();
1068
- });
1069
-
1070
- it('should reject mixed types in $in operator', async () => {
1071
- await expect(
1072
- vectorDB.query({
1073
- indexName: testIndexName2,
1074
- queryVector: [1, 0, 0],
1075
- filter: {
1076
- field: { $in: ['string', 123] }, // mixed string and number
1077
- },
1078
- }),
1079
- ).rejects.toThrow();
1080
- });
1081
- it('should handle undefined filter', async () => {
1082
- const results1 = await vectorDB.query({
1083
- indexName: testIndexName2,
1084
- queryVector: [1, 0, 0],
1085
- filter: undefined,
1086
- });
1087
- const results2 = await vectorDB.query({
1088
- indexName: testIndexName2,
1089
- queryVector: [1, 0, 0],
1090
- });
1091
- expect(results1).toEqual(results2);
1092
- expect(results1.length).toBeGreaterThan(0);
1093
- });
1094
-
1095
- it('should handle null filter', async () => {
1096
- const results = await vectorDB.query({
1097
- indexName: testIndexName2,
1098
- queryVector: [1, 0, 0],
1099
- filter: null,
1100
- });
1101
- const results2 = await vectorDB.query({
1102
- indexName: testIndexName2,
1103
- queryVector: [1, 0, 0],
1104
- });
1105
- expect(results).toEqual(results2);
1106
- expect(results.length).toBeGreaterThan(0);
1107
- });
1108
- });
1109
-
1110
- describe('Additional Edge Cases', () => {
1111
- it('should handle exact boundary conditions', async () => {
1112
- const results = await vectorDB.query({
1113
- indexName: testIndexName2,
1114
- queryVector: [1, 0, 0],
1115
- filter: {
1116
- $and: [{ price: { $gte: 25 } }, { price: { $lte: 1000 } }],
1117
- },
1118
- });
1119
- expect(results.length).toBeGreaterThan(0);
1120
- expect(results.some(r => r.metadata?.price === 25)).toBe(true);
1121
- expect(results.some(r => r.metadata?.price === 1000)).toBe(true);
1122
- });
1123
- });
1124
-
1125
- describe('Additional Complex Logical Combinations', () => {
1126
- it('should handle deeply nested $or conditions', async () => {
1127
- const results = await vectorDB.query({
1128
- indexName: testIndexName2,
1129
- queryVector: [1, 0, 0],
1130
- filter: {
1131
- $or: [
1132
- {
1133
- $and: [{ category: 'electronics' }, { $or: [{ price: { $gt: 900 } }, { rating: { $gt: 4.8 } }] }],
1134
- },
1135
- {
1136
- $and: [{ category: 'books' }, { $or: [{ price: { $lt: 30 } }, { rating: { $gt: 4.5 } }] }],
1137
- },
1138
- ],
1139
- },
1140
- });
1141
- expect(results.length).toBeGreaterThan(0);
1142
- results.forEach(result => {
1143
- if (result.metadata?.category === 'electronics') {
1144
- expect(Number(result.metadata?.price) > 900 || Number(result.metadata?.rating) > 4.8).toBe(true);
1145
- } else if (result.metadata?.category === 'books') {
1146
- expect(Number(result.metadata?.price) < 30 || Number(result.metadata?.rating) > 4.5).toBe(true);
1147
- }
1148
- });
1149
- });
1150
-
1151
- it('should handle multiple field comparisons with same value', async () => {
1152
- const results = await vectorDB.query({
1153
- indexName: testIndexName2,
1154
- queryVector: [1, 0, 0],
1155
- filter: {
1156
- $or: [{ price: { $gt: 500 } }, { rating: { $gt: 4.5 } }],
1157
- },
1158
- });
1159
- expect(results.length).toBeGreaterThan(0);
1160
- results.forEach(result => {
1161
- expect(Number(result.metadata?.price) > 500 || Number(result.metadata?.rating) > 4.5).toBe(true);
1162
- });
1163
- });
1164
- });
1165
-
1166
- describe('Performance Edge Cases', () => {
1167
- it('should handle filters with many conditions', async () => {
1168
- const results = await vectorDB.query({
1169
- indexName: testIndexName2,
1170
- queryVector: [1, 0, 0],
1171
- filter: {
1172
- $and: Array(10)
1173
- .fill(null)
1174
- .map(() => ({
1175
- $or: [{ price: { $gt: 100 } }, { rating: { $gt: 4.0 } }],
1176
- })),
1177
- },
1178
- });
1179
- expect(results.length).toBeGreaterThan(0);
1180
- results.forEach(result => {
1181
- expect(Number(result.metadata?.price) > 100 || Number(result.metadata?.rating) > 4.0).toBe(true);
1182
- });
1183
- });
1184
-
1185
- it('should handle deeply nested conditions efficiently', async () => {
1186
- const results = await vectorDB.query({
1187
- indexName: testIndexName2,
1188
- queryVector: [1, 0, 0],
1189
- filter: {
1190
- $or: Array(5)
1191
- .fill(null)
1192
- .map(() => ({
1193
- $and: [
1194
- { category: { $in: ['electronics', 'books'] } },
1195
- { price: { $gt: 50 } },
1196
- { rating: { $gt: 4.0 } },
1197
- ],
1198
- })),
1199
- },
1200
- });
1201
- expect(results.length).toBeGreaterThan(0);
1202
- results.forEach(result => {
1203
- expect(['electronics', 'books']).toContain(result.metadata?.category);
1204
- expect(Number(result.metadata?.price)).toBeGreaterThan(50);
1205
- expect(Number(result.metadata?.rating)).toBeGreaterThan(4.0);
1206
- });
1207
- });
1208
-
1209
- it('should handle large number of $or conditions', async () => {
1210
- const results = await vectorDB.query({
1211
- indexName: testIndexName2,
1212
- queryVector: [1, 0, 0],
1213
- filter: {
1214
- $or: [
1215
- ...Array(5)
1216
- .fill(null)
1217
- .map((_, i) => ({
1218
- price: { $gt: i * 100 },
1219
- })),
1220
- ...Array(5)
1221
- .fill(null)
1222
- .map((_, i) => ({
1223
- rating: { $gt: 4.0 + i * 0.1 },
1224
- })),
1225
- ],
1226
- },
1227
- });
1228
- expect(results.length).toBeGreaterThan(0);
1229
- });
1230
- });
1231
- });
1232
-
1233
- describe('Document Operations and Filtering', () => {
1234
- const testDocuments = [
1235
- 'The quick brown fox jumps over the lazy dog',
1236
- 'Pack my box with five dozen liquor jugs',
1237
- 'How vexingly quick daft zebras JUMP',
1238
- ];
1239
-
1240
- beforeAll(async () => {
1241
- try {
1242
- await vectorDB.deleteIndex({ indexName: testIndexName3 });
1243
- } catch {
1244
- // Ignore errors if index doesn't exist
1245
- }
1246
- await vectorDB.createIndex({ indexName: testIndexName3, dimension });
1247
-
1248
- const testVectors = [
1249
- [1.0, 0.0, 0.0],
1250
- [0.0, 1.0, 0.0],
1251
- [0.0, 0.0, 1.0],
1252
- ];
1253
-
1254
- const testMetadata = [
1255
- { source: 'pangram1', length: 43 },
1256
- { source: 'pangram2', length: 32 },
1257
- { source: 'pangram3', length: 30 },
1258
- ];
1259
- const testIds = ['doc1', 'doc2', 'doc3'];
1260
-
1261
- await vectorDB.upsert({
1262
- indexName: testIndexName3,
1263
- vectors: testVectors,
1264
- documents: testDocuments,
1265
- metadata: testMetadata,
1266
- ids: testIds,
1267
- });
1268
-
1269
- // Wait for indexing
1270
- await new Promise(resolve => setTimeout(resolve, 2000));
1271
- });
1272
-
1273
- afterAll(async () => {
1274
- // Cleanup after tests
1275
- try {
1276
- await vectorDB.deleteIndex({ indexName: testIndexName3 });
1277
- } catch {
1278
- // Ignore cleanup errors
1279
- }
1280
- });
1281
-
1282
- describe('Basic Document Operations', () => {
1283
- it('should store and retrieve documents', async () => {
1284
- const results = await vectorDB.query({ indexName: testIndexName3, queryVector: [1.0, 0.0, 0.0], topK: 3 });
1285
- expect(results).toHaveLength(3);
1286
- // Verify documents are returned
1287
-
1288
- expect(results[0]!.document).toBe(testDocuments[0]);
1289
- });
1290
-
1291
- it('should filter documents using $contains', async () => {
1292
- const results = await vectorDB.query({
1293
- indexName: testIndexName3,
1294
- queryVector: [1.0, 0.0, 0.0],
1295
- topK: 3,
1296
- documentFilter: { $contains: 'quick' },
1297
- });
1298
- expect(results).toHaveLength(2);
1299
- });
1300
-
1301
- it('should filter with $not_contains', async () => {
1302
- const results = await vectorDB.query({
1303
- indexName: testIndexName3,
1304
- queryVector: [1.0, 0.0, 0.0],
1305
- topK: 3,
1306
- documentFilter: { $not_contains: 'fox' },
1307
- });
1308
- expect(results.every(r => !r.document?.includes('fox'))).toBe(true);
1309
- });
1310
-
1311
- it('should combine metadata and document filters', async () => {
1312
- const results = await vectorDB.query({
1313
- indexName: testIndexName3,
1314
- queryVector: [1.0, 0.0, 0.0],
1315
- topK: 3,
1316
- filter: { source: 'pangram1' },
1317
- documentFilter: { $contains: 'fox' },
1318
- });
1319
- expect(results).toHaveLength(1);
1320
- expect(results[0]!.metadata?.source).toBe('pangram1');
1321
- expect(results[0]!.document).toContain('fox');
1322
- });
1323
-
1324
- it('should get records with metadata and document filters', async () => {
1325
- const results = await vectorDB.get({
1326
- indexName: testIndexName3,
1327
- filter: { source: 'pangram1' },
1328
- documentFilter: { $contains: 'fox' },
1329
- });
1330
- expect(results).toHaveLength(1);
1331
- expect(results[0]!.metadata?.source).toBe('pangram1');
1332
- expect(results[0]!.document).toContain('fox');
1333
- });
1334
- });
1335
-
1336
- describe('Complex Document Filtering', () => {
1337
- it('should handle $and conditions', async () => {
1338
- const results = await vectorDB.query({
1339
- indexName: testIndexName3,
1340
- queryVector: [1.0, 0.0, 0.0],
1341
- topK: 3,
1342
- documentFilter: { $and: [{ $contains: 'quick' }, { $not_contains: 'fox' }] },
1343
- });
1344
- expect(results).toHaveLength(1);
1345
- expect(results[0]!.document).toContain('quick');
1346
- expect(results[0]!.document).not.toContain('fox');
1347
- });
1348
-
1349
- it('should handle $or conditions', async () => {
1350
- const results = await vectorDB.query({
1351
- indexName: testIndexName3,
1352
- queryVector: [1.0, 0.0, 0.0],
1353
- topK: 3,
1354
- documentFilter: { $or: [{ $contains: 'fox' }, { $contains: 'zebras' }] },
1355
- });
1356
- expect(results).toHaveLength(2);
1357
- expect(results[0]!.document).toContain('fox');
1358
- expect(results[1]!.document).toContain('zebras');
1359
- });
1360
- });
1361
-
1362
- describe('Edge Cases and Validation', () => {
1363
- it('allows empty string in $contains', async () => {
1364
- const results = await vectorDB.query({
1365
- indexName: testIndexName3,
1366
- queryVector: [1.0, 0.0, 0.0],
1367
- topK: 3,
1368
- documentFilter: { $contains: '' },
1369
- });
1370
- expect(results).toHaveLength(3);
1371
- });
1372
-
1373
- it('should be case sensitive', async () => {
1374
- // First verify lowercase works
1375
- const lowerResults = await vectorDB.query({
1376
- indexName: testIndexName3,
1377
- queryVector: [1.0, 0.0, 0.0],
1378
- topK: 3,
1379
- documentFilter: { $contains: 'quick' },
1380
- });
1381
- expect(lowerResults.length).toBe(2);
1382
-
1383
- // Then verify uppercase doesn't match
1384
- const upperResults = await vectorDB.query({
1385
- indexName: testIndexName3,
1386
- queryVector: [1.0, 0.0, 0.0],
1387
- topK: 3,
1388
- documentFilter: { $contains: 'QUICK' },
1389
- });
1390
- expect(upperResults.length).toBe(0);
1391
-
1392
- const upperResults2 = await vectorDB.query({
1393
- indexName: testIndexName3,
1394
- queryVector: [1.0, 0.0, 0.0],
1395
- topK: 3,
1396
- documentFilter: { $contains: 'JUMP' },
1397
- });
1398
- expect(upperResults2.length).toBe(1);
1399
- });
1400
-
1401
- it('should handle exact string matches', async () => {
1402
- const results = await vectorDB.query({
1403
- indexName: testIndexName3,
1404
- queryVector: [1.0, 0.0, 0.0],
1405
- topK: 3,
1406
- documentFilter: { $contains: 'quick brown' }, // Test multi-word match
1407
- });
1408
- expect(results.length).toBe(1);
1409
- expect(results[0]!.document).toContain('quick brown');
1410
- });
1411
-
1412
- it('should handle deeply nested logical operators', async () => {
1413
- const results = await vectorDB.query({
1414
- indexName: testIndexName3,
1415
- queryVector: [1.0, 0.0, 0.0],
1416
- topK: 3,
1417
- documentFilter: {
1418
- $or: [
1419
- {
1420
- $and: [{ $contains: 'quick' }, { $not_contains: 'fox' }],
1421
- },
1422
- {
1423
- $and: [{ $contains: 'box' }, { $not_contains: 'quick' }],
1424
- },
1425
- ],
1426
- },
1427
- });
1428
- expect(results.length).toBeGreaterThan(0);
1429
- results.forEach(result => {
1430
- if (result.document?.includes('quick')) {
1431
- expect(result.document).not.toContain('fox');
1432
- } else if (result.document?.includes('box')) {
1433
- expect(result.document).not.toContain('quick');
1434
- }
1435
- });
1436
- });
1437
- it('should handle undefined document filter', async () => {
1438
- const results1 = await vectorDB.query({
1439
- indexName: testIndexName3,
1440
- queryVector: [1, 0, 0],
1441
- documentFilter: undefined,
1442
- });
1443
- const results2 = await vectorDB.query({
1444
- indexName: testIndexName3,
1445
- queryVector: [1, 0, 0],
1446
- });
1447
- expect(results1).toEqual(results2);
1448
- expect(results1.length).toBeGreaterThan(0);
1449
- });
1450
-
1451
- it('should handle empty object document filter', async () => {
1452
- await expect(
1453
- vectorDB.query({
1454
- indexName: testIndexName3,
1455
- queryVector: [1, 0, 0],
1456
- // @ts-ignore
1457
- documentFilter: {},
1458
- }),
1459
- ).rejects.toThrow();
1460
- });
1461
-
1462
- it('should handle null filter', async () => {
1463
- const results = await vectorDB.query({
1464
- indexName: testIndexName3,
1465
- queryVector: [1, 0, 0],
1466
- documentFilter: null,
1467
- });
1468
- const results2 = await vectorDB.query({
1469
- indexName: testIndexName3,
1470
- queryVector: [1, 0, 0],
1471
- });
1472
- expect(results).toEqual(results2);
1473
- expect(results.length).toBeGreaterThan(0);
1474
- });
1475
- });
1476
- });
1477
-
1478
- describe('Performance and Concurrency', () => {
1479
- const perfTestIndex = 'perf-test-index';
1480
-
1481
- beforeEach(async () => {
1482
- try {
1483
- await vectorDB.deleteIndex({ indexName: perfTestIndex });
1484
- } catch {
1485
- // Ignore errors if index doesn't exist
1486
- }
1487
- await vectorDB.createIndex({ indexName: perfTestIndex, dimension });
1488
- }, 10000);
1489
-
1490
- afterEach(async () => {
1491
- try {
1492
- await vectorDB.deleteIndex({ indexName: perfTestIndex });
1493
- } catch {
1494
- // Ignore cleanup errors
1495
- }
1496
- }, 10000);
1497
-
1498
- it('handles concurrent operations correctly', async () => {
1499
- const promises = Array(10)
1500
- .fill(0)
1501
- .map((_, i) =>
1502
- vectorDB.upsert({
1503
- indexName: perfTestIndex,
1504
- vectors: [[1, 0, 0]],
1505
- metadata: [{ test: 'concurrent', id: i }],
1506
- ids: [`concurrent-${i}`],
1507
- }),
1508
- );
1509
- await Promise.all(promises);
1510
-
1511
- const results = await vectorDB.query({
1512
- indexName: perfTestIndex,
1513
- queryVector: [1, 0, 0],
1514
- filter: { test: 'concurrent' },
1515
- });
1516
- expect(results).toHaveLength(10);
1517
- }, 15000);
1518
-
1519
- it('handles large batch operations', async () => {
1520
- const batchSize = 100; // Using 100 instead of 1000 to keep tests fast
1521
- const vectors = Array(batchSize)
1522
- .fill(0)
1523
- .map(() => [1, 0, 0]);
1524
- const metadata = vectors.map((_, i) => ({ index: i, test: 'batch' }));
1525
- const ids = vectors.map((_, i) => `batch-${i}`);
1526
-
1527
- await vectorDB.upsert({
1528
- indexName: perfTestIndex,
1529
- vectors,
1530
- metadata,
1531
- ids,
1532
- });
1533
-
1534
- // Verify all vectors were inserted
1535
- const stats = await vectorDB.describeIndex({ indexName: perfTestIndex });
1536
- expect(stats.count).toBe(batchSize);
1537
-
1538
- const results = await vectorDB.query({
1539
- indexName: perfTestIndex,
1540
- queryVector: [1, 0, 0],
1541
- filter: { test: 'batch' },
1542
- topK: batchSize,
1543
- });
1544
- expect(results).toHaveLength(batchSize);
1545
-
1546
- // Test querying with pagination
1547
- const pageSize = 20;
1548
- const pages: QueryResult[][] = [];
1549
- for (let i = 0; i < batchSize; i += pageSize) {
1550
- const page = await vectorDB.query({
1551
- indexName: perfTestIndex,
1552
- queryVector: [1, 0, 0],
1553
- filter: { test: 'batch' },
1554
- topK: pageSize,
1555
- });
1556
- pages.push(page);
1557
- expect(page).toHaveLength(Math.min(pageSize, batchSize - i));
1558
- }
1559
- expect(pages).toHaveLength(Math.ceil(batchSize / pageSize));
1560
- }, 30000);
1561
- });
1562
- });
1563
-
1564
- // Metadata filtering tests for Memory system
1565
- describe('Chroma Metadata Filtering', () => {
1566
- const chromaVector = new ChromaVector();
1567
-
1568
- createVectorTestSuite({
1569
- vector: chromaVector,
1570
- createIndex: async (indexName: string) => {
1571
- // Using dimension 4 as required by the metadata filtering test vectors
1572
- await chromaVector.createIndex({ indexName, dimension: 4 });
1573
- },
1574
- deleteIndex: async (indexName: string) => {
1575
- await chromaVector.deleteIndex({ indexName });
1576
- },
1577
- waitForIndexing: async () => {
1578
- // Chroma may need a short wait for indexing
1579
- await new Promise(resolve => setTimeout(resolve, 2000));
1580
- },
1581
- });
1582
- });
1583
-
1584
- // ChromaCloudVector fork functionality tests (requires CHROMA_API_KEY)
1585
- describe.skipIf(!process.env.CHROMA_API_KEY)('ChromaCloudVector Fork Tests', () => {
1586
- let cloudVector: ChromaVector;
1587
- const testIndexName = 'fork-test-index';
1588
- const forkedIndexName = 'forked-test-index';
1589
- const dimension = 3;
1590
-
1591
- beforeEach(async () => {
1592
- cloudVector = new ChromaVector({
1593
- apiKey: process.env.CHROMA_API_KEY,
1594
- });
1595
-
1596
- // Clean up any existing test indexes
1597
- try {
1598
- await cloudVector.deleteIndex({ indexName: testIndexName });
1599
- } catch {
1600
- // Ignore errors if index doesn't exist
1601
- }
1602
- try {
1603
- await cloudVector.deleteIndex({ indexName: forkedIndexName });
1604
- } catch {
1605
- // Ignore errors if index doesn't exist
1606
- }
1607
- });
1608
-
1609
- afterEach(async () => {
1610
- // Clean up test indexes
1611
- try {
1612
- await cloudVector.deleteIndex({ indexName: testIndexName });
1613
- } catch {
1614
- // Ignore cleanup errors
1615
- }
1616
- try {
1617
- await cloudVector.deleteIndex({ indexName: forkedIndexName });
1618
- } catch {
1619
- // Ignore cleanup errors
1620
- }
1621
- });
1622
-
1623
- it('should fork an index successfully', async () => {
1624
- // Create initial index with some data
1625
- await cloudVector.createIndex({ indexName: testIndexName, dimension });
1626
-
1627
- const testVectors = [
1628
- [1.0, 0.0, 0.0],
1629
- [0.0, 1.0, 0.0],
1630
- [0.0, 0.0, 1.0],
1631
- ];
1632
- const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
1633
- const testIds = ['vec1', 'vec2', 'vec3'];
1634
-
1635
- await cloudVector.upsert({
1636
- indexName: testIndexName,
1637
- vectors: testVectors,
1638
- ids: testIds,
1639
- metadata: testMetadata,
1640
- });
1641
-
1642
- // Fork the index
1643
- await cloudVector.forkIndex({
1644
- indexName: testIndexName,
1645
- newIndexName: forkedIndexName,
1646
- });
1647
-
1648
- // Verify both indexes exist and have the same data
1649
- let originalStats = await cloudVector.describeIndex({ indexName: testIndexName });
1650
- let forkedStats = await cloudVector.describeIndex({ indexName: forkedIndexName });
1651
-
1652
- expect(originalStats.count).toBe(3);
1653
- expect(forkedStats.count).toBe(3);
1654
-
1655
- await cloudVector.deleteVector({ indexName: forkedIndexName, id: 'vec1' });
1656
-
1657
- originalStats = await cloudVector.describeIndex({ indexName: testIndexName });
1658
- forkedStats = await cloudVector.describeIndex({ indexName: forkedIndexName });
1659
-
1660
- expect(originalStats.count).toBe(3);
1661
- expect(forkedStats.count).toBe(2);
1662
- });
1663
-
1664
- it('should throw error when forking non-existent index', async () => {
1665
- await expect(
1666
- cloudVector.forkIndex({
1667
- indexName: 'non-existent-index',
1668
- newIndexName: forkedIndexName,
1669
- }),
1670
- ).rejects.toThrow();
1671
- });
1672
- });