@mastra/opensearch 0.11.7 → 0.11.8-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 +9 -0
- package/package.json +17 -4
- package/.turbo/turbo-build.log +0 -4
- package/docker-compose.yaml +0 -23
- package/eslint.config.js +0 -6
- package/src/index.ts +0 -1
- package/src/vector/filter.test.ts +0 -661
- package/src/vector/filter.ts +0 -479
- package/src/vector/index.test.ts +0 -1558
- package/src/vector/index.ts +0 -436
- package/src/vector/prompt.ts +0 -82
- 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,1558 +0,0 @@
|
|
|
1
|
-
// To setup a Opensearch server, run the docker compose file in the opensearch directory
|
|
2
|
-
import type { QueryResult } from '@mastra/core/vector';
|
|
3
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import { OpenSearchVector } from './index';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Helper function to check if two vectors are similar (cosine similarity close to 1)
|
|
9
|
-
* This is needed because OpenSearch may normalize vectors when using cosine similarity
|
|
10
|
-
*/
|
|
11
|
-
function areVectorsSimilar(v1: number[] | undefined, v2: number[] | undefined, threshold = 0.99): boolean {
|
|
12
|
-
if (!v1 || !v2 || v1.length !== v2.length) return false;
|
|
13
|
-
|
|
14
|
-
// Calculate cosine similarity
|
|
15
|
-
let dotProduct = 0;
|
|
16
|
-
let mag1 = 0;
|
|
17
|
-
let mag2 = 0;
|
|
18
|
-
|
|
19
|
-
for (let i = 0; i < v1.length; i++) {
|
|
20
|
-
dotProduct += v1[i] * v2[i];
|
|
21
|
-
mag1 += v1[i] * v1[i];
|
|
22
|
-
mag2 += v2[i] * v2[i];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
mag1 = Math.sqrt(mag1);
|
|
26
|
-
mag2 = Math.sqrt(mag2);
|
|
27
|
-
|
|
28
|
-
if (mag1 === 0 || mag2 === 0) return false;
|
|
29
|
-
|
|
30
|
-
const similarity = dotProduct / (mag1 * mag2);
|
|
31
|
-
return similarity >= threshold;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('OpenSearchVector', () => {
|
|
35
|
-
let vectorDB: OpenSearchVector;
|
|
36
|
-
const url = 'http://localhost:9200';
|
|
37
|
-
const testIndexName = 'test-index-' + Date.now();
|
|
38
|
-
const testIndexName2 = 'test-index2-' + Date.now();
|
|
39
|
-
|
|
40
|
-
beforeAll(async () => {
|
|
41
|
-
// Initialize PgVector
|
|
42
|
-
vectorDB = new OpenSearchVector({ url });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
afterAll(async () => {
|
|
46
|
-
// Clean up test tables
|
|
47
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Index Management Tests
|
|
51
|
-
describe('Index Management', () => {
|
|
52
|
-
describe('createIndex', () => {
|
|
53
|
-
afterAll(async () => {
|
|
54
|
-
await vectorDB.deleteIndex({ indexName: testIndexName2 });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should create a new vector table with specified dimensions', async () => {
|
|
58
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
59
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName });
|
|
60
|
-
expect(stats?.dimension).toBe(3);
|
|
61
|
-
expect(stats?.count).toBe(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should create index with specified metric', async () => {
|
|
65
|
-
await vectorDB.createIndex({ indexName: testIndexName2, dimension: 3, metric: 'euclidean' });
|
|
66
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName2 });
|
|
67
|
-
expect(stats.metric).toBe('euclidean');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should throw error if dimension is invalid', async () => {
|
|
71
|
-
await expect(vectorDB.createIndex({ indexName: 'testIndexNameFail', dimension: 0 })).rejects.toThrow();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('metrics', () => {
|
|
76
|
-
const testIndex = 'test_metric';
|
|
77
|
-
afterEach(async () => {
|
|
78
|
-
await vectorDB.deleteIndex({ indexName: testIndex });
|
|
79
|
-
});
|
|
80
|
-
it('should create index with cosine metric', async () => {
|
|
81
|
-
await vectorDB.createIndex({
|
|
82
|
-
indexName: testIndex,
|
|
83
|
-
dimension: 3,
|
|
84
|
-
metric: 'cosine',
|
|
85
|
-
});
|
|
86
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndex });
|
|
87
|
-
expect(stats.metric).toBe('cosine');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should create index with euclidean metric', async () => {
|
|
91
|
-
await vectorDB.createIndex({
|
|
92
|
-
indexName: testIndex,
|
|
93
|
-
dimension: 3,
|
|
94
|
-
metric: 'euclidean',
|
|
95
|
-
});
|
|
96
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndex });
|
|
97
|
-
expect(stats.metric).toBe('euclidean');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should create index with dotproduct', async () => {
|
|
101
|
-
await vectorDB.createIndex({
|
|
102
|
-
indexName: testIndex,
|
|
103
|
-
dimension: 3,
|
|
104
|
-
metric: 'dotproduct',
|
|
105
|
-
});
|
|
106
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndex });
|
|
107
|
-
expect(stats.metric).toBe('dotproduct');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('listIndexes', () => {
|
|
112
|
-
const indexName = 'test_query_3';
|
|
113
|
-
beforeAll(async () => {
|
|
114
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
afterAll(async () => {
|
|
118
|
-
await vectorDB.deleteIndex({ indexName });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should list all vector tables', async () => {
|
|
122
|
-
const indexes = await vectorDB.listIndexes();
|
|
123
|
-
expect(indexes).toContain(indexName);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should not return created index in list if it is deleted', async () => {
|
|
127
|
-
await vectorDB.deleteIndex({ indexName });
|
|
128
|
-
const indexes = await vectorDB.listIndexes();
|
|
129
|
-
expect(indexes).not.toContain(indexName);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('describeIndex', () => {
|
|
134
|
-
const indexName = 'test_query_4';
|
|
135
|
-
beforeAll(async () => {
|
|
136
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
afterAll(async () => {
|
|
140
|
-
await vectorDB.deleteIndex({ indexName });
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should return correct index stats', async () => {
|
|
144
|
-
const vectors = [
|
|
145
|
-
[1, 2, 3],
|
|
146
|
-
[4, 5, 6],
|
|
147
|
-
];
|
|
148
|
-
await vectorDB.upsert({ indexName, vectors });
|
|
149
|
-
|
|
150
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
151
|
-
expect(stats).toEqual({
|
|
152
|
-
dimension: 3,
|
|
153
|
-
count: 2,
|
|
154
|
-
metric: 'cosine',
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should throw error for non-existent index', async () => {
|
|
159
|
-
await expect(vectorDB.describeIndex({ indexName: 'non_existent' })).rejects.toThrow();
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Verify basic index creation and deletion
|
|
164
|
-
describe('Basic Index Operations', () => {
|
|
165
|
-
const testIndexName = 'basic-query';
|
|
166
|
-
it('should create an index and verify its existence', async () => {
|
|
167
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 1536 });
|
|
168
|
-
|
|
169
|
-
const indexes = await vectorDB.listIndexes();
|
|
170
|
-
expect(indexes).toContain(testIndexName);
|
|
171
|
-
|
|
172
|
-
// Delete the index after the test
|
|
173
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should throw an error if dimension is not a positive integer', async () => {
|
|
177
|
-
await expect(vectorDB.createIndex({ indexName: testIndexName, dimension: -1 })).rejects.toThrow(
|
|
178
|
-
'Dimension must be a positive integer',
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should delete an index and verify its deletion', async () => {
|
|
183
|
-
const deleteTestIndex = 'test-deletion-' + Date.now();
|
|
184
|
-
await vectorDB.createIndex({ indexName: deleteTestIndex, dimension: 1536 });
|
|
185
|
-
|
|
186
|
-
let indexes = await vectorDB.listIndexes();
|
|
187
|
-
expect(indexes).toContain(deleteTestIndex);
|
|
188
|
-
|
|
189
|
-
await vectorDB.deleteIndex({ indexName: deleteTestIndex });
|
|
190
|
-
|
|
191
|
-
indexes = await vectorDB.listIndexes();
|
|
192
|
-
expect(indexes).not.toContain(deleteTestIndex);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('Vector Operations', () => {
|
|
198
|
-
let testIndexName = 'test_vector';
|
|
199
|
-
beforeEach(async () => {
|
|
200
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
afterEach(async () => {
|
|
204
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('query', () => {
|
|
208
|
-
it('should query vectors and return nearest neighbors', async () => {
|
|
209
|
-
const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
|
|
210
|
-
const testVectors = [
|
|
211
|
-
[1.0, 0.0, 0.0],
|
|
212
|
-
[0.0, 1.0, 0.0],
|
|
213
|
-
[0.0, 0.0, 1.0],
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
217
|
-
|
|
218
|
-
const results = await vectorDB.query({
|
|
219
|
-
indexName: testIndexName,
|
|
220
|
-
queryVector: [1.0, 0.1, 0.1],
|
|
221
|
-
topK: 3,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
expect(results).toHaveLength(3);
|
|
225
|
-
expect(results[0]?.score).toBeGreaterThan(0);
|
|
226
|
-
expect(results[0]?.metadata).toBeDefined();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should query vectors and return vector in results', async () => {
|
|
230
|
-
const dimension = 3;
|
|
231
|
-
const queryVector = [1.0, 0.1, 0.1];
|
|
232
|
-
const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
|
|
233
|
-
const testVectors = [
|
|
234
|
-
[1.0, 0.0, 0.0],
|
|
235
|
-
[0.0, 1.0, 0.0],
|
|
236
|
-
[0.0, 0.0, 1.0],
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
240
|
-
|
|
241
|
-
const results = await vectorDB.query({
|
|
242
|
-
indexName: testIndexName,
|
|
243
|
-
queryVector,
|
|
244
|
-
topK: 3,
|
|
245
|
-
includeVector: true,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
expect(results).toHaveLength(3);
|
|
249
|
-
expect(results?.[0]?.vector).toBeDefined();
|
|
250
|
-
expect(results?.[0]?.vector).toHaveLength(dimension);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should query vectors with metadata filter - A', async () => {
|
|
254
|
-
const testMetadata = [
|
|
255
|
-
{ label: 'x-axis', num: 1 },
|
|
256
|
-
{ label: 'y-axis', num: 2 },
|
|
257
|
-
{ label: 'z-axis', num: 3 },
|
|
258
|
-
];
|
|
259
|
-
const testVectors = [
|
|
260
|
-
[0.0, 1.0, 0.0],
|
|
261
|
-
[0.0, 1.0, 0.0],
|
|
262
|
-
[0.0, 1.0, 0.0],
|
|
263
|
-
];
|
|
264
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
265
|
-
const queryFilter = { label: 'x-axis', num: 1 };
|
|
266
|
-
|
|
267
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
268
|
-
|
|
269
|
-
const results = await vectorDB.query({
|
|
270
|
-
indexName: testIndexName,
|
|
271
|
-
queryVector: queryVector,
|
|
272
|
-
filter: queryFilter,
|
|
273
|
-
topK: 10,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
expect(results).toHaveLength(1);
|
|
277
|
-
expect(results[0]?.metadata?.label).toBe('x-axis');
|
|
278
|
-
}, 50000);
|
|
279
|
-
|
|
280
|
-
it('should query vectors with metadata filter - B', async () => {
|
|
281
|
-
const testMetadata = [
|
|
282
|
-
{ label: 'x-axis', num: 1 },
|
|
283
|
-
{ label: 'y-axis', num: 2 },
|
|
284
|
-
{ label: 'z-axis', num: 3 },
|
|
285
|
-
];
|
|
286
|
-
const testVectors = [
|
|
287
|
-
[0.0, 1.0, 0.0],
|
|
288
|
-
[0.0, 1.0, 0.0],
|
|
289
|
-
[0.0, 1.0, 0.0],
|
|
290
|
-
];
|
|
291
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
292
|
-
const queryFilter = { label: 'x-axis', num: 2 };
|
|
293
|
-
|
|
294
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
295
|
-
|
|
296
|
-
const results = await vectorDB.query({
|
|
297
|
-
indexName: testIndexName,
|
|
298
|
-
queryVector: queryVector,
|
|
299
|
-
filter: queryFilter,
|
|
300
|
-
topK: 10,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
expect(results).toHaveLength(0);
|
|
304
|
-
}, 50000);
|
|
305
|
-
|
|
306
|
-
it('should query vectors with complex metadata filter - A', async () => {
|
|
307
|
-
const testMetadata = [
|
|
308
|
-
{ label: 'x-axis', num: 1 },
|
|
309
|
-
{ label: 'y-axis', num: 2 },
|
|
310
|
-
{ label: 'z-axis', num: 3 },
|
|
311
|
-
];
|
|
312
|
-
const testVectors = [
|
|
313
|
-
[0.0, 1.0, 0.0],
|
|
314
|
-
[0.0, 1.0, 0.0],
|
|
315
|
-
[0.0, 1.0, 0.0],
|
|
316
|
-
];
|
|
317
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
318
|
-
const queryFilter = {
|
|
319
|
-
$and: [{ label: 'y-axis' }, { num: { $gt: 1 } }],
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
323
|
-
|
|
324
|
-
const results = await vectorDB.query({
|
|
325
|
-
indexName: testIndexName,
|
|
326
|
-
queryVector: queryVector,
|
|
327
|
-
filter: queryFilter,
|
|
328
|
-
topK: 10,
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
expect(results).toHaveLength(1);
|
|
332
|
-
expect(results[0]?.metadata?.label).toBe('y-axis');
|
|
333
|
-
}, 50000);
|
|
334
|
-
|
|
335
|
-
it('should query vectors with complex metadata filter - B', async () => {
|
|
336
|
-
const testMetadata = [
|
|
337
|
-
{ label: 'x-axis', num: 1 },
|
|
338
|
-
{ label: 'y-axis', num: 2 },
|
|
339
|
-
{ label: 'z-axis', num: 3 },
|
|
340
|
-
];
|
|
341
|
-
const testVectors = [
|
|
342
|
-
[0.0, 1.0, 0.0],
|
|
343
|
-
[0.0, 1.0, 0.0],
|
|
344
|
-
[0.0, 1.0, 0.0],
|
|
345
|
-
];
|
|
346
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
347
|
-
const queryFilter = { $and: [{ label: 'x-axis' }, { num: { $gt: 1 } }] };
|
|
348
|
-
|
|
349
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
350
|
-
|
|
351
|
-
const results = await vectorDB.query({
|
|
352
|
-
indexName: testIndexName,
|
|
353
|
-
queryVector: queryVector,
|
|
354
|
-
filter: queryFilter,
|
|
355
|
-
topK: 10,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
expect(results).toHaveLength(0);
|
|
359
|
-
}, 50000);
|
|
360
|
-
|
|
361
|
-
it('should handle complex nested filters with multiple conditions', async () => {
|
|
362
|
-
const testMetadata = [
|
|
363
|
-
{
|
|
364
|
-
category: 'electronics',
|
|
365
|
-
price: 100,
|
|
366
|
-
tags: ['new', 'featured'],
|
|
367
|
-
specs: { color: 'black', weight: 500 },
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
category: 'electronics',
|
|
371
|
-
price: 200,
|
|
372
|
-
tags: ['used', 'sale'],
|
|
373
|
-
specs: { color: 'white', weight: 300 },
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
category: 'clothing',
|
|
377
|
-
price: 50,
|
|
378
|
-
tags: ['new', 'featured'],
|
|
379
|
-
specs: { color: 'blue', weight: 100 },
|
|
380
|
-
},
|
|
381
|
-
];
|
|
382
|
-
|
|
383
|
-
const testVectors = [
|
|
384
|
-
[1.0, 0.0, 0.0],
|
|
385
|
-
[0.0, 1.0, 0.0],
|
|
386
|
-
[0.0, 0.0, 1.0],
|
|
387
|
-
];
|
|
388
|
-
|
|
389
|
-
await vectorDB.upsert({
|
|
390
|
-
indexName: testIndexName,
|
|
391
|
-
vectors: testVectors,
|
|
392
|
-
metadata: testMetadata,
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const complexFilter = {
|
|
396
|
-
$and: [
|
|
397
|
-
{ category: 'electronics' },
|
|
398
|
-
{ price: { $gt: 150 } },
|
|
399
|
-
{ tags: { $in: ['sale', 'featured'] } },
|
|
400
|
-
{ 'specs.weight': { $lt: 400 } },
|
|
401
|
-
],
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
const results = await vectorDB.query({
|
|
405
|
-
indexName: testIndexName,
|
|
406
|
-
queryVector: [0.0, 1.0, 0.0],
|
|
407
|
-
filter: complexFilter,
|
|
408
|
-
topK: 10,
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
expect(results).toHaveLength(1);
|
|
412
|
-
expect(results[0]?.metadata).toEqual(testMetadata[1]);
|
|
413
|
-
}, 50000);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
describe('upsert', () => {
|
|
417
|
-
let testIndexName = 'test_vector_upsert';
|
|
418
|
-
beforeEach(async () => {
|
|
419
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
afterEach(async () => {
|
|
423
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('should insert new vectors', async () => {
|
|
427
|
-
const vectors = [
|
|
428
|
-
[1, 2, 3],
|
|
429
|
-
[4, 5, 6],
|
|
430
|
-
];
|
|
431
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors });
|
|
432
|
-
|
|
433
|
-
expect(ids).toHaveLength(2);
|
|
434
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName });
|
|
435
|
-
expect(stats.count).toBe(2);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('should update existing vectors', async () => {
|
|
439
|
-
const vectors = [[1, 2, 3]];
|
|
440
|
-
const metadata = [{ test: 'initial' }];
|
|
441
|
-
const [id] = await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
|
|
442
|
-
|
|
443
|
-
const updatedVectors = [[4, 5, 6]];
|
|
444
|
-
const updatedMetadata = [{ test: 'updated' }];
|
|
445
|
-
await vectorDB.upsert({
|
|
446
|
-
indexName: testIndexName,
|
|
447
|
-
vectors: updatedVectors,
|
|
448
|
-
metadata: updatedMetadata,
|
|
449
|
-
ids: [id!],
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector: [4, 5, 6], topK: 1 });
|
|
453
|
-
expect(results[0]?.id).toBe(id);
|
|
454
|
-
expect(results[0]?.metadata).toEqual({ test: 'updated' });
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it('should handle metadata correctly', async () => {
|
|
458
|
-
const vectors = [[1, 2, 3]];
|
|
459
|
-
const metadata = [{ test: 'value', num: 123 }];
|
|
460
|
-
|
|
461
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
|
|
462
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector: [1, 2, 3], topK: 1 });
|
|
463
|
-
|
|
464
|
-
expect(results[0]?.metadata).toEqual(metadata[0]);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('should throw an error if vector dimension does not match index dimension', async () => {
|
|
468
|
-
await expect(vectorDB.upsert({ indexName: testIndexName, vectors: [[1, 2, 3, 4]] })).rejects.toThrow(
|
|
469
|
-
'Vector dimension does not match index dimension',
|
|
470
|
-
);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
describe('updates', () => {
|
|
475
|
-
let testIndexName = 'test_vector_updates';
|
|
476
|
-
const testVectors = [
|
|
477
|
-
[1, 2, 3],
|
|
478
|
-
[4, 5, 6],
|
|
479
|
-
[7, 8, 9],
|
|
480
|
-
];
|
|
481
|
-
|
|
482
|
-
beforeEach(async () => {
|
|
483
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
afterEach(async () => {
|
|
487
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it('should update the vector by id', async () => {
|
|
491
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
492
|
-
expect(ids).toHaveLength(3);
|
|
493
|
-
|
|
494
|
-
const idToBeUpdated = ids[0];
|
|
495
|
-
const newVector = [1, 2, 3];
|
|
496
|
-
const newMetaData = {
|
|
497
|
-
test: 'updates',
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
const update = {
|
|
501
|
-
vector: newVector,
|
|
502
|
-
metadata: newMetaData,
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
506
|
-
|
|
507
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
508
|
-
indexName: testIndexName,
|
|
509
|
-
queryVector: newVector,
|
|
510
|
-
topK: 2,
|
|
511
|
-
includeVector: true,
|
|
512
|
-
});
|
|
513
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
514
|
-
// Check vector similarity instead of exact equality due to normalization
|
|
515
|
-
expect(areVectorsSimilar(results[0]?.vector, newVector)).toBe(true);
|
|
516
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it('should only update the metadata by id', async () => {
|
|
520
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
521
|
-
expect(ids).toHaveLength(3);
|
|
522
|
-
|
|
523
|
-
const idToBeUpdated = ids[0];
|
|
524
|
-
const newMetaData = {
|
|
525
|
-
test: 'updates',
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
const update = {
|
|
529
|
-
metadata: newMetaData,
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
533
|
-
|
|
534
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
535
|
-
indexName: testIndexName,
|
|
536
|
-
queryVector: testVectors[0],
|
|
537
|
-
topK: 2,
|
|
538
|
-
includeVector: true,
|
|
539
|
-
});
|
|
540
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
541
|
-
// Check vector similarity instead of exact equality due to normalization
|
|
542
|
-
expect(areVectorsSimilar(results[0]?.vector, testVectors[0])).toBe(true);
|
|
543
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it('should only update vector embeddings by id', async () => {
|
|
547
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
548
|
-
expect(ids).toHaveLength(3);
|
|
549
|
-
|
|
550
|
-
const idToBeUpdated = ids[0];
|
|
551
|
-
const newVector = [4, 4, 4];
|
|
552
|
-
|
|
553
|
-
const update = {
|
|
554
|
-
vector: newVector,
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
558
|
-
|
|
559
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
560
|
-
indexName: testIndexName,
|
|
561
|
-
queryVector: newVector,
|
|
562
|
-
topK: 2,
|
|
563
|
-
includeVector: true,
|
|
564
|
-
});
|
|
565
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
566
|
-
// Check vector similarity instead of exact equality due to normalization
|
|
567
|
-
expect(areVectorsSimilar(results[0]?.vector, newVector)).toBe(true);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('should throw exception when no updates are given', async () => {
|
|
571
|
-
await expect(vectorDB.updateVector({ indexName: testIndexName, id: 'id', update: {} })).rejects.toThrow(
|
|
572
|
-
'No updates provided',
|
|
573
|
-
);
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
describe('deletes', () => {
|
|
578
|
-
let testIndexName = 'test_vector_deletes';
|
|
579
|
-
const testVectors = [
|
|
580
|
-
[1, 2, 3],
|
|
581
|
-
[4, 5, 6],
|
|
582
|
-
[7, 8, 9],
|
|
583
|
-
];
|
|
584
|
-
|
|
585
|
-
beforeEach(async () => {
|
|
586
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
afterEach(async () => {
|
|
590
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
it('should delete the vector by id', async () => {
|
|
594
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
595
|
-
expect(ids).toHaveLength(3);
|
|
596
|
-
const idToBeDeleted = ids[0];
|
|
597
|
-
|
|
598
|
-
await vectorDB.deleteVector({ indexName: testIndexName, id: idToBeDeleted });
|
|
599
|
-
|
|
600
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
601
|
-
indexName: testIndexName,
|
|
602
|
-
queryVector: [1.0, 0.0, 0.0],
|
|
603
|
-
topK: 2,
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
expect(results).toHaveLength(2);
|
|
607
|
-
expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
// Advanced Query and Filter Tests
|
|
613
|
-
describe('Advanced Query and Filter Operations', () => {
|
|
614
|
-
const indexName = 'test_query_filters';
|
|
615
|
-
|
|
616
|
-
beforeEach(async () => {
|
|
617
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
618
|
-
const vectors = [
|
|
619
|
-
[1, 0.1, 0],
|
|
620
|
-
[0.9, 0.2, 0],
|
|
621
|
-
[0.95, 0.1, 0],
|
|
622
|
-
[0.85, 0.2, 0],
|
|
623
|
-
[0.9, 0.1, 0],
|
|
624
|
-
];
|
|
625
|
-
|
|
626
|
-
const metadata = [
|
|
627
|
-
{
|
|
628
|
-
category: 'electronics',
|
|
629
|
-
price: 100,
|
|
630
|
-
tags: ['new', 'premium'],
|
|
631
|
-
active: true,
|
|
632
|
-
ratings: [4.5, 4.8, 4.2], // Array of numbers
|
|
633
|
-
stock: [
|
|
634
|
-
{ location: 'A', count: 25 },
|
|
635
|
-
{ location: 'B', count: 15 },
|
|
636
|
-
], // Array of objects
|
|
637
|
-
reviews: [
|
|
638
|
-
{ user: 'alice', score: 5, verified: true },
|
|
639
|
-
{ user: 'bob', score: 4, verified: true },
|
|
640
|
-
{ user: 'charlie', score: 3, verified: false },
|
|
641
|
-
], // Complex array objects
|
|
642
|
-
},
|
|
643
|
-
{
|
|
644
|
-
category: 'books',
|
|
645
|
-
price: 50,
|
|
646
|
-
tags: ['used'],
|
|
647
|
-
active: true,
|
|
648
|
-
ratings: [3.8, 4.0, 4.1],
|
|
649
|
-
stock: [
|
|
650
|
-
{ location: 'A', count: 10 },
|
|
651
|
-
{ location: 'C', count: 30 },
|
|
652
|
-
],
|
|
653
|
-
reviews: [
|
|
654
|
-
{ user: 'dave', score: 4, verified: true },
|
|
655
|
-
{ user: 'eve', score: 5, verified: false },
|
|
656
|
-
],
|
|
657
|
-
},
|
|
658
|
-
{ category: 'electronics', price: 75, tags: ['refurbished'], active: false },
|
|
659
|
-
{ category: 'books', price: 25, tags: ['used', 'sale'], active: true },
|
|
660
|
-
{ category: 'clothing', price: 60, tags: ['new'], active: true },
|
|
661
|
-
];
|
|
662
|
-
|
|
663
|
-
await vectorDB.upsert({ indexName, vectors, metadata });
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
afterEach(async () => {
|
|
667
|
-
await vectorDB.deleteIndex({ indexName });
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
// Numeric Comparison Tests
|
|
671
|
-
describe('Comparison Operators', () => {
|
|
672
|
-
it('should handle numeric string comparisons', async () => {
|
|
673
|
-
// Insert a record with numeric string
|
|
674
|
-
await vectorDB.upsert({ indexName, vectors: [[1, 0.1, 0]], metadata: [{ numericString: '123' }] });
|
|
675
|
-
|
|
676
|
-
const results = await vectorDB.query({
|
|
677
|
-
indexName,
|
|
678
|
-
queryVector: [1, 0, 0],
|
|
679
|
-
filter: { numericString: { $gt: '100' } },
|
|
680
|
-
});
|
|
681
|
-
expect(results.length).toBeGreaterThan(0);
|
|
682
|
-
expect(results[0]?.metadata?.numericString).toBe('123');
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it('should filter with $gt operator', async () => {
|
|
686
|
-
const results = await vectorDB.query({
|
|
687
|
-
indexName,
|
|
688
|
-
queryVector: [1, 0, 0],
|
|
689
|
-
filter: { price: { $gt: 75 } },
|
|
690
|
-
});
|
|
691
|
-
expect(results).toHaveLength(1);
|
|
692
|
-
expect(results[0]?.metadata?.price).toBe(100);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it('should filter with $lte operator', async () => {
|
|
696
|
-
const results = await vectorDB.query({
|
|
697
|
-
indexName,
|
|
698
|
-
queryVector: [1, 0, 0],
|
|
699
|
-
filter: { price: { $lte: 50 } },
|
|
700
|
-
});
|
|
701
|
-
expect(results).toHaveLength(2);
|
|
702
|
-
results.forEach(result => {
|
|
703
|
-
expect(result.metadata?.price).toBeLessThanOrEqual(50);
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
it('should filter with lt operator', async () => {
|
|
708
|
-
const results = await vectorDB.query({
|
|
709
|
-
indexName,
|
|
710
|
-
queryVector: [1, 0, 0],
|
|
711
|
-
filter: { price: { $lt: 60 } },
|
|
712
|
-
});
|
|
713
|
-
expect(results).toHaveLength(2);
|
|
714
|
-
results.forEach(result => {
|
|
715
|
-
expect(result.metadata?.price).toBeLessThan(60);
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it('should filter with gte operator', async () => {
|
|
720
|
-
const results = await vectorDB.query({
|
|
721
|
-
indexName,
|
|
722
|
-
queryVector: [1, 0, 0],
|
|
723
|
-
filter: { price: { $gte: 75 } },
|
|
724
|
-
});
|
|
725
|
-
expect(results).toHaveLength(2);
|
|
726
|
-
results.forEach(result => {
|
|
727
|
-
expect(result.metadata?.price).toBeGreaterThanOrEqual(75);
|
|
728
|
-
});
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it('should filter with ne operator', async () => {
|
|
732
|
-
const results = await vectorDB.query({
|
|
733
|
-
indexName,
|
|
734
|
-
queryVector: [1, 0, 0],
|
|
735
|
-
filter: { category: { $ne: 'electronics' } },
|
|
736
|
-
});
|
|
737
|
-
expect(results.length).toBeGreaterThan(0);
|
|
738
|
-
results.forEach(result => {
|
|
739
|
-
expect(result.metadata?.category).not.toBe('electronics');
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
it('should filter with $gt and $lte operator', async () => {
|
|
744
|
-
const results = await vectorDB.query({
|
|
745
|
-
indexName,
|
|
746
|
-
queryVector: [1, 0, 0],
|
|
747
|
-
filter: { price: { $gt: 70, $lte: 100 } },
|
|
748
|
-
});
|
|
749
|
-
expect(results).toHaveLength(2);
|
|
750
|
-
results.forEach(result => {
|
|
751
|
-
expect(result.metadata?.price).toBeGreaterThan(70);
|
|
752
|
-
expect(result.metadata?.price).toBeLessThanOrEqual(100);
|
|
753
|
-
});
|
|
754
|
-
});
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
// Array Operator Tests
|
|
758
|
-
describe('Array Operators', () => {
|
|
759
|
-
it('should filter with $in operator for scalar field', async () => {
|
|
760
|
-
const results = await vectorDB.query({
|
|
761
|
-
indexName,
|
|
762
|
-
queryVector: [1, 0, 0],
|
|
763
|
-
filter: { category: { $in: ['electronics', 'clothing'] } },
|
|
764
|
-
});
|
|
765
|
-
expect(results).toHaveLength(3);
|
|
766
|
-
results.forEach(result => {
|
|
767
|
-
expect(['electronics', 'clothing']).toContain(result.metadata?.category);
|
|
768
|
-
});
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
it('should filter with $in operator for array field', async () => {
|
|
772
|
-
// Insert a record with tags as array
|
|
773
|
-
await vectorDB.upsert({
|
|
774
|
-
indexName,
|
|
775
|
-
vectors: [[2, 0.2, 0]],
|
|
776
|
-
metadata: [{ tags: ['featured', 'sale', 'new'] }],
|
|
777
|
-
});
|
|
778
|
-
const results = await vectorDB.query({
|
|
779
|
-
indexName,
|
|
780
|
-
queryVector: [1, 0, 0],
|
|
781
|
-
filter: { tags: { $in: ['sale', 'clearance'] } },
|
|
782
|
-
});
|
|
783
|
-
expect(results.length).toBeGreaterThan(0);
|
|
784
|
-
results.forEach(result => {
|
|
785
|
-
expect(result.metadata?.tags.some((tag: string) => ['sale', 'clearance'].includes(tag))).toBe(true);
|
|
786
|
-
});
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
it('should filter with $nin operator for scalar field', async () => {
|
|
790
|
-
const results = await vectorDB.query({
|
|
791
|
-
indexName,
|
|
792
|
-
queryVector: [1, 0, 0],
|
|
793
|
-
filter: { category: { $nin: ['electronics', 'books'] } },
|
|
794
|
-
});
|
|
795
|
-
expect(results.length).toBeGreaterThan(0);
|
|
796
|
-
results.forEach(result => {
|
|
797
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
it('should filter with $nin operator for array field', async () => {
|
|
802
|
-
// Insert a record with tags as array
|
|
803
|
-
await vectorDB.upsert({
|
|
804
|
-
indexName,
|
|
805
|
-
vectors: [[2, 0.3, 0]],
|
|
806
|
-
metadata: [{ tags: ['clearance', 'used'] }],
|
|
807
|
-
});
|
|
808
|
-
const results = await vectorDB.query({
|
|
809
|
-
indexName,
|
|
810
|
-
queryVector: [1, 0, 0],
|
|
811
|
-
filter: { tags: { $nin: ['new', 'sale'] } },
|
|
812
|
-
});
|
|
813
|
-
expect(results.length).toBeGreaterThan(0);
|
|
814
|
-
results.forEach(result => {
|
|
815
|
-
expect(result.metadata?.tags.every((tag: string) => !['new', 'sale'].includes(tag))).toBe(true);
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it('should handle empty arrays in in/nin operators', async () => {
|
|
820
|
-
// Should return no results for empty IN
|
|
821
|
-
const resultsIn = await vectorDB.query({
|
|
822
|
-
indexName,
|
|
823
|
-
queryVector: [1, 0, 0],
|
|
824
|
-
filter: { category: { $in: [] } },
|
|
825
|
-
});
|
|
826
|
-
expect(resultsIn).toHaveLength(0);
|
|
827
|
-
|
|
828
|
-
// Should return all results for empty NIN
|
|
829
|
-
const resultsNin = await vectorDB.query({
|
|
830
|
-
indexName,
|
|
831
|
-
queryVector: [1, 0, 0],
|
|
832
|
-
filter: { category: { $nin: [] } },
|
|
833
|
-
});
|
|
834
|
-
expect(resultsNin.length).toBeGreaterThan(0);
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it('should filter with $all operator', async () => {
|
|
838
|
-
const results = await vectorDB.query({
|
|
839
|
-
indexName,
|
|
840
|
-
queryVector: [1, 0, 0],
|
|
841
|
-
filter: { tags: { $all: ['used', 'sale'] } },
|
|
842
|
-
});
|
|
843
|
-
expect(results).toHaveLength(1);
|
|
844
|
-
results.forEach(result => {
|
|
845
|
-
expect(result.metadata?.tags).toContain('used');
|
|
846
|
-
expect(result.metadata?.tags).toContain('sale');
|
|
847
|
-
});
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
it('should filter with $all using single value', async () => {
|
|
851
|
-
const results = await vectorDB.query({
|
|
852
|
-
indexName,
|
|
853
|
-
queryVector: [1, 0, 0],
|
|
854
|
-
filter: { tags: { $all: ['new'] } },
|
|
855
|
-
});
|
|
856
|
-
expect(results.length).toBeGreaterThan(0);
|
|
857
|
-
results.forEach(result => {
|
|
858
|
-
expect(result.metadata?.tags).toContain('new');
|
|
859
|
-
});
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
it('should handle empty array for $all', async () => {
|
|
863
|
-
const results = await vectorDB.query({
|
|
864
|
-
indexName,
|
|
865
|
-
queryVector: [1, 0, 0],
|
|
866
|
-
filter: { tags: { $all: [] } },
|
|
867
|
-
});
|
|
868
|
-
expect(results).toHaveLength(0);
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
it('should handle non-array field $all', async () => {
|
|
872
|
-
// First insert a record with non-array field
|
|
873
|
-
await vectorDB.upsert({ indexName, vectors: [[1, 0.1, 0]], metadata: [{ tags: 'not-an-array' }] });
|
|
874
|
-
|
|
875
|
-
const results = await vectorDB.query({
|
|
876
|
-
indexName,
|
|
877
|
-
queryVector: [1, 0, 0],
|
|
878
|
-
filter: { tags: { $all: ['value'] } },
|
|
879
|
-
});
|
|
880
|
-
expect(results).toHaveLength(0);
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
// String Pattern Tests
|
|
884
|
-
it('should handle exact string matches', async () => {
|
|
885
|
-
const results = await vectorDB.query({
|
|
886
|
-
indexName,
|
|
887
|
-
queryVector: [1, 0, 0],
|
|
888
|
-
filter: { category: 'electronics' },
|
|
889
|
-
});
|
|
890
|
-
expect(results).toHaveLength(2);
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
it('should handle case-sensitive string matches', async () => {
|
|
894
|
-
const results = await vectorDB.query({
|
|
895
|
-
indexName,
|
|
896
|
-
queryVector: [1, 0, 0],
|
|
897
|
-
filter: { category: 'ELECTRONICS' },
|
|
898
|
-
});
|
|
899
|
-
expect(results).toHaveLength(0);
|
|
900
|
-
});
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
// Logical Operator Tests
|
|
904
|
-
describe('Logical Operators', () => {
|
|
905
|
-
it('should handle AND filter conditions', async () => {
|
|
906
|
-
const results = await vectorDB.query({
|
|
907
|
-
indexName,
|
|
908
|
-
queryVector: [1, 0, 0],
|
|
909
|
-
filter: { $and: [{ category: { $eq: 'electronics' } }, { price: { $gt: 75 } }] },
|
|
910
|
-
});
|
|
911
|
-
expect(results).toHaveLength(1);
|
|
912
|
-
expect(results[0]?.metadata?.category).toBe('electronics');
|
|
913
|
-
expect(results[0]?.metadata?.price).toBeGreaterThan(75);
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
it('should handle OR filter conditions', async () => {
|
|
917
|
-
const results = await vectorDB.query({
|
|
918
|
-
indexName,
|
|
919
|
-
queryVector: [1, 0, 0],
|
|
920
|
-
filter: { $or: [{ category: { $eq: 'electronics' } }, { category: { $eq: 'books' } }] },
|
|
921
|
-
});
|
|
922
|
-
expect(results.length).toBeGreaterThan(1);
|
|
923
|
-
results.forEach(result => {
|
|
924
|
-
expect(['electronics', 'books']).toContain(result?.metadata?.category);
|
|
925
|
-
});
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
it('should handle $not operator', async () => {
|
|
929
|
-
const results = await vectorDB.query({
|
|
930
|
-
indexName,
|
|
931
|
-
queryVector: [1, 0, 0],
|
|
932
|
-
filter: { $not: { category: 'electronics' } },
|
|
933
|
-
});
|
|
934
|
-
expect(results.length).toBeGreaterThan(0);
|
|
935
|
-
results.forEach(result => {
|
|
936
|
-
expect(result.metadata?.category).not.toBe('electronics');
|
|
937
|
-
});
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
it('should handle nested $not with $or', async () => {
|
|
941
|
-
const results = await vectorDB.query({
|
|
942
|
-
indexName,
|
|
943
|
-
queryVector: [1, 0, 0],
|
|
944
|
-
filter: { $not: { $or: [{ category: 'electronics' }, { category: 'books' }] } },
|
|
945
|
-
});
|
|
946
|
-
expect(results.length).toBeGreaterThan(0);
|
|
947
|
-
results.forEach(result => {
|
|
948
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
it('should handle $not with comparison operators', async () => {
|
|
953
|
-
const results = await vectorDB.query({
|
|
954
|
-
indexName,
|
|
955
|
-
queryVector: [1, 0, 0],
|
|
956
|
-
filter: { price: { $not: { $gt: 100 } } },
|
|
957
|
-
});
|
|
958
|
-
expect(results.length).toBeGreaterThan(0);
|
|
959
|
-
results.forEach(result => {
|
|
960
|
-
expect(Number(result.metadata?.price)).toBeLessThanOrEqual(100);
|
|
961
|
-
});
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
it('should handle $not with $in operator', async () => {
|
|
965
|
-
const results = await vectorDB.query({
|
|
966
|
-
indexName,
|
|
967
|
-
queryVector: [1, 0, 0],
|
|
968
|
-
filter: { category: { $not: { $in: ['electronics', 'books'] } } },
|
|
969
|
-
});
|
|
970
|
-
expect(results.length).toBeGreaterThan(0);
|
|
971
|
-
results.forEach(result => {
|
|
972
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
973
|
-
});
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
it('should handle $not with multiple nested conditions', async () => {
|
|
977
|
-
const results = await vectorDB.query({
|
|
978
|
-
indexName,
|
|
979
|
-
queryVector: [1, 0, 0],
|
|
980
|
-
filter: { $not: { $and: [{ category: 'electronics' }, { price: { $gt: 50 } }] } },
|
|
981
|
-
});
|
|
982
|
-
expect(results.length).toBeGreaterThan(0);
|
|
983
|
-
results.forEach(result => {
|
|
984
|
-
expect(result.metadata?.category !== 'electronics' || result.metadata?.price <= 50).toBe(true);
|
|
985
|
-
});
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
it('should handle $not with $exists operator', async () => {
|
|
989
|
-
const results = await vectorDB.query({
|
|
990
|
-
indexName,
|
|
991
|
-
queryVector: [1, 0, 0],
|
|
992
|
-
filter: { tags: { $not: { $exists: true } } },
|
|
993
|
-
});
|
|
994
|
-
expect(results.length).toBe(0); // All test data has tags
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
it('should handle $not with array operators', async () => {
|
|
998
|
-
const results = await vectorDB.query({
|
|
999
|
-
indexName,
|
|
1000
|
-
queryVector: [1, 0, 0],
|
|
1001
|
-
filter: { tags: { $not: { $all: ['new', 'premium'] } } },
|
|
1002
|
-
});
|
|
1003
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1004
|
-
results.forEach(result => {
|
|
1005
|
-
expect(!result.metadata?.tags.includes('new') || !result.metadata?.tags.includes('premium')).toBe(true);
|
|
1006
|
-
});
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
it('should handle $not with complex nested conditions', async () => {
|
|
1010
|
-
const results = await vectorDB.query({
|
|
1011
|
-
indexName,
|
|
1012
|
-
queryVector: [1, 0, 0],
|
|
1013
|
-
filter: {
|
|
1014
|
-
$not: {
|
|
1015
|
-
$or: [
|
|
1016
|
-
{
|
|
1017
|
-
$and: [{ category: 'electronics' }, { price: { $gt: 90 } }],
|
|
1018
|
-
},
|
|
1019
|
-
{
|
|
1020
|
-
$and: [{ category: 'books' }, { price: { $lt: 30 } }],
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1023
|
-
},
|
|
1024
|
-
},
|
|
1025
|
-
});
|
|
1026
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1027
|
-
results.forEach(result => {
|
|
1028
|
-
const notExpensiveElectronics = !(result.metadata?.category === 'electronics' && result.metadata?.price > 90);
|
|
1029
|
-
const notCheapBooks = !(result.metadata?.category === 'books' && result.metadata?.price < 30);
|
|
1030
|
-
expect(notExpensiveElectronics && notCheapBooks).toBe(true);
|
|
1031
|
-
});
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
it('should handle $not with empty arrays', async () => {
|
|
1035
|
-
const results = await vectorDB.query({
|
|
1036
|
-
indexName,
|
|
1037
|
-
queryVector: [1, 0, 0],
|
|
1038
|
-
filter: { tags: { $not: { $in: [] } } },
|
|
1039
|
-
});
|
|
1040
|
-
expect(results.length).toBeGreaterThan(0); // Should match all records
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
it('should handle $not with null values', async () => {
|
|
1044
|
-
// First insert a record with null value
|
|
1045
|
-
await vectorDB.upsert({
|
|
1046
|
-
indexName,
|
|
1047
|
-
vectors: [[1, 0.1, 0]],
|
|
1048
|
-
metadata: [{ category: null, price: 0 }],
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
const results = await vectorDB.query({
|
|
1052
|
-
indexName,
|
|
1053
|
-
queryVector: [1, 0, 0],
|
|
1054
|
-
filter: { category: { $not: { $eq: null } } },
|
|
1055
|
-
});
|
|
1056
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1057
|
-
results.forEach(result => {
|
|
1058
|
-
expect(result.metadata?.category).not.toBeNull();
|
|
1059
|
-
});
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
it('should handle $not with boolean values', async () => {
|
|
1063
|
-
const results = await vectorDB.query({
|
|
1064
|
-
indexName,
|
|
1065
|
-
queryVector: [1, 0, 0],
|
|
1066
|
-
filter: { active: { $not: { $eq: true } } },
|
|
1067
|
-
});
|
|
1068
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1069
|
-
results.forEach(result => {
|
|
1070
|
-
expect(result.metadata?.active).not.toBe(true);
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
it('should handle $not with multiple conditions', async () => {
|
|
1075
|
-
const results = await vectorDB.query({
|
|
1076
|
-
indexName,
|
|
1077
|
-
queryVector: [1, 0, 0],
|
|
1078
|
-
filter: { $not: { category: 'electronics', price: { $gt: 50 } } },
|
|
1079
|
-
});
|
|
1080
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
it('should handle $not with $not operator', async () => {
|
|
1084
|
-
const results = await vectorDB.query({
|
|
1085
|
-
indexName,
|
|
1086
|
-
queryVector: [1, 0, 0],
|
|
1087
|
-
filter: { $not: { $not: { category: 'electronics' } } },
|
|
1088
|
-
});
|
|
1089
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
it('should handle $not in nested fields', async () => {
|
|
1093
|
-
// Create a unique identifier for this test
|
|
1094
|
-
const testId = 'test-' + Date.now();
|
|
1095
|
-
await vectorDB.upsert({
|
|
1096
|
-
indexName,
|
|
1097
|
-
vectors: [[1, 0.1, 0]],
|
|
1098
|
-
metadata: [{ user: { profile: { price: 10 } }, testId }],
|
|
1099
|
-
});
|
|
1100
|
-
await vectorDB.upsert({
|
|
1101
|
-
indexName,
|
|
1102
|
-
vectors: [[1, 0.1, 0]],
|
|
1103
|
-
metadata: [{ user: { profile: { price: 50 } }, testId }],
|
|
1104
|
-
});
|
|
1105
|
-
const results = await vectorDB.query({
|
|
1106
|
-
indexName,
|
|
1107
|
-
queryVector: [1, 0, 0],
|
|
1108
|
-
filter: {
|
|
1109
|
-
$and: [
|
|
1110
|
-
{ testId }, // Only match our specific test document
|
|
1111
|
-
{ 'user.profile.price': { $not: { $gt: 25 } } },
|
|
1112
|
-
],
|
|
1113
|
-
},
|
|
1114
|
-
});
|
|
1115
|
-
expect(results.length).toBe(1);
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
it('should handle $not with multiple operators', async () => {
|
|
1119
|
-
const results = await vectorDB.query({
|
|
1120
|
-
indexName,
|
|
1121
|
-
queryVector: [1, 0, 0],
|
|
1122
|
-
filter: { price: { $not: { $gte: 30, $lte: 70 } } },
|
|
1123
|
-
});
|
|
1124
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1125
|
-
results.forEach(result => {
|
|
1126
|
-
const price = Number(result.metadata?.price);
|
|
1127
|
-
expect(price < 30 || price > 70).toBe(true);
|
|
1128
|
-
});
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
it('should handle $not with comparison operators', async () => {
|
|
1132
|
-
const results = await vectorDB.query({
|
|
1133
|
-
indexName,
|
|
1134
|
-
queryVector: [1, 0, 0],
|
|
1135
|
-
filter: { price: { $not: { $gt: 100 } } },
|
|
1136
|
-
});
|
|
1137
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1138
|
-
results.forEach(result => {
|
|
1139
|
-
expect(Number(result.metadata?.price)).toBeLessThanOrEqual(100);
|
|
1140
|
-
});
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
it('should handle $not with $and', async () => {
|
|
1144
|
-
const results = await vectorDB.query({
|
|
1145
|
-
indexName,
|
|
1146
|
-
queryVector: [1, 0, 0],
|
|
1147
|
-
filter: { $not: { $and: [{ category: 'electronics' }, { price: { $gt: 50 } }] } },
|
|
1148
|
-
});
|
|
1149
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1150
|
-
results.forEach(result => {
|
|
1151
|
-
expect(result.metadata?.category !== 'electronics' || result.metadata?.price <= 50).toBe(true);
|
|
1152
|
-
});
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
it('should handle nested $and with $or and $not', async () => {
|
|
1156
|
-
const results = await vectorDB.query({
|
|
1157
|
-
indexName,
|
|
1158
|
-
queryVector: [1, 0, 0],
|
|
1159
|
-
filter: {
|
|
1160
|
-
$and: [{ $or: [{ category: 'electronics' }, { category: 'books' }] }, { $not: { price: { $lt: 50 } } }],
|
|
1161
|
-
},
|
|
1162
|
-
});
|
|
1163
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1164
|
-
results.forEach(result => {
|
|
1165
|
-
expect(['electronics', 'books']).toContain(result.metadata?.category);
|
|
1166
|
-
expect(result.metadata?.price).toBeGreaterThanOrEqual(50);
|
|
1167
|
-
});
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
it('should handle $or with multiple $not conditions', async () => {
|
|
1171
|
-
const results = await vectorDB.query({
|
|
1172
|
-
indexName,
|
|
1173
|
-
queryVector: [1, 0, 0],
|
|
1174
|
-
filter: { $or: [{ $not: { category: 'electronics' } }, { $not: { price: { $gt: 50 } } }] },
|
|
1175
|
-
});
|
|
1176
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1177
|
-
results.forEach(result => {
|
|
1178
|
-
expect(result.metadata?.category !== 'electronics' || result.metadata?.price <= 50).toBe(true);
|
|
1179
|
-
});
|
|
1180
|
-
});
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
// Edge Cases and Special Values
|
|
1184
|
-
describe('Edge Cases and Special Values', () => {
|
|
1185
|
-
it('should handle empty result sets with valid filters', async () => {
|
|
1186
|
-
const results = await vectorDB.query({
|
|
1187
|
-
indexName,
|
|
1188
|
-
queryVector: [1, 0, 0],
|
|
1189
|
-
filter: { price: { $gt: 1000 } },
|
|
1190
|
-
});
|
|
1191
|
-
expect(results).toHaveLength(0);
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
it('should throw error for invalid operator', async () => {
|
|
1195
|
-
await expect(
|
|
1196
|
-
vectorDB.query({
|
|
1197
|
-
indexName,
|
|
1198
|
-
queryVector: [1, 0, 0],
|
|
1199
|
-
filter: { price: { $invalid: 100 } as any },
|
|
1200
|
-
}),
|
|
1201
|
-
).rejects.toThrow('Unsupported operator: $invalid');
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
it('should handle empty filter object', async () => {
|
|
1205
|
-
const results = await vectorDB.query({
|
|
1206
|
-
indexName,
|
|
1207
|
-
queryVector: [1, 0, 0],
|
|
1208
|
-
filter: {},
|
|
1209
|
-
});
|
|
1210
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
it('should handle numeric string comparisons', async () => {
|
|
1214
|
-
await vectorDB.upsert({
|
|
1215
|
-
indexName,
|
|
1216
|
-
vectors: [[1, 0.1, 0]],
|
|
1217
|
-
metadata: [{ numericString: '123' }],
|
|
1218
|
-
});
|
|
1219
|
-
const results = await vectorDB.query({
|
|
1220
|
-
indexName,
|
|
1221
|
-
queryVector: [1, 0, 0],
|
|
1222
|
-
filter: { numericString: { $gt: '100' } },
|
|
1223
|
-
});
|
|
1224
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1225
|
-
expect(results[0]?.metadata?.numericString).toBe('123');
|
|
1226
|
-
});
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
describe('Edge Cases and Special Values', () => {
|
|
1230
|
-
// Additional Edge Cases
|
|
1231
|
-
it('should handle empty result sets with valid filters', async () => {
|
|
1232
|
-
const results = await vectorDB.query({
|
|
1233
|
-
indexName,
|
|
1234
|
-
queryVector: [1, 0, 0],
|
|
1235
|
-
filter: { price: { $gt: 1000 } },
|
|
1236
|
-
});
|
|
1237
|
-
expect(results).toHaveLength(0);
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
it('should handle empty filter object', async () => {
|
|
1241
|
-
const results = await vectorDB.query({
|
|
1242
|
-
indexName,
|
|
1243
|
-
queryVector: [1, 0, 0],
|
|
1244
|
-
filter: {},
|
|
1245
|
-
});
|
|
1246
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
// Empty Conditions Tests
|
|
1250
|
-
it('should handle empty conditions in logical operators', async () => {
|
|
1251
|
-
const results = await vectorDB.query({
|
|
1252
|
-
indexName,
|
|
1253
|
-
queryVector: [1, 0, 0],
|
|
1254
|
-
filter: { $and: [], category: 'electronics' },
|
|
1255
|
-
});
|
|
1256
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1257
|
-
results.forEach(result => {
|
|
1258
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1259
|
-
});
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
it('should handle empty $and conditions', async () => {
|
|
1263
|
-
const results = await vectorDB.query({
|
|
1264
|
-
indexName,
|
|
1265
|
-
queryVector: [1, 0, 0],
|
|
1266
|
-
filter: { $and: [], category: 'electronics' },
|
|
1267
|
-
});
|
|
1268
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1269
|
-
results.forEach(result => {
|
|
1270
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1271
|
-
});
|
|
1272
|
-
});
|
|
1273
|
-
|
|
1274
|
-
it('should handle empty $or conditions', async () => {
|
|
1275
|
-
const results = await vectorDB.query({
|
|
1276
|
-
indexName,
|
|
1277
|
-
queryVector: [1, 0, 0],
|
|
1278
|
-
filter: { $or: [], category: 'electronics' },
|
|
1279
|
-
});
|
|
1280
|
-
expect(results).toHaveLength(0);
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
it('should handle empty $not conditions', async () => {
|
|
1284
|
-
await expect(
|
|
1285
|
-
vectorDB.query({
|
|
1286
|
-
indexName,
|
|
1287
|
-
queryVector: [1, 0, 0],
|
|
1288
|
-
filter: { $not: {}, category: 'electronics' },
|
|
1289
|
-
}),
|
|
1290
|
-
).rejects.toThrow('$not operator cannot be empty');
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
it('should handle multiple empty logical operators', async () => {
|
|
1294
|
-
const results = await vectorDB.query({
|
|
1295
|
-
indexName,
|
|
1296
|
-
queryVector: [1, 0, 0],
|
|
1297
|
-
filter: { $and: [], $or: [], category: 'electronics' },
|
|
1298
|
-
});
|
|
1299
|
-
expect(results).toHaveLength(0);
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
// Nested Field Tests
|
|
1303
|
-
it('should handle deeply nested metadata paths', async () => {
|
|
1304
|
-
await vectorDB.upsert({
|
|
1305
|
-
indexName,
|
|
1306
|
-
vectors: [[1, 0.1, 0]],
|
|
1307
|
-
metadata: [
|
|
1308
|
-
{
|
|
1309
|
-
level1: {
|
|
1310
|
-
level2: {
|
|
1311
|
-
level3: 'deep value',
|
|
1312
|
-
},
|
|
1313
|
-
},
|
|
1314
|
-
},
|
|
1315
|
-
],
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
const results = await vectorDB.query({
|
|
1319
|
-
indexName,
|
|
1320
|
-
queryVector: [1, 0, 0],
|
|
1321
|
-
filter: { 'level1.level2.level3': 'deep value' },
|
|
1322
|
-
});
|
|
1323
|
-
expect(results).toHaveLength(1);
|
|
1324
|
-
expect(results[0]?.metadata?.level1?.level2?.level3).toBe('deep value');
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
it('should handle non-existent nested paths', async () => {
|
|
1328
|
-
const results = await vectorDB.query({
|
|
1329
|
-
indexName,
|
|
1330
|
-
queryVector: [1, 0, 0],
|
|
1331
|
-
filter: { 'nonexistent.path': 'value' },
|
|
1332
|
-
});
|
|
1333
|
-
expect(results).toHaveLength(0);
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
// Complex Nested Operators Test
|
|
1337
|
-
it('should handle deeply nested logical operators', async () => {
|
|
1338
|
-
const results = await vectorDB.query({
|
|
1339
|
-
indexName,
|
|
1340
|
-
queryVector: [1, 0, 0],
|
|
1341
|
-
filter: {
|
|
1342
|
-
$and: [
|
|
1343
|
-
{
|
|
1344
|
-
$or: [{ category: 'electronics' }, { $and: [{ category: 'books' }, { price: { $lt: 30 } }] }],
|
|
1345
|
-
},
|
|
1346
|
-
{
|
|
1347
|
-
$not: {
|
|
1348
|
-
$or: [{ active: false }, { price: { $gt: 100 } }],
|
|
1349
|
-
},
|
|
1350
|
-
},
|
|
1351
|
-
],
|
|
1352
|
-
},
|
|
1353
|
-
});
|
|
1354
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1355
|
-
results.forEach(result => {
|
|
1356
|
-
// First condition: electronics OR (books AND price < 30)
|
|
1357
|
-
const firstCondition =
|
|
1358
|
-
result.metadata?.category === 'electronics' ||
|
|
1359
|
-
(result.metadata?.category === 'books' && result.metadata?.price < 30);
|
|
1360
|
-
|
|
1361
|
-
// Second condition: NOT (active = false OR price > 100)
|
|
1362
|
-
const secondCondition = result.metadata?.active !== false && result.metadata?.price <= 100;
|
|
1363
|
-
|
|
1364
|
-
expect(firstCondition && secondCondition).toBe(true);
|
|
1365
|
-
});
|
|
1366
|
-
});
|
|
1367
|
-
|
|
1368
|
-
it('should handle multiple logical operators at root level', async () => {
|
|
1369
|
-
const results = await vectorDB.query({
|
|
1370
|
-
indexName,
|
|
1371
|
-
queryVector: [1, 0, 0],
|
|
1372
|
-
filter: {
|
|
1373
|
-
$and: [{ category: 'electronics' }],
|
|
1374
|
-
$or: [{ price: { $lt: 100 } }, { price: { $gt: 20 } }],
|
|
1375
|
-
},
|
|
1376
|
-
});
|
|
1377
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1378
|
-
results.forEach(result => {
|
|
1379
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1380
|
-
expect(result.metadata?.price < 100 || result.metadata?.price > 20).toBe(true);
|
|
1381
|
-
});
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
it('should handle undefined filter', async () => {
|
|
1385
|
-
const results1 = await vectorDB.query({
|
|
1386
|
-
indexName,
|
|
1387
|
-
queryVector: [1, 0, 0],
|
|
1388
|
-
filter: undefined,
|
|
1389
|
-
});
|
|
1390
|
-
const results2 = await vectorDB.query({
|
|
1391
|
-
indexName,
|
|
1392
|
-
queryVector: [1, 0, 0],
|
|
1393
|
-
});
|
|
1394
|
-
expect(results1).toEqual(results2);
|
|
1395
|
-
expect(results1.length).toBeGreaterThan(0);
|
|
1396
|
-
});
|
|
1397
|
-
|
|
1398
|
-
it('should handle empty object filter', async () => {
|
|
1399
|
-
const results = await vectorDB.query({
|
|
1400
|
-
indexName,
|
|
1401
|
-
queryVector: [1, 0, 0],
|
|
1402
|
-
filter: {},
|
|
1403
|
-
});
|
|
1404
|
-
const results2 = await vectorDB.query({
|
|
1405
|
-
indexName,
|
|
1406
|
-
queryVector: [1, 0, 0],
|
|
1407
|
-
});
|
|
1408
|
-
expect(results).toEqual(results2);
|
|
1409
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1410
|
-
});
|
|
1411
|
-
|
|
1412
|
-
it('should handle null filter', async () => {
|
|
1413
|
-
const results = await vectorDB.query({
|
|
1414
|
-
indexName,
|
|
1415
|
-
queryVector: [1, 0, 0],
|
|
1416
|
-
filter: null,
|
|
1417
|
-
});
|
|
1418
|
-
const results2 = await vectorDB.query({
|
|
1419
|
-
indexName,
|
|
1420
|
-
queryVector: [1, 0, 0],
|
|
1421
|
-
});
|
|
1422
|
-
expect(results).toEqual(results2);
|
|
1423
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1424
|
-
});
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
// Regex Operator Tests
|
|
1428
|
-
describe('Regex Operators', () => {
|
|
1429
|
-
it('should handle $regex with case sensitivity', async () => {
|
|
1430
|
-
const results = await vectorDB.query({
|
|
1431
|
-
indexName,
|
|
1432
|
-
queryVector: [1, 0, 0],
|
|
1433
|
-
filter: { category: { $regex: 'ELECTRONICS' } },
|
|
1434
|
-
});
|
|
1435
|
-
expect(results).toHaveLength(0);
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
it('should handle $regex with start anchor', async () => {
|
|
1439
|
-
const results = await vectorDB.query({
|
|
1440
|
-
indexName,
|
|
1441
|
-
queryVector: [1, 0, 0],
|
|
1442
|
-
filter: { category: { $regex: '^elect' } },
|
|
1443
|
-
});
|
|
1444
|
-
expect(results).toHaveLength(2);
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1447
|
-
it('should handle $regex with end anchor', async () => {
|
|
1448
|
-
const results = await vectorDB.query({
|
|
1449
|
-
indexName,
|
|
1450
|
-
queryVector: [1, 0, 0],
|
|
1451
|
-
filter: { category: { $regex: 'nics$' } },
|
|
1452
|
-
});
|
|
1453
|
-
expect(results).toHaveLength(2);
|
|
1454
|
-
});
|
|
1455
|
-
|
|
1456
|
-
it('should handle dotall flag', async () => {
|
|
1457
|
-
await vectorDB.upsert({
|
|
1458
|
-
indexName,
|
|
1459
|
-
vectors: [[1, 0.1, 0]],
|
|
1460
|
-
metadata: [{ description: 'First\nSecond\nThird' }],
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
const withoutS = await vectorDB.query({
|
|
1464
|
-
indexName,
|
|
1465
|
-
queryVector: [1, 0, 0],
|
|
1466
|
-
filter: { description: { $regex: 'First[^\\n]*Third' } },
|
|
1467
|
-
});
|
|
1468
|
-
expect(withoutS).toHaveLength(0);
|
|
1469
|
-
});
|
|
1470
|
-
it('should handle $not with $regex operator', async () => {
|
|
1471
|
-
const results = await vectorDB.query({
|
|
1472
|
-
indexName,
|
|
1473
|
-
queryVector: [1, 0, 0],
|
|
1474
|
-
filter: { category: { $not: { $regex: '^elect' } } },
|
|
1475
|
-
});
|
|
1476
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1477
|
-
results.forEach(result => {
|
|
1478
|
-
expect(result.metadata?.category).not.toMatch(/^elect/);
|
|
1479
|
-
});
|
|
1480
|
-
});
|
|
1481
|
-
});
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
describe('Error Handling', () => {
|
|
1485
|
-
const testIndexName = 'test_index_error';
|
|
1486
|
-
beforeAll(async () => {
|
|
1487
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
1488
|
-
});
|
|
1489
|
-
|
|
1490
|
-
afterAll(async () => {
|
|
1491
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
it('should handle non-existent index queries', async () => {
|
|
1495
|
-
await expect(vectorDB.query({ indexName: 'non-existent-index', queryVector: [1, 2, 3] })).rejects.toThrow();
|
|
1496
|
-
});
|
|
1497
|
-
|
|
1498
|
-
it('should handle invalid dimension vectors', async () => {
|
|
1499
|
-
const invalidVector = [1, 2, 3, 4]; // 4D vector for 3D index
|
|
1500
|
-
await expect(vectorDB.upsert({ indexName: testIndexName, vectors: [invalidVector] })).rejects.toThrow();
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
it('should handle duplicate index creation gracefully', async () => {
|
|
1504
|
-
const infoSpy = vi.spyOn(vectorDB['logger'], 'info');
|
|
1505
|
-
const warnSpy = vi.spyOn(vectorDB['logger'], 'warn');
|
|
1506
|
-
|
|
1507
|
-
const duplicateIndexName = `duplicate-test`;
|
|
1508
|
-
const dimension = 768;
|
|
1509
|
-
|
|
1510
|
-
try {
|
|
1511
|
-
// Create index first time
|
|
1512
|
-
await vectorDB.createIndex({
|
|
1513
|
-
indexName: duplicateIndexName,
|
|
1514
|
-
dimension,
|
|
1515
|
-
metric: 'cosine',
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
// Try to create with same dimensions - should not throw
|
|
1519
|
-
await expect(
|
|
1520
|
-
vectorDB.createIndex({
|
|
1521
|
-
indexName: duplicateIndexName,
|
|
1522
|
-
dimension,
|
|
1523
|
-
metric: 'cosine',
|
|
1524
|
-
}),
|
|
1525
|
-
).resolves.not.toThrow();
|
|
1526
|
-
|
|
1527
|
-
expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('already exists with'));
|
|
1528
|
-
|
|
1529
|
-
// Try to create with same dimensions and different metric - should not throw
|
|
1530
|
-
await expect(
|
|
1531
|
-
vectorDB.createIndex({
|
|
1532
|
-
indexName: duplicateIndexName,
|
|
1533
|
-
dimension,
|
|
1534
|
-
metric: 'euclidean',
|
|
1535
|
-
}),
|
|
1536
|
-
).resolves.not.toThrow();
|
|
1537
|
-
|
|
1538
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Attempted to create index with metric'));
|
|
1539
|
-
|
|
1540
|
-
// Try to create with different dimensions - should throw
|
|
1541
|
-
await expect(
|
|
1542
|
-
vectorDB.createIndex({
|
|
1543
|
-
indexName: duplicateIndexName,
|
|
1544
|
-
dimension: dimension + 1,
|
|
1545
|
-
metric: 'cosine',
|
|
1546
|
-
}),
|
|
1547
|
-
).rejects.toThrow(
|
|
1548
|
-
`Index "${duplicateIndexName}" already exists with ${dimension} dimensions, but ${dimension + 1} dimensions were requested`,
|
|
1549
|
-
);
|
|
1550
|
-
} finally {
|
|
1551
|
-
infoSpy.mockRestore();
|
|
1552
|
-
warnSpy.mockRestore();
|
|
1553
|
-
// Cleanup
|
|
1554
|
-
await vectorDB.deleteIndex({ indexName: duplicateIndexName });
|
|
1555
|
-
}
|
|
1556
|
-
});
|
|
1557
|
-
});
|
|
1558
|
-
});
|