@mastra/pinecone 0.0.0-commonjs-20250227130920

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,762 @@
1
+ import dotenv from 'dotenv';
2
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+
4
+ import { PineconeVector } from './';
5
+
6
+ dotenv.config();
7
+
8
+ const PINECONE_API_KEY = process.env.PINECONE_API_KEY!;
9
+
10
+ // if (!PINECONE_API_KEY) {
11
+ // throw new Error('Please set PINECONE_API_KEY and PINECONE_ENVIRONMENT in .env file');
12
+ // }
13
+ // TODO: skip until we the secrets on Github
14
+
15
+ function waitUntilReady(vectorDB: PineconeVector, indexName: string) {
16
+ return new Promise(resolve => {
17
+ const interval = setInterval(async () => {
18
+ try {
19
+ const stats = await vectorDB.describeIndex(indexName);
20
+ if (!!stats) {
21
+ clearInterval(interval);
22
+ resolve(true);
23
+ }
24
+ } catch (error) {
25
+ console.log(error);
26
+ }
27
+ }, 1000);
28
+ });
29
+ }
30
+
31
+ function waitUntilVectorsIndexed(vectorDB: PineconeVector, indexName: string, expectedCount: number) {
32
+ return new Promise((resolve, reject) => {
33
+ const maxAttempts = 30; // 30 seconds max
34
+ let attempts = 0;
35
+ const interval = setInterval(async () => {
36
+ try {
37
+ const stats = await vectorDB.describeIndex(indexName);
38
+ if (stats && stats.count >= expectedCount) {
39
+ clearInterval(interval);
40
+ resolve(true);
41
+ }
42
+ attempts++;
43
+ if (attempts >= maxAttempts) {
44
+ clearInterval(interval);
45
+ reject(new Error('Timeout waiting for vectors to be indexed'));
46
+ }
47
+ } catch (error) {
48
+ console.log(error);
49
+ }
50
+ }, 1000);
51
+ });
52
+ }
53
+ // TODO: our pinecone account is over the limit, tests don't work in CI
54
+ describe.skip('PineconeVector Integration Tests', () => {
55
+ let vectorDB: PineconeVector;
56
+ const testIndexName = 'test-index-' + Date.now(); // Unique index name for each test run
57
+ const dimension = 3;
58
+
59
+ beforeAll(async () => {
60
+ vectorDB = new PineconeVector(PINECONE_API_KEY);
61
+ // Create test index
62
+ await vectorDB.createIndex(testIndexName, dimension);
63
+ await waitUntilReady(vectorDB, testIndexName);
64
+ }, 500000);
65
+
66
+ afterAll(async () => {
67
+ // Cleanup: delete test index
68
+ await vectorDB.deleteIndex(testIndexName);
69
+ }, 500000);
70
+
71
+ describe('Index Operations', () => {
72
+ it('should list indexes including our test index', async () => {
73
+ const indexes = await vectorDB.listIndexes();
74
+ expect(indexes).toContain(testIndexName);
75
+ }, 500000);
76
+
77
+ it('should describe index with correct properties', async () => {
78
+ const stats = await vectorDB.describeIndex(testIndexName);
79
+ expect(stats.dimension).toBe(dimension);
80
+ expect(stats.metric).toBe('cosine');
81
+ expect(typeof stats.count).toBe('number');
82
+ }, 500000);
83
+ });
84
+
85
+ describe('Vector Operations', () => {
86
+ const testVectors = [
87
+ [1.0, 0.0, 0.0],
88
+ [0.0, 1.0, 0.0],
89
+ [0.0, 0.0, 1.0],
90
+ ];
91
+ const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
92
+ let vectorIds: string[];
93
+
94
+ it('should upsert vectors with metadata', async () => {
95
+ vectorIds = await vectorDB.upsert(testIndexName, testVectors, testMetadata);
96
+ expect(vectorIds).toHaveLength(3);
97
+ // Wait for vectors to be indexed
98
+ await waitUntilVectorsIndexed(vectorDB, testIndexName, 3);
99
+ }, 500000);
100
+
101
+ it.skip('should query vectors and return nearest neighbors', async () => {
102
+ const queryVector = [1.0, 0.1, 0.1];
103
+ const results = await vectorDB.query(testIndexName, queryVector, 3);
104
+
105
+ expect(results).toHaveLength(3);
106
+ expect(results[0]!.score).toBeGreaterThan(0);
107
+ expect(results[0]!.metadata).toBeDefined();
108
+ }, 500000);
109
+
110
+ it('should query vectors with metadata filter', async () => {
111
+ const queryVector = [0.0, 1.0, 0.0];
112
+ const filter = { label: 'y-axis' };
113
+
114
+ const results = await vectorDB.query(testIndexName, queryVector, 1, filter);
115
+
116
+ expect(results).toHaveLength(1);
117
+ expect(results?.[0]?.metadata?.label).toBe('y-axis');
118
+ }, 500000);
119
+
120
+ it('should query vectors and return vectors in results', async () => {
121
+ const queryVector = [0.0, 1.0, 0.0];
122
+ const results = await vectorDB.query(testIndexName, queryVector, 1, undefined, true);
123
+
124
+ expect(results).toHaveLength(1);
125
+ expect(results?.[0]?.vector).toBeDefined();
126
+ expect(results?.[0]?.vector).toHaveLength(dimension);
127
+ }, 500000);
128
+ });
129
+
130
+ describe('Error Handling', () => {
131
+ it('should handle non-existent index query gracefully', async () => {
132
+ const nonExistentIndex = 'non-existent-index';
133
+ await expect(vectorDB.query(nonExistentIndex, [1, 0, 0])).rejects.toThrow();
134
+ }, 500000);
135
+
136
+ it('should handle incorrect dimension vectors', async () => {
137
+ const wrongDimVector = [[1, 0]]; // 2D vector for 3D index
138
+ await expect(vectorDB.upsert(testIndexName, wrongDimVector)).rejects.toThrow();
139
+ }, 500000);
140
+ });
141
+
142
+ describe('Performance Tests', () => {
143
+ it('should handle batch upsert of 1000 vectors', async () => {
144
+ const batchSize = 1000;
145
+ const vectors = Array(batchSize)
146
+ .fill(null)
147
+ .map(() =>
148
+ Array(dimension)
149
+ .fill(null)
150
+ .map(() => Math.random()),
151
+ );
152
+ const metadata = vectors.map((_, i) => ({ id: i }));
153
+
154
+ const start = Date.now();
155
+ const ids = await vectorDB.upsert(testIndexName, vectors, metadata);
156
+ const duration = Date.now() - start;
157
+
158
+ expect(ids).toHaveLength(batchSize);
159
+ console.log(`Batch upsert of ${batchSize} vectors took ${duration}ms`);
160
+ }, 300000); // 5 minute timeout
161
+
162
+ it('should perform multiple concurrent queries', async () => {
163
+ const queryVector = [1, 0, 0];
164
+ const numQueries = 10;
165
+
166
+ const start = Date.now();
167
+ const promises = Array(numQueries)
168
+ .fill(null)
169
+ .map(() => vectorDB.query(testIndexName, queryVector));
170
+
171
+ const results = await Promise.all(promises);
172
+ const duration = Date.now() - start;
173
+
174
+ expect(results).toHaveLength(numQueries);
175
+ console.log(`${numQueries} concurrent queries took ${duration}ms`);
176
+ }, 500000);
177
+ });
178
+
179
+ describe('Filter Validation in Queries', () => {
180
+ it('rejects queries with null values', async () => {
181
+ await expect(
182
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
183
+ field: null,
184
+ }),
185
+ ).rejects.toThrow();
186
+
187
+ await expect(
188
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
189
+ other: { $eq: null },
190
+ }),
191
+ ).rejects.toThrow('the $eq operator must be followed by a string, boolean or a number, got null instead');
192
+ });
193
+
194
+ it('rejects invalid array operator values', async () => {
195
+ // Test non-undefined values
196
+ const invalidValues = [123, 'string', true, { key: 'value' }, null];
197
+ for (const op of ['$in', '$nin']) {
198
+ for (const val of invalidValues) {
199
+ await expect(
200
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
201
+ field: { [op]: val },
202
+ }),
203
+ ).rejects.toThrow(`the ${op} operator must be followed by a list of strings or a list of numbers`);
204
+ }
205
+ }
206
+ });
207
+
208
+ it('validates comparison operators', async () => {
209
+ const numOps = ['$gt', '$gte', '$lt', '$lte'];
210
+ const invalidNumericValues = ['not-a-number', true, [], {}, null]; // Removed undefined
211
+ for (const op of numOps) {
212
+ for (const val of invalidNumericValues) {
213
+ await expect(
214
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
215
+ field: { [op]: val },
216
+ }),
217
+ ).rejects.toThrow(`the ${op} operator must be followed by a number`);
218
+ }
219
+ }
220
+ });
221
+
222
+ it('rejects multiple invalid values', async () => {
223
+ await expect(
224
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
225
+ field1: { $in: 'not-array' },
226
+ field2: { $exists: 'not-boolean' },
227
+ field3: { $gt: 'not-number' },
228
+ }),
229
+ ).rejects.toThrow();
230
+ });
231
+
232
+ it('rejects invalid array values', async () => {
233
+ await expect(
234
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
235
+ field: { $in: [null] },
236
+ }),
237
+ ).rejects.toThrow('the $in operator must be followed by a list of strings or a list of numbers');
238
+
239
+ await expect(
240
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
241
+ field: { $in: [undefined] },
242
+ }),
243
+ ).rejects.toThrow('the $in operator must be followed by a list of strings or a list of numbers');
244
+
245
+ await expect(
246
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
247
+ field: { $all: 'not-an-array' },
248
+ }),
249
+ ).rejects.toThrow('A non-empty array is required for the $all operator');
250
+ });
251
+
252
+ it('handles empty object filters', async () => {
253
+ // Test empty object at top level
254
+ await expect(vectorDB.query(testIndexName, [1, 0, 0], 10, { field: { $eq: {} } })).rejects.toThrow(
255
+ 'the $eq operator must be followed by a string, boolean or a number, got {} instead',
256
+ );
257
+ });
258
+
259
+ it('handles empty/undefined filters by returning all results', async () => {
260
+ // Empty objects and undefined are ignored by Pinecone
261
+ // and will return all results without filtering
262
+ const noFilterCases = [{ field: {} }, { field: undefined }, { field: { $in: undefined } }];
263
+
264
+ for (const filter of noFilterCases) {
265
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, filter);
266
+ expect(results.length).toBeGreaterThan(0);
267
+ }
268
+ });
269
+ it('handles empty object filters', async () => {
270
+ // Test empty object at top level
271
+ await expect(vectorDB.query(testIndexName, [1, 0, 0], 10, {})).rejects.toThrow(
272
+ 'You must enter a `filter` object with at least one key-value pair.',
273
+ );
274
+ });
275
+ });
276
+
277
+ describe('Metadata Filter Tests', () => {
278
+ const testVectors = [
279
+ [1.0, 0.0, 0.0],
280
+ [0.0, 1.0, 0.0],
281
+ [0.0, 0.0, 1.0],
282
+ [0.5, 0.5, 0.0],
283
+ [0.3, 0.3, 0.3],
284
+ [0.8, 0.1, 0.1],
285
+ [0.1, 0.8, 0.1],
286
+ [0.1, 0.1, 0.8],
287
+ ];
288
+
289
+ const testMetadata = [
290
+ { category: 'electronics', price: 1000, tags: ['premium', 'new'], inStock: true, rating: 4.5 },
291
+ { category: 'books', price: 50, tags: ['bestseller'], inStock: true, rating: 4.8 },
292
+ { category: 'electronics', price: 500, tags: ['refurbished'], inStock: false, rating: 4.0 },
293
+ { category: 'clothing', price: 75, tags: ['summer', 'sale'], inStock: true, rating: 4.2 },
294
+ { category: 'books', price: 30, tags: ['paperback', 'sale'], inStock: true, rating: 4.1 },
295
+ { category: 'electronics', price: 800, tags: ['premium'], inStock: true, rating: 4.7 },
296
+ { category: 'clothing', price: 150, tags: ['premium', 'new'], inStock: false, rating: 4.4 },
297
+ { category: 'books', price: 25, tags: ['paperback', 'bestseller'], inStock: true, rating: 4.3 },
298
+ ];
299
+
300
+ beforeAll(async () => {
301
+ await vectorDB.upsert(testIndexName, testVectors, testMetadata);
302
+ // Wait for vectors to be indexed
303
+ await waitUntilVectorsIndexed(vectorDB, testIndexName, testVectors.length);
304
+ }, 500000);
305
+
306
+ describe('Comparison Operators', () => {
307
+ it('should filter with implict $eq', async () => {
308
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
309
+ category: 'electronics',
310
+ });
311
+ expect(results.length).toBeGreaterThan(0);
312
+ results.forEach(result => {
313
+ expect(result.metadata?.category).toBe('electronics');
314
+ });
315
+ });
316
+ it('should filter with $eq operator', async () => {
317
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
318
+ category: { $eq: 'electronics' },
319
+ });
320
+ expect(results.length).toBeGreaterThan(0);
321
+ results.forEach(result => {
322
+ expect(result.metadata?.category).toBe('electronics');
323
+ });
324
+ });
325
+
326
+ it('should filter with $gt operator', async () => {
327
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
328
+ price: { $gt: 500 },
329
+ });
330
+ expect(results.length).toBeGreaterThan(0);
331
+ results.forEach(result => {
332
+ expect(Number(result.metadata?.price)).toBeGreaterThan(500);
333
+ });
334
+ });
335
+
336
+ it('should filter with $gte operator', async () => {
337
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
338
+ price: { $gte: 500 },
339
+ });
340
+ expect(results.length).toBeGreaterThan(0);
341
+ results.forEach(result => {
342
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
343
+ });
344
+ });
345
+
346
+ it('should filter with $lt operator', async () => {
347
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
348
+ price: { $lt: 100 },
349
+ });
350
+ expect(results.length).toBeGreaterThan(0);
351
+ results.forEach(result => {
352
+ expect(Number(result.metadata?.price)).toBeLessThan(100);
353
+ });
354
+ });
355
+
356
+ it('should filter with $lte operator', async () => {
357
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
358
+ price: { $lte: 50 },
359
+ });
360
+ expect(results.length).toBeGreaterThan(0);
361
+ results.forEach(result => {
362
+ expect(Number(result.metadata?.price)).toBeLessThanOrEqual(50);
363
+ });
364
+ });
365
+
366
+ it('should filter with $ne operator', async () => {
367
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
368
+ category: { $ne: 'electronics' },
369
+ });
370
+ expect(results.length).toBeGreaterThan(0);
371
+ results.forEach(result => {
372
+ expect(result.metadata?.category).not.toBe('electronics');
373
+ });
374
+ });
375
+
376
+ it('filters with $gte, $lt, $lte operators', async () => {
377
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
378
+ price: { $gte: 25, $lte: 30 },
379
+ });
380
+ expect(results.length).toBe(2);
381
+ results.forEach(result => {
382
+ expect(Number(result.metadata?.price)).toBeLessThanOrEqual(30);
383
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(25);
384
+ });
385
+ });
386
+ });
387
+
388
+ describe('Array Operators', () => {
389
+ it('should filter with $in operator for strings', async () => {
390
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
391
+ category: { $in: ['electronics', 'books'] },
392
+ });
393
+ expect(results.length).toBeGreaterThan(0);
394
+ results.forEach(result => {
395
+ expect(['electronics', 'books']).toContain(result.metadata?.category);
396
+ });
397
+ });
398
+
399
+ it('should filter with $in operator for numbers', async () => {
400
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
401
+ price: { $in: [50, 75, 1000] },
402
+ });
403
+ expect(results.length).toBeGreaterThan(0);
404
+ results.forEach(result => {
405
+ expect([50, 75, 1000]).toContain(result.metadata?.price);
406
+ });
407
+ });
408
+
409
+ it('should filter with $nin operator', async () => {
410
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
411
+ category: { $nin: ['electronics', 'books'] },
412
+ });
413
+ expect(results.length).toBeGreaterThan(0);
414
+ results.forEach(result => {
415
+ expect(['electronics', 'books']).not.toContain(result.metadata?.category);
416
+ });
417
+ });
418
+
419
+ it('should filter with $all operator', async () => {
420
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
421
+ tags: { $all: ['premium', 'new'] },
422
+ });
423
+ expect(results.length).toBeGreaterThan(0);
424
+ results.forEach(result => {
425
+ expect(result.metadata?.tags).toContain('premium');
426
+ expect(result.metadata?.tags).toContain('new');
427
+ });
428
+ });
429
+ });
430
+
431
+ describe('Logical Operators', () => {
432
+ it('should filter with implict $and', async () => {
433
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
434
+ category: 'electronics',
435
+ price: { $gt: 700 },
436
+ inStock: true,
437
+ });
438
+ expect(results.length).toBeGreaterThan(0);
439
+ results.forEach(result => {
440
+ expect(result.metadata?.category).toBe('electronics');
441
+ expect(Number(result.metadata?.price)).toBeGreaterThan(700);
442
+ expect(result.metadata?.inStock).toBe(true);
443
+ });
444
+ });
445
+ it('should filter with $and operator', async () => {
446
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
447
+ $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { inStock: true }],
448
+ });
449
+ expect(results.length).toBeGreaterThan(0);
450
+ results.forEach(result => {
451
+ expect(result.metadata?.category).toBe('electronics');
452
+ expect(Number(result.metadata?.price)).toBeGreaterThan(700);
453
+ expect(result.metadata?.inStock).toBe(true);
454
+ });
455
+ });
456
+
457
+ it('should filter with $or operator', async () => {
458
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
459
+ $or: [{ price: { $gt: 900 } }, { tags: { $all: ['bestseller'] } }],
460
+ });
461
+ expect(results.length).toBeGreaterThan(0);
462
+ results.forEach(result => {
463
+ const condition1 = Number(result.metadata?.price) > 900;
464
+ const condition2 = result.metadata?.tags?.includes('bestseller');
465
+ expect(condition1 || condition2).toBe(true);
466
+ });
467
+ });
468
+
469
+ it('should handle nested logical operators', async () => {
470
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
471
+ $and: [
472
+ {
473
+ $or: [{ category: 'electronics' }, { category: 'books' }],
474
+ },
475
+ { price: { $lt: 100 } },
476
+ { inStock: true },
477
+ ],
478
+ });
479
+ expect(results.length).toBeGreaterThan(0);
480
+ results.forEach(result => {
481
+ expect(['electronics', 'books']).toContain(result.metadata?.category);
482
+ expect(Number(result.metadata?.price)).toBeLessThan(100);
483
+ expect(result.metadata?.inStock).toBe(true);
484
+ });
485
+ });
486
+ });
487
+
488
+ describe('Complex Filter Combinations', () => {
489
+ it('should combine comparison and array operators', async () => {
490
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
491
+ $and: [{ price: { $gte: 500 } }, { tags: { $in: ['premium', 'refurbished'] } }],
492
+ });
493
+ expect(results.length).toBeGreaterThan(0);
494
+ results.forEach(result => {
495
+ expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
496
+ expect(result.metadata?.tags?.some(tag => ['premium', 'refurbished'].includes(tag))).toBe(true);
497
+ });
498
+ });
499
+
500
+ it('should handle multiple conditions on same field', async () => {
501
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
502
+ $and: [{ price: { $gte: 30 } }, { price: { $lte: 800 } }],
503
+ });
504
+ expect(results.length).toBeGreaterThan(0);
505
+ results.forEach(result => {
506
+ const price = Number(result.metadata?.price);
507
+ expect(price).toBeGreaterThanOrEqual(30);
508
+ expect(price).toBeLessThanOrEqual(800);
509
+ });
510
+ });
511
+
512
+ it('should handle complex nested conditions', async () => {
513
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
514
+ $or: [
515
+ {
516
+ $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { tags: { $all: ['premium'] } }],
517
+ },
518
+ {
519
+ $and: [{ category: 'books' }, { price: { $lt: 50 } }, { tags: { $in: ['paperback'] } }],
520
+ },
521
+ ],
522
+ });
523
+ expect(results.length).toBeGreaterThan(0);
524
+ results.forEach(result => {
525
+ const isExpensiveElectronics =
526
+ result.metadata?.category === 'electronics' &&
527
+ Number(result.metadata?.price) > 700 &&
528
+ result.metadata?.tags?.includes('premium');
529
+
530
+ const isCheapBook =
531
+ result.metadata?.category === 'books' &&
532
+ Number(result.metadata?.price) < 50 &&
533
+ result.metadata?.tags?.includes('paperback');
534
+
535
+ expect(isExpensiveElectronics || isCheapBook).toBe(true);
536
+ });
537
+ });
538
+
539
+ it('combines existence checks with other operators', async () => {
540
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
541
+ $and: [{ category: 'clothing' }, { optionalField: { $exists: false } }],
542
+ });
543
+ expect(results.length).toBe(2);
544
+ expect(results[0]!.metadata!.category).toBe('clothing');
545
+ expect('optionalField' in results[0]!.metadata!).toBe(false);
546
+ });
547
+ });
548
+
549
+ describe('Edge Cases', () => {
550
+ it('should handle numeric comparisons with decimals', async () => {
551
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
552
+ rating: { $gt: 4.5 },
553
+ });
554
+ expect(results.length).toBeGreaterThan(0);
555
+ results.forEach(result => {
556
+ expect(Number(result.metadata?.rating)).toBeGreaterThan(4.5);
557
+ });
558
+ });
559
+
560
+ it('should handle boolean values', async () => {
561
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
562
+ inStock: { $eq: false },
563
+ });
564
+ expect(results.length).toBeGreaterThan(0);
565
+ results.forEach(result => {
566
+ expect(result.metadata?.inStock).toBe(false);
567
+ });
568
+ });
569
+
570
+ it('should handle empty array in $in operator', async () => {
571
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
572
+ category: { $in: [] },
573
+ });
574
+ expect(results).toHaveLength(0);
575
+ });
576
+
577
+ it('should handle single value in $all operator', async () => {
578
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
579
+ tags: { $all: ['premium'] },
580
+ });
581
+ expect(results.length).toBeGreaterThan(0);
582
+ results.forEach(result => {
583
+ expect(result.metadata?.tags).toContain('premium');
584
+ });
585
+ });
586
+ });
587
+ });
588
+
589
+ describe('Additional Validation Tests', () => {
590
+ it('should reject non-numeric values in numeric comparisons', async () => {
591
+ await expect(
592
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
593
+ price: { $gt: '500' }, // string instead of number
594
+ }),
595
+ ).rejects.toThrow('the $gt operator must be followed by a number');
596
+ });
597
+
598
+ it('should reject invalid types in $in operator', async () => {
599
+ await expect(
600
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
601
+ price: { $in: [true, false] }, // booleans instead of numbers
602
+ }),
603
+ ).rejects.toThrow('the $in operator must be followed by a list of strings or a list of numbers');
604
+ });
605
+
606
+ it('should reject mixed types in $in operator', async () => {
607
+ await expect(
608
+ vectorDB.query(testIndexName, [1, 0, 0], 10, {
609
+ field: { $in: ['string', 123] }, // mixed string and number
610
+ }),
611
+ ).rejects.toThrow();
612
+ });
613
+ it('should handle undefined filter', async () => {
614
+ const results1 = await vectorDB.query(testIndexName, [1, 0, 0], 10, undefined);
615
+ const results2 = await vectorDB.query(testIndexName, [1, 0, 0], 10);
616
+ expect(results1).toEqual(results2);
617
+ expect(results1.length).toBeGreaterThan(0);
618
+ });
619
+
620
+ it('should handle null filter', async () => {
621
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, null as any);
622
+ const results2 = await vectorDB.query(testIndexName, [1, 0, 0], 10);
623
+ expect(results).toEqual(results2);
624
+ expect(results.length).toBeGreaterThan(0);
625
+ });
626
+ });
627
+
628
+ describe('Additional Edge Cases', () => {
629
+ it('should handle exact boundary conditions', async () => {
630
+ // Test exact boundary values from our test data
631
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
632
+ $and: [
633
+ { price: { $gte: 25 } }, // lowest price in our data
634
+ { price: { $lte: 1000 } }, // highest price in our data
635
+ ],
636
+ });
637
+ expect(results.length).toBeGreaterThan(0);
638
+ // Should include both boundary values
639
+ expect(results.some(r => r.metadata?.price === 25)).toBe(true);
640
+ expect(results.some(r => r.metadata?.price === 1000)).toBe(true);
641
+ });
642
+
643
+ it('should handle multiple $all conditions on same array field', async () => {
644
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
645
+ $and: [{ tags: { $all: ['premium'] } }, { tags: { $all: ['new'] } }],
646
+ });
647
+ expect(results.length).toBeGreaterThan(0);
648
+ results.forEach(result => {
649
+ expect(result.metadata?.tags).toContain('premium');
650
+ expect(result.metadata?.tags).toContain('new');
651
+ });
652
+ });
653
+
654
+ it('should handle multiple array operator combinations', async () => {
655
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
656
+ $and: [{ tags: { $all: ['premium'] } }, { tags: { $in: ['new', 'refurbished'] } }],
657
+ });
658
+ expect(results.length).toBeGreaterThan(0);
659
+ results.forEach(result => {
660
+ expect(result.metadata?.tags).toContain('premium');
661
+ expect(result.metadata?.tags?.some(tag => ['new', 'refurbished'].includes(tag))).toBe(true);
662
+ });
663
+ });
664
+ });
665
+
666
+ describe('Additional Complex Logical Combinations', () => {
667
+ it('should handle deeply nested $or conditions', async () => {
668
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
669
+ $or: [
670
+ {
671
+ $and: [{ category: 'electronics' }, { $or: [{ price: { $gt: 900 } }, { tags: { $all: ['premium'] } }] }],
672
+ },
673
+ {
674
+ $and: [{ category: 'books' }, { $or: [{ price: { $lt: 30 } }, { tags: { $all: ['bestseller'] } }] }],
675
+ },
676
+ ],
677
+ });
678
+ expect(results.length).toBeGreaterThan(0);
679
+ results.forEach(result => {
680
+ if (result.metadata?.category === 'electronics') {
681
+ expect(Number(result.metadata?.price) > 900 || result.metadata?.tags?.includes('premium')).toBe(true);
682
+ } else if (result.metadata?.category === 'books') {
683
+ expect(Number(result.metadata?.price) < 30 || result.metadata?.tags?.includes('bestseller')).toBe(true);
684
+ }
685
+ });
686
+ });
687
+
688
+ it('should handle multiple field comparisons with same value', async () => {
689
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
690
+ $or: [{ price: { $gt: 500 } }, { rating: { $gt: 4.5 } }],
691
+ });
692
+ expect(results.length).toBeGreaterThan(0);
693
+ results.forEach(result => {
694
+ expect(Number(result.metadata?.price) > 500 || Number(result.metadata?.rating) > 4.5).toBe(true);
695
+ });
696
+ });
697
+
698
+ it('should handle combination of array and numeric comparisons', async () => {
699
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
700
+ $and: [
701
+ { tags: { $in: ['premium', 'bestseller'] } },
702
+ { $or: [{ price: { $gt: 500 } }, { rating: { $gt: 4.5 } }] },
703
+ ],
704
+ });
705
+ expect(results.length).toBeGreaterThan(0);
706
+ results.forEach(result => {
707
+ expect(['premium', 'bestseller'].some(tag => result.metadata?.tags?.includes(tag))).toBe(true);
708
+ expect(Number(result.metadata?.price) > 500 || Number(result.metadata?.rating) > 4.5).toBe(true);
709
+ });
710
+ });
711
+ });
712
+
713
+ describe('Performance Edge Cases', () => {
714
+ it('should handle filters with many conditions', async () => {
715
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
716
+ $and: Array(10)
717
+ .fill(null)
718
+ .map(() => ({
719
+ $or: [{ price: { $gt: 100 } }, { rating: { $gt: 4.0 } }],
720
+ })),
721
+ });
722
+ expect(results.length).toBeGreaterThan(0);
723
+ results.forEach(result => {
724
+ expect(Number(result.metadata?.price) > 100 || Number(result.metadata?.rating) > 4.0).toBe(true);
725
+ });
726
+ });
727
+
728
+ it('should handle deeply nested conditions efficiently', async () => {
729
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
730
+ $or: Array(5)
731
+ .fill(null)
732
+ .map(() => ({
733
+ $and: [{ category: { $in: ['electronics', 'books'] } }, { price: { $gt: 50 } }, { rating: { $gt: 4.0 } }],
734
+ })),
735
+ });
736
+ expect(results.length).toBeGreaterThan(0);
737
+ results.forEach(result => {
738
+ expect(['electronics', 'books']).toContain(result.metadata?.category);
739
+ expect(Number(result.metadata?.price)).toBeGreaterThan(50);
740
+ expect(Number(result.metadata?.rating)).toBeGreaterThan(4.0);
741
+ });
742
+ });
743
+
744
+ it('should handle large number of $or conditions', async () => {
745
+ const results = await vectorDB.query(testIndexName, [1, 0, 0], 10, {
746
+ $or: [
747
+ ...Array(5)
748
+ .fill(null)
749
+ .map((_, i) => ({
750
+ price: { $gt: i * 100 },
751
+ })),
752
+ ...Array(5)
753
+ .fill(null)
754
+ .map((_, i) => ({
755
+ rating: { $gt: 4.0 + i * 0.1 },
756
+ })),
757
+ ],
758
+ });
759
+ expect(results.length).toBeGreaterThan(0);
760
+ });
761
+ });
762
+ });