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