@mastra/pinecone 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 -458
- package/src/vector/filter.ts +0 -133
- package/src/vector/index.test.ts +0 -1485
- package/src/vector/index.ts +0 -364
- package/src/vector/prompt.ts +0 -81
- 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,1485 +0,0 @@
|
|
|
1
|
-
import type { QueryResult } from '@mastra/core/vector';
|
|
2
|
-
import dotenv from 'dotenv';
|
|
3
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, afterEach } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import { PineconeVector } from './';
|
|
6
|
-
|
|
7
|
-
dotenv.config();
|
|
8
|
-
|
|
9
|
-
const PINECONE_API_KEY = process.env.PINECONE_API_KEY!;
|
|
10
|
-
|
|
11
|
-
// if (!PINECONE_API_KEY) {
|
|
12
|
-
// throw new Error('Please set PINECONE_API_KEY and PINECONE_ENVIRONMENT in .env file');
|
|
13
|
-
// }
|
|
14
|
-
// TODO: skip until we the secrets on Github
|
|
15
|
-
|
|
16
|
-
vi.setConfig({ testTimeout: 80_000, hookTimeout: 80_000 });
|
|
17
|
-
|
|
18
|
-
// Helper function to create sparse vectors for testing
|
|
19
|
-
function createSparseVector(text: string) {
|
|
20
|
-
const words = text.toLowerCase().split(/\W+/).filter(Boolean);
|
|
21
|
-
const uniqueWords = Array.from(new Set(words));
|
|
22
|
-
const indices: number[] = [];
|
|
23
|
-
const values: number[] = [];
|
|
24
|
-
|
|
25
|
-
// Create a simple term frequency vector
|
|
26
|
-
uniqueWords.forEach((word, i) => {
|
|
27
|
-
const frequency = words.filter(w => w === word).length;
|
|
28
|
-
indices.push(i);
|
|
29
|
-
values.push(frequency);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
return { indices, values };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function waitUntilReady(vectorDB: PineconeVector, indexName: string) {
|
|
36
|
-
return new Promise(resolve => {
|
|
37
|
-
const interval = setInterval(async () => {
|
|
38
|
-
try {
|
|
39
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
40
|
-
if (!!stats) {
|
|
41
|
-
clearInterval(interval);
|
|
42
|
-
resolve(true);
|
|
43
|
-
}
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.log(error);
|
|
46
|
-
}
|
|
47
|
-
}, 5000);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function waitUntilIndexDeleted(vectorDB: PineconeVector, indexName: string) {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const maxAttempts = 60;
|
|
54
|
-
let attempts = 0;
|
|
55
|
-
|
|
56
|
-
const interval = setInterval(async () => {
|
|
57
|
-
try {
|
|
58
|
-
const indexes = await vectorDB.listIndexes();
|
|
59
|
-
if (!indexes.includes(indexName)) {
|
|
60
|
-
clearInterval(interval);
|
|
61
|
-
resolve(true);
|
|
62
|
-
}
|
|
63
|
-
attempts++;
|
|
64
|
-
if (attempts >= maxAttempts) {
|
|
65
|
-
clearInterval(interval);
|
|
66
|
-
reject(new Error('Timeout waiting for index to be deleted'));
|
|
67
|
-
}
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.log(error);
|
|
70
|
-
}
|
|
71
|
-
}, 5000);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function waitUntilVectorsIndexed(
|
|
76
|
-
vectorDB: PineconeVector,
|
|
77
|
-
indexName: string,
|
|
78
|
-
expectedCount: number,
|
|
79
|
-
exactCount = false,
|
|
80
|
-
) {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
const maxAttempts = 60;
|
|
83
|
-
let attempts = 0;
|
|
84
|
-
let lastCount = 0;
|
|
85
|
-
let stableCount = 0;
|
|
86
|
-
|
|
87
|
-
const interval = setInterval(async () => {
|
|
88
|
-
try {
|
|
89
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
90
|
-
const check = exactCount ? stats?.count === expectedCount : stats?.count >= expectedCount;
|
|
91
|
-
if (stats && check) {
|
|
92
|
-
if (stats.count === lastCount) {
|
|
93
|
-
stableCount++;
|
|
94
|
-
if (stableCount >= 2) {
|
|
95
|
-
clearInterval(interval);
|
|
96
|
-
resolve(true);
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
stableCount = 1;
|
|
100
|
-
}
|
|
101
|
-
lastCount = stats.count;
|
|
102
|
-
}
|
|
103
|
-
attempts++;
|
|
104
|
-
if (attempts >= maxAttempts) {
|
|
105
|
-
clearInterval(interval);
|
|
106
|
-
reject(new Error('Timeout waiting for vectors to be indexed'));
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.log(error);
|
|
110
|
-
}
|
|
111
|
-
}, 10000);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
// TODO: our pinecone account is over the limit, tests don't work in CI
|
|
115
|
-
describe.skip('PineconeVector Integration Tests', () => {
|
|
116
|
-
let vectorDB: PineconeVector;
|
|
117
|
-
const testIndexName = 'test-index'; // Unique index name for each test run
|
|
118
|
-
const indexNameUpdate = 'test-index-update';
|
|
119
|
-
const indexNameDelete = 'test-index-delete';
|
|
120
|
-
const indexNameNamespace = 'test-index-namespace';
|
|
121
|
-
const indexNameHybrid = 'test-index-hybrid';
|
|
122
|
-
const dimension = 3;
|
|
123
|
-
|
|
124
|
-
beforeAll(async () => {
|
|
125
|
-
vectorDB = new PineconeVector({
|
|
126
|
-
apiKey: PINECONE_API_KEY,
|
|
127
|
-
});
|
|
128
|
-
// Delete test index
|
|
129
|
-
try {
|
|
130
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
131
|
-
await waitUntilIndexDeleted(vectorDB, testIndexName);
|
|
132
|
-
} catch {
|
|
133
|
-
// Ignore errors if index doesn't exist
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
await vectorDB.deleteIndex({ indexName: indexNameUpdate });
|
|
137
|
-
await waitUntilIndexDeleted(vectorDB, indexNameUpdate);
|
|
138
|
-
} catch {
|
|
139
|
-
// Ignore errors if index doesn't exist
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
await vectorDB.deleteIndex({ indexName: indexNameDelete });
|
|
143
|
-
await waitUntilIndexDeleted(vectorDB, indexNameDelete);
|
|
144
|
-
} catch {
|
|
145
|
-
// Ignore errors if index doesn't exist
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
await vectorDB.deleteIndex({ indexName: indexNameNamespace });
|
|
149
|
-
await waitUntilIndexDeleted(vectorDB, indexNameNamespace);
|
|
150
|
-
} catch {
|
|
151
|
-
// Ignore errors if index doesn't exist
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
await vectorDB.deleteIndex({ indexName: indexNameHybrid });
|
|
155
|
-
await waitUntilIndexDeleted(vectorDB, indexNameHybrid);
|
|
156
|
-
} catch {
|
|
157
|
-
// Ignore errors if index doesn't exist
|
|
158
|
-
}
|
|
159
|
-
// Create test index
|
|
160
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension });
|
|
161
|
-
await waitUntilReady(vectorDB, testIndexName);
|
|
162
|
-
}, 500000);
|
|
163
|
-
|
|
164
|
-
afterAll(async () => {
|
|
165
|
-
// Cleanup: delete test index
|
|
166
|
-
try {
|
|
167
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
168
|
-
} catch {
|
|
169
|
-
// Ignore errors if index doesn't exist
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
await vectorDB.deleteIndex({ indexName: indexNameUpdate });
|
|
173
|
-
} catch {
|
|
174
|
-
// Ignore errors if index doesn't exist
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
await vectorDB.deleteIndex({ indexName: indexNameDelete });
|
|
178
|
-
} catch {
|
|
179
|
-
// Ignore errors if index doesn't exist
|
|
180
|
-
}
|
|
181
|
-
try {
|
|
182
|
-
await vectorDB.deleteIndex({ indexName: indexNameNamespace });
|
|
183
|
-
} catch {
|
|
184
|
-
// Ignore errors if index doesn't exist
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
await vectorDB.deleteIndex({ indexName: indexNameHybrid });
|
|
188
|
-
} catch {
|
|
189
|
-
// Ignore errors if index doesn't exist
|
|
190
|
-
}
|
|
191
|
-
}, 500000);
|
|
192
|
-
|
|
193
|
-
describe('Index Operations', () => {
|
|
194
|
-
it('should list indexes including our test index', async () => {
|
|
195
|
-
const indexes = await vectorDB.listIndexes();
|
|
196
|
-
expect(indexes).toContain(testIndexName);
|
|
197
|
-
}, 500000);
|
|
198
|
-
|
|
199
|
-
it('should describe index with correct properties', async () => {
|
|
200
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName });
|
|
201
|
-
expect(stats.dimension).toBe(dimension);
|
|
202
|
-
expect(stats.metric).toBe('cosine');
|
|
203
|
-
expect(typeof stats.count).toBe('number');
|
|
204
|
-
}, 500000);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('Vector Operations', () => {
|
|
208
|
-
const testVectors = [
|
|
209
|
-
[1.0, 0.0, 0.0],
|
|
210
|
-
[0.0, 1.0, 0.0],
|
|
211
|
-
[0.0, 0.0, 1.0],
|
|
212
|
-
];
|
|
213
|
-
const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
|
|
214
|
-
let vectorIds: string[];
|
|
215
|
-
|
|
216
|
-
it('should upsert vectors with metadata', async () => {
|
|
217
|
-
vectorIds = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
218
|
-
expect(vectorIds).toHaveLength(3);
|
|
219
|
-
// Wait for vectors to be indexed
|
|
220
|
-
await waitUntilVectorsIndexed(vectorDB, testIndexName, 3);
|
|
221
|
-
}, 500000);
|
|
222
|
-
|
|
223
|
-
it.skip('should query vectors and return nearest neighbors', async () => {
|
|
224
|
-
const queryVector = [1.0, 0.1, 0.1];
|
|
225
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector, topK: 3 });
|
|
226
|
-
|
|
227
|
-
expect(results).toHaveLength(3);
|
|
228
|
-
expect(results[0]!.score).toBeGreaterThan(0);
|
|
229
|
-
expect(results[0]!.metadata).toBeDefined();
|
|
230
|
-
}, 500000);
|
|
231
|
-
|
|
232
|
-
it('should query vectors with metadata filter', async () => {
|
|
233
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
234
|
-
const filter = { label: 'y-axis' };
|
|
235
|
-
|
|
236
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector, topK: 1, filter });
|
|
237
|
-
|
|
238
|
-
expect(results).toHaveLength(1);
|
|
239
|
-
expect(results?.[0]?.metadata?.label).toBe('y-axis');
|
|
240
|
-
}, 500000);
|
|
241
|
-
|
|
242
|
-
it('should query vectors and return vectors in results', async () => {
|
|
243
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
244
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector, topK: 1, includeVector: true });
|
|
245
|
-
|
|
246
|
-
expect(results).toHaveLength(1);
|
|
247
|
-
expect(results?.[0]?.vector).toBeDefined();
|
|
248
|
-
expect(results?.[0]?.vector).toHaveLength(dimension);
|
|
249
|
-
}, 500000);
|
|
250
|
-
|
|
251
|
-
describe('vector update operations', () => {
|
|
252
|
-
const testVectors = [
|
|
253
|
-
[1, 2, 3],
|
|
254
|
-
[4, 5, 6],
|
|
255
|
-
[7, 8, 9],
|
|
256
|
-
];
|
|
257
|
-
|
|
258
|
-
beforeEach(async () => {
|
|
259
|
-
await vectorDB.createIndex({ indexName: indexNameUpdate, dimension, metric: 'cosine' });
|
|
260
|
-
await waitUntilReady(vectorDB, indexNameUpdate);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
afterEach(async () => {
|
|
264
|
-
try {
|
|
265
|
-
await vectorDB.deleteIndex({ indexName: indexNameUpdate });
|
|
266
|
-
await waitUntilIndexDeleted(vectorDB, indexNameUpdate);
|
|
267
|
-
} catch {
|
|
268
|
-
// Ignore errors if index doesn't exist
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should update the vector by id', async () => {
|
|
273
|
-
const ids = await vectorDB.upsert({ indexName: indexNameUpdate, vectors: testVectors });
|
|
274
|
-
expect(ids).toHaveLength(3);
|
|
275
|
-
|
|
276
|
-
const idToBeUpdated = ids[0];
|
|
277
|
-
const newVector = [1, 2, 4];
|
|
278
|
-
const newMetaData = {
|
|
279
|
-
test: 'updates',
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const update = {
|
|
283
|
-
vector: newVector,
|
|
284
|
-
metadata: newMetaData,
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
await vectorDB.updateVector({ indexName: indexNameUpdate, id: idToBeUpdated, update });
|
|
288
|
-
|
|
289
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameUpdate, 3);
|
|
290
|
-
|
|
291
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
292
|
-
indexName: indexNameUpdate,
|
|
293
|
-
queryVector: newVector,
|
|
294
|
-
topK: 10,
|
|
295
|
-
includeVector: true,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
299
|
-
expect(results[0]?.vector).toEqual(newVector);
|
|
300
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
301
|
-
}, 500000);
|
|
302
|
-
|
|
303
|
-
it('should only update the metadata by id', async () => {
|
|
304
|
-
const ids = await vectorDB.upsert({ indexName: indexNameUpdate, vectors: testVectors });
|
|
305
|
-
expect(ids).toHaveLength(3);
|
|
306
|
-
|
|
307
|
-
const idToBeUpdated = ids[0];
|
|
308
|
-
const newMetaData = {
|
|
309
|
-
test: 'updates',
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const update = {
|
|
313
|
-
metadata: newMetaData,
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
await vectorDB.updateVector({ indexName: indexNameUpdate, id: idToBeUpdated, update });
|
|
317
|
-
|
|
318
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameUpdate, 3);
|
|
319
|
-
|
|
320
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
321
|
-
indexName: indexNameUpdate,
|
|
322
|
-
queryVector: testVectors[0],
|
|
323
|
-
topK: 2,
|
|
324
|
-
includeVector: true,
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
328
|
-
expect(results[0]?.vector).toEqual(testVectors[0]);
|
|
329
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
330
|
-
}, 500000);
|
|
331
|
-
|
|
332
|
-
it('should only update vector embeddings by id', async () => {
|
|
333
|
-
const ids = await vectorDB.upsert({ indexName: indexNameUpdate, vectors: testVectors });
|
|
334
|
-
expect(ids).toHaveLength(3);
|
|
335
|
-
|
|
336
|
-
const idToBeUpdated = ids[0];
|
|
337
|
-
const newVector = [4, 4, 4];
|
|
338
|
-
|
|
339
|
-
const update = {
|
|
340
|
-
vector: newVector,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
await vectorDB.updateVector({ indexName: indexNameUpdate, id: idToBeUpdated, update });
|
|
344
|
-
|
|
345
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameUpdate, 3);
|
|
346
|
-
|
|
347
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
348
|
-
indexName: indexNameUpdate,
|
|
349
|
-
queryVector: newVector,
|
|
350
|
-
topK: 10,
|
|
351
|
-
includeVector: true,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const updatedResult = results.find(r => r.id === idToBeUpdated);
|
|
355
|
-
expect(updatedResult).toBeDefined();
|
|
356
|
-
expect(updatedResult?.vector).toEqual(newVector);
|
|
357
|
-
}, 500000);
|
|
358
|
-
|
|
359
|
-
it('should throw exception when no updates are given', async () => {
|
|
360
|
-
await expect(vectorDB.updateVector({ indexName: indexNameUpdate, id: 'id', update: {} })).rejects.toThrow(
|
|
361
|
-
'No updates provided',
|
|
362
|
-
);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('should throw error for non-existent index', async () => {
|
|
366
|
-
const nonExistentIndex = 'non-existent-index';
|
|
367
|
-
await expect(
|
|
368
|
-
vectorDB.updateVector({ indexName: nonExistentIndex, id: 'test-id', update: { vector: [1, 2, 3] } }),
|
|
369
|
-
).rejects.toThrow();
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('should throw error for invalid vector dimension', async () => {
|
|
373
|
-
const [id] = await vectorDB.upsert({
|
|
374
|
-
indexName: indexNameUpdate,
|
|
375
|
-
vectors: [[1, 2, 3]],
|
|
376
|
-
metadata: [{ test: 'initial' }],
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
await expect(
|
|
380
|
-
vectorDB.updateVector({ indexName: indexNameUpdate, id, update: { vector: [1, 2] } }), // Wrong dimension
|
|
381
|
-
).rejects.toThrow();
|
|
382
|
-
}, 500000);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
describe('vector delete operations', () => {
|
|
386
|
-
const testVectors = [
|
|
387
|
-
[1, 2, 3],
|
|
388
|
-
[4, 5, 6],
|
|
389
|
-
[7, 8, 9],
|
|
390
|
-
];
|
|
391
|
-
|
|
392
|
-
beforeEach(async () => {
|
|
393
|
-
await vectorDB.createIndex({ indexName: indexNameDelete, dimension, metric: 'cosine' });
|
|
394
|
-
await waitUntilReady(vectorDB, indexNameDelete);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
afterEach(async () => {
|
|
398
|
-
try {
|
|
399
|
-
await vectorDB.deleteIndex({ indexName: indexNameDelete });
|
|
400
|
-
await waitUntilIndexDeleted(vectorDB, indexNameDelete);
|
|
401
|
-
} catch {
|
|
402
|
-
// Ignore errors if index doesn't exist
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('should delete the vector by id', async () => {
|
|
407
|
-
const ids = await vectorDB.upsert({ indexName: indexNameDelete, vectors: testVectors });
|
|
408
|
-
expect(ids).toHaveLength(3);
|
|
409
|
-
const idToBeDeleted = ids[0];
|
|
410
|
-
|
|
411
|
-
await vectorDB.deleteVector({ indexName: indexNameDelete, id: idToBeDeleted });
|
|
412
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameDelete, 2, true);
|
|
413
|
-
|
|
414
|
-
// Query all vectors similar to the deleted one
|
|
415
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
416
|
-
indexName: indexNameDelete,
|
|
417
|
-
queryVector: testVectors[0],
|
|
418
|
-
topK: 3,
|
|
419
|
-
includeVector: true,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
const resultIds = results.map(r => r.id);
|
|
423
|
-
expect(resultIds).not.toContain(idToBeDeleted);
|
|
424
|
-
}, 500000);
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
describe('Namespace Operations', () => {
|
|
429
|
-
const namespace1 = 'test-namespace-1';
|
|
430
|
-
const namespace2 = 'test-namespace-2';
|
|
431
|
-
const testVector = [1.0, 0.0, 0.0];
|
|
432
|
-
const testMetadata = { label: 'test' };
|
|
433
|
-
|
|
434
|
-
beforeEach(async () => {
|
|
435
|
-
await vectorDB.createIndex({ indexName: indexNameNamespace, dimension, metric: 'cosine' });
|
|
436
|
-
await waitUntilReady(vectorDB, indexNameNamespace);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
afterEach(async () => {
|
|
440
|
-
try {
|
|
441
|
-
await vectorDB.deleteIndex({ indexName: indexNameNamespace });
|
|
442
|
-
await waitUntilIndexDeleted(vectorDB, indexNameNamespace);
|
|
443
|
-
} catch {
|
|
444
|
-
// Ignore errors if index doesn't exist
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('should isolate vectors in different namespaces', async () => {
|
|
449
|
-
// Insert same vector in two namespaces
|
|
450
|
-
const [id1] = await vectorDB.upsert({
|
|
451
|
-
indexName: indexNameNamespace,
|
|
452
|
-
vectors: [testVector],
|
|
453
|
-
metadata: [testMetadata],
|
|
454
|
-
namespace: namespace1,
|
|
455
|
-
});
|
|
456
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 1);
|
|
457
|
-
|
|
458
|
-
const [id2] = await vectorDB.upsert({
|
|
459
|
-
indexName: indexNameNamespace,
|
|
460
|
-
vectors: [testVector],
|
|
461
|
-
metadata: [{ ...testMetadata, label: 'test2' }],
|
|
462
|
-
namespace: namespace2,
|
|
463
|
-
});
|
|
464
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 2);
|
|
465
|
-
|
|
466
|
-
// Query namespace1
|
|
467
|
-
const results1 = await vectorDB.query({
|
|
468
|
-
indexName: indexNameNamespace,
|
|
469
|
-
queryVector: testVector,
|
|
470
|
-
namespace: namespace1,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// Query namespace2
|
|
474
|
-
const results2 = await vectorDB.query({
|
|
475
|
-
indexName: indexNameNamespace,
|
|
476
|
-
queryVector: testVector,
|
|
477
|
-
namespace: namespace2,
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// Verify isolation
|
|
481
|
-
expect(results1).toHaveLength(1);
|
|
482
|
-
expect(results2).toHaveLength(1);
|
|
483
|
-
expect(results1[0]?.id).toBe(id1);
|
|
484
|
-
expect(results1[0]?.metadata?.label).toBe('test');
|
|
485
|
-
expect(results2[0]?.id).toBe(id2);
|
|
486
|
-
expect(results2[0]?.metadata?.label).toBe('test2');
|
|
487
|
-
}, 500000);
|
|
488
|
-
|
|
489
|
-
it('should update vectors within specific namespace', async () => {
|
|
490
|
-
const [id] = await vectorDB.upsert({
|
|
491
|
-
indexName: indexNameNamespace,
|
|
492
|
-
vectors: [testVector],
|
|
493
|
-
metadata: [testMetadata],
|
|
494
|
-
namespace: namespace1,
|
|
495
|
-
});
|
|
496
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 1);
|
|
497
|
-
|
|
498
|
-
// Update in namespace1
|
|
499
|
-
await vectorDB.updateVector({
|
|
500
|
-
indexName: indexNameNamespace,
|
|
501
|
-
id,
|
|
502
|
-
update: { metadata: { label: 'updated' } },
|
|
503
|
-
namespace: namespace1,
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 1);
|
|
507
|
-
|
|
508
|
-
// Query to verify update
|
|
509
|
-
const results = await vectorDB.query({
|
|
510
|
-
indexName: indexNameNamespace,
|
|
511
|
-
queryVector: testVector,
|
|
512
|
-
namespace: namespace1,
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
expect(results).toHaveLength(1);
|
|
516
|
-
expect(results[0]?.metadata?.label).toBe('updated');
|
|
517
|
-
}, 500000);
|
|
518
|
-
|
|
519
|
-
it('should delete vectors from specific namespace', async () => {
|
|
520
|
-
const [id] = await vectorDB.upsert({
|
|
521
|
-
indexName: indexNameNamespace,
|
|
522
|
-
vectors: [testVector],
|
|
523
|
-
metadata: [testMetadata],
|
|
524
|
-
namespace: namespace1,
|
|
525
|
-
});
|
|
526
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 1);
|
|
527
|
-
|
|
528
|
-
// Delete from namespace1
|
|
529
|
-
await vectorDB.deleteVector({ indexName: indexNameNamespace, id, namespace: namespace1 });
|
|
530
|
-
|
|
531
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 0, true);
|
|
532
|
-
|
|
533
|
-
// Query to verify deletion
|
|
534
|
-
const results = await vectorDB.query({
|
|
535
|
-
indexName: indexNameNamespace,
|
|
536
|
-
queryVector: testVector,
|
|
537
|
-
namespace: namespace1,
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
expect(results.length).toBe(0);
|
|
541
|
-
}, 500000);
|
|
542
|
-
|
|
543
|
-
it('should show namespace stats in describeIndex', async () => {
|
|
544
|
-
await vectorDB.upsert({
|
|
545
|
-
indexName: indexNameNamespace,
|
|
546
|
-
vectors: [testVector],
|
|
547
|
-
metadata: [testMetadata],
|
|
548
|
-
namespace: namespace1,
|
|
549
|
-
});
|
|
550
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 1);
|
|
551
|
-
await vectorDB.upsert({
|
|
552
|
-
indexName: indexNameNamespace,
|
|
553
|
-
vectors: [testVector],
|
|
554
|
-
metadata: [{ ...testMetadata, label: 'test2' }],
|
|
555
|
-
namespace: namespace2,
|
|
556
|
-
});
|
|
557
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameNamespace, 2);
|
|
558
|
-
|
|
559
|
-
const stats = await vectorDB.describeIndex({ indexName: indexNameNamespace });
|
|
560
|
-
expect(stats.namespaces).toBeDefined();
|
|
561
|
-
expect(stats.namespaces?.[namespace1]).toBeDefined();
|
|
562
|
-
expect(stats.namespaces?.[namespace2]).toBeDefined();
|
|
563
|
-
expect(stats.namespaces?.[namespace1].recordCount).toBe(1);
|
|
564
|
-
expect(stats.namespaces?.[namespace2].recordCount).toBe(1);
|
|
565
|
-
}, 500000);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
describe('Error Handling', () => {
|
|
569
|
-
const testIndexName = 'test-index-error';
|
|
570
|
-
beforeAll(async () => {
|
|
571
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
afterAll(async () => {
|
|
575
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
576
|
-
});
|
|
577
|
-
it('should handle non-existent index query gracefully', async () => {
|
|
578
|
-
const nonExistentIndex = 'non-existent-index';
|
|
579
|
-
await expect(vectorDB.query({ indexName: nonExistentIndex, queryVector: [1, 0, 0] })).rejects.toThrow();
|
|
580
|
-
}, 500000);
|
|
581
|
-
|
|
582
|
-
it('should handle incorrect dimension vectors', async () => {
|
|
583
|
-
const wrongDimVector = [[1, 0]]; // 2D vector for 3D index
|
|
584
|
-
await expect(vectorDB.upsert({ indexName: testIndexName, vectors: wrongDimVector })).rejects.toThrow();
|
|
585
|
-
}, 500000);
|
|
586
|
-
|
|
587
|
-
it('should handle duplicate index creation gracefully', async () => {
|
|
588
|
-
const duplicateIndexName = `duplicate-test`;
|
|
589
|
-
const dimension = 768;
|
|
590
|
-
const infoSpy = vi.spyOn(vectorDB['logger'], 'info');
|
|
591
|
-
const warnSpy = vi.spyOn(vectorDB['logger'], 'warn');
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
// Create index first time
|
|
595
|
-
await vectorDB.createIndex({
|
|
596
|
-
indexName: duplicateIndexName,
|
|
597
|
-
dimension,
|
|
598
|
-
metric: 'cosine',
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
// Try to create with same dimensions - should not throw
|
|
602
|
-
await expect(
|
|
603
|
-
vectorDB.createIndex({
|
|
604
|
-
indexName: duplicateIndexName,
|
|
605
|
-
dimension,
|
|
606
|
-
metric: 'cosine',
|
|
607
|
-
}),
|
|
608
|
-
).resolves.not.toThrow();
|
|
609
|
-
|
|
610
|
-
expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('already exists with'));
|
|
611
|
-
|
|
612
|
-
// Try to create with same dimensions and different metric - should not throw
|
|
613
|
-
await expect(
|
|
614
|
-
vectorDB.createIndex({
|
|
615
|
-
indexName: duplicateIndexName,
|
|
616
|
-
dimension,
|
|
617
|
-
metric: 'euclidean',
|
|
618
|
-
}),
|
|
619
|
-
).resolves.not.toThrow();
|
|
620
|
-
|
|
621
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Attempted to create index with metric'));
|
|
622
|
-
|
|
623
|
-
// Try to create with different dimensions - should throw
|
|
624
|
-
await expect(
|
|
625
|
-
vectorDB.createIndex({
|
|
626
|
-
indexName: duplicateIndexName,
|
|
627
|
-
dimension: dimension + 1,
|
|
628
|
-
metric: 'cosine',
|
|
629
|
-
}),
|
|
630
|
-
).rejects.toThrow(
|
|
631
|
-
`Index "${duplicateIndexName}" already exists with ${dimension} dimensions, but ${dimension + 1} dimensions were requested`,
|
|
632
|
-
);
|
|
633
|
-
} finally {
|
|
634
|
-
infoSpy.mockRestore();
|
|
635
|
-
warnSpy.mockRestore();
|
|
636
|
-
// Cleanup
|
|
637
|
-
await vectorDB.deleteIndex({ indexName: duplicateIndexName });
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
describe('Performance Tests', () => {
|
|
643
|
-
it('should handle batch upsert of 1000 vectors', async () => {
|
|
644
|
-
const batchSize = 1000;
|
|
645
|
-
const vectors = Array(batchSize)
|
|
646
|
-
.fill(null)
|
|
647
|
-
.map(() =>
|
|
648
|
-
Array(dimension)
|
|
649
|
-
.fill(null)
|
|
650
|
-
.map(() => Math.random()),
|
|
651
|
-
);
|
|
652
|
-
const metadata = vectors.map((_, i) => ({ id: i }));
|
|
653
|
-
|
|
654
|
-
const start = Date.now();
|
|
655
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
|
|
656
|
-
const duration = Date.now() - start;
|
|
657
|
-
|
|
658
|
-
expect(ids).toHaveLength(batchSize);
|
|
659
|
-
console.log(`Batch upsert of ${batchSize} vectors took ${duration}ms`);
|
|
660
|
-
}, 300000); // 5 minute timeout
|
|
661
|
-
|
|
662
|
-
it('should perform multiple concurrent queries', async () => {
|
|
663
|
-
const queryVector = [1, 0, 0];
|
|
664
|
-
const numQueries = 10;
|
|
665
|
-
|
|
666
|
-
const start = Date.now();
|
|
667
|
-
const promises = Array(numQueries)
|
|
668
|
-
.fill(null)
|
|
669
|
-
.map(() => vectorDB.query({ indexName: testIndexName, queryVector }));
|
|
670
|
-
|
|
671
|
-
const results = await Promise.all(promises);
|
|
672
|
-
const duration = Date.now() - start;
|
|
673
|
-
|
|
674
|
-
expect(results).toHaveLength(numQueries);
|
|
675
|
-
console.log(`${numQueries} concurrent queries took ${duration}ms`);
|
|
676
|
-
}, 500000);
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
describe('Filter Validation in Queries', () => {
|
|
680
|
-
it('rejects queries with null values', async () => {
|
|
681
|
-
await expect(
|
|
682
|
-
vectorDB.query({ indexName: testIndexName, queryVector: [1, 0, 0], topK: 10, filter: { field: null } }),
|
|
683
|
-
).rejects.toThrow();
|
|
684
|
-
|
|
685
|
-
await expect(
|
|
686
|
-
vectorDB.query({
|
|
687
|
-
indexName: testIndexName,
|
|
688
|
-
queryVector: [1, 0, 0],
|
|
689
|
-
filter: { other: { $eq: null } },
|
|
690
|
-
}),
|
|
691
|
-
).rejects.toThrow('the $eq operator must be followed by a string, boolean or a number, got null instead');
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
it('rejects invalid array operator values', async () => {
|
|
695
|
-
// Test non-undefined values
|
|
696
|
-
const invalidValues = [123, 'string', true, { key: 'value' }, null];
|
|
697
|
-
for (const op of ['$in', '$nin']) {
|
|
698
|
-
for (const val of invalidValues) {
|
|
699
|
-
await expect(
|
|
700
|
-
vectorDB.query({
|
|
701
|
-
indexName: testIndexName,
|
|
702
|
-
queryVector: [1, 0, 0],
|
|
703
|
-
filter: { field: { [op]: val } },
|
|
704
|
-
}),
|
|
705
|
-
).rejects.toThrow(`the ${op} operator must be followed by a list of strings or a list of numbers`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
it('validates comparison operators', async () => {
|
|
711
|
-
const numOps = ['$gt', '$gte', '$lt', '$lte'];
|
|
712
|
-
const invalidNumericValues = ['not-a-number', true, [], {}, null]; // Removed undefined
|
|
713
|
-
for (const op of numOps) {
|
|
714
|
-
for (const val of invalidNumericValues) {
|
|
715
|
-
await expect(
|
|
716
|
-
vectorDB.query({
|
|
717
|
-
indexName: testIndexName,
|
|
718
|
-
queryVector: [1, 0, 0],
|
|
719
|
-
filter: { field: { [op]: val } },
|
|
720
|
-
}),
|
|
721
|
-
).rejects.toThrow(`the ${op} operator must be followed by a number`);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it('rejects multiple invalid values', async () => {
|
|
727
|
-
await expect(
|
|
728
|
-
vectorDB.query({
|
|
729
|
-
indexName: testIndexName,
|
|
730
|
-
queryVector: [1, 0, 0],
|
|
731
|
-
filter: {
|
|
732
|
-
field1: { $in: 'not-array' },
|
|
733
|
-
field2: { $exists: 'not-boolean' },
|
|
734
|
-
field3: { $gt: 'not-number' },
|
|
735
|
-
} as any,
|
|
736
|
-
}),
|
|
737
|
-
).rejects.toThrow();
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
it('rejects invalid array values', async () => {
|
|
741
|
-
await expect(
|
|
742
|
-
vectorDB.query({
|
|
743
|
-
indexName: testIndexName,
|
|
744
|
-
queryVector: [1, 0, 0],
|
|
745
|
-
filter: { field: { $in: [null] } },
|
|
746
|
-
}),
|
|
747
|
-
).rejects.toThrow('the $in operator must be followed by a list of strings or a list of numbers');
|
|
748
|
-
|
|
749
|
-
await expect(
|
|
750
|
-
vectorDB.query({
|
|
751
|
-
indexName: testIndexName,
|
|
752
|
-
queryVector: [1, 0, 0],
|
|
753
|
-
filter: { field: { $in: [undefined] } },
|
|
754
|
-
}),
|
|
755
|
-
).rejects.toThrow('the $in operator must be followed by a list of strings or a list of numbers');
|
|
756
|
-
|
|
757
|
-
await expect(
|
|
758
|
-
vectorDB.query({
|
|
759
|
-
indexName: testIndexName,
|
|
760
|
-
queryVector: [1, 0, 0],
|
|
761
|
-
filter: { field: { $all: 'not-an-array' } } as any,
|
|
762
|
-
}),
|
|
763
|
-
).rejects.toThrow('A non-empty array is required for the $all operator');
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
it('handles empty object filters', async () => {
|
|
767
|
-
// Test empty object at top level
|
|
768
|
-
await expect(
|
|
769
|
-
vectorDB.query({
|
|
770
|
-
indexName: testIndexName,
|
|
771
|
-
queryVector: [1, 0, 0],
|
|
772
|
-
filter: { field: { $eq: {} } },
|
|
773
|
-
}),
|
|
774
|
-
).rejects.toThrow('the $eq operator must be followed by a string, boolean or a number, got {} instead');
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
it('handles empty/undefined filters by returning all results', async () => {
|
|
778
|
-
// Empty objects and undefined are ignored by Pinecone
|
|
779
|
-
// and will return all results without filtering
|
|
780
|
-
const noFilterCases = [{ field: {} }, { field: undefined }, { field: { $in: undefined } }];
|
|
781
|
-
|
|
782
|
-
for (const filter of noFilterCases) {
|
|
783
|
-
const results = await vectorDB.query({
|
|
784
|
-
indexName: testIndexName,
|
|
785
|
-
queryVector: [1, 0, 0],
|
|
786
|
-
filter,
|
|
787
|
-
});
|
|
788
|
-
expect(results.length).toBeGreaterThan(0);
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
it('handles empty object filters', async () => {
|
|
792
|
-
// Test empty object at top level
|
|
793
|
-
await expect(
|
|
794
|
-
vectorDB.query({
|
|
795
|
-
indexName: testIndexName,
|
|
796
|
-
queryVector: [1, 0, 0],
|
|
797
|
-
filter: {},
|
|
798
|
-
}),
|
|
799
|
-
).rejects.toThrow('You must enter a `filter` object with at least one key-value pair.');
|
|
800
|
-
});
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
describe('Metadata Filter Tests', () => {
|
|
804
|
-
const testVectors = [
|
|
805
|
-
[1.0, 0.0, 0.0],
|
|
806
|
-
[0.0, 1.0, 0.0],
|
|
807
|
-
[0.0, 0.0, 1.0],
|
|
808
|
-
[0.5, 0.5, 0.0],
|
|
809
|
-
[0.3, 0.3, 0.3],
|
|
810
|
-
[0.8, 0.1, 0.1],
|
|
811
|
-
[0.1, 0.8, 0.1],
|
|
812
|
-
[0.1, 0.1, 0.8],
|
|
813
|
-
];
|
|
814
|
-
|
|
815
|
-
const testMetadata = [
|
|
816
|
-
{ category: 'electronics', price: 1000, tags: ['premium', 'new'], inStock: true, rating: 4.5 },
|
|
817
|
-
{ category: 'books', price: 50, tags: ['bestseller'], inStock: true, rating: 4.8 },
|
|
818
|
-
{ category: 'electronics', price: 500, tags: ['refurbished'], inStock: false, rating: 4.0 },
|
|
819
|
-
{ category: 'clothing', price: 75, tags: ['summer', 'sale'], inStock: true, rating: 4.2 },
|
|
820
|
-
{ category: 'books', price: 30, tags: ['paperback', 'sale'], inStock: true, rating: 4.1 },
|
|
821
|
-
{ category: 'electronics', price: 800, tags: ['premium'], inStock: true, rating: 4.7 },
|
|
822
|
-
{ category: 'clothing', price: 150, tags: ['premium', 'new'], inStock: false, rating: 4.4 },
|
|
823
|
-
{ category: 'books', price: 25, tags: ['paperback', 'bestseller'], inStock: true, rating: 4.3 },
|
|
824
|
-
];
|
|
825
|
-
|
|
826
|
-
beforeAll(async () => {
|
|
827
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors, metadata: testMetadata });
|
|
828
|
-
// Wait for vectors to be indexed
|
|
829
|
-
await waitUntilVectorsIndexed(vectorDB, testIndexName, testVectors.length);
|
|
830
|
-
}, 500000);
|
|
831
|
-
|
|
832
|
-
describe('Comparison Operators', () => {
|
|
833
|
-
it('should filter with implict $eq', async () => {
|
|
834
|
-
const results = await vectorDB.query({
|
|
835
|
-
indexName: testIndexName,
|
|
836
|
-
queryVector: [1, 0, 0],
|
|
837
|
-
filter: { category: 'electronics' },
|
|
838
|
-
});
|
|
839
|
-
expect(results.length).toBeGreaterThan(0);
|
|
840
|
-
results.forEach(result => {
|
|
841
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
842
|
-
});
|
|
843
|
-
});
|
|
844
|
-
it('should filter with $eq operator', async () => {
|
|
845
|
-
const results = await vectorDB.query({
|
|
846
|
-
indexName: testIndexName,
|
|
847
|
-
queryVector: [1, 0, 0],
|
|
848
|
-
filter: { category: { $eq: 'electronics' } },
|
|
849
|
-
});
|
|
850
|
-
expect(results.length).toBeGreaterThan(0);
|
|
851
|
-
results.forEach(result => {
|
|
852
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
853
|
-
});
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it('should filter with $gt operator', async () => {
|
|
857
|
-
const results = await vectorDB.query({
|
|
858
|
-
indexName: testIndexName,
|
|
859
|
-
queryVector: [1, 0, 0],
|
|
860
|
-
filter: { price: { $gt: 500 } },
|
|
861
|
-
});
|
|
862
|
-
expect(results.length).toBeGreaterThan(0);
|
|
863
|
-
results.forEach(result => {
|
|
864
|
-
expect(Number(result.metadata?.price)).toBeGreaterThan(500);
|
|
865
|
-
});
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
it('should filter with $gte operator', async () => {
|
|
869
|
-
const results = await vectorDB.query({
|
|
870
|
-
indexName: testIndexName,
|
|
871
|
-
queryVector: [1, 0, 0],
|
|
872
|
-
filter: { price: { $gte: 500 } },
|
|
873
|
-
});
|
|
874
|
-
expect(results.length).toBeGreaterThan(0);
|
|
875
|
-
results.forEach(result => {
|
|
876
|
-
expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
|
|
877
|
-
});
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
it('should filter with $lt operator', async () => {
|
|
881
|
-
const results = await vectorDB.query({
|
|
882
|
-
indexName: testIndexName,
|
|
883
|
-
queryVector: [1, 0, 0],
|
|
884
|
-
filter: { price: { $lt: 100 } },
|
|
885
|
-
});
|
|
886
|
-
expect(results.length).toBeGreaterThan(0);
|
|
887
|
-
results.forEach(result => {
|
|
888
|
-
expect(Number(result.metadata?.price)).toBeLessThan(100);
|
|
889
|
-
});
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
it('should filter with $lte operator', async () => {
|
|
893
|
-
const results = await vectorDB.query({
|
|
894
|
-
indexName: testIndexName,
|
|
895
|
-
queryVector: [1, 0, 0],
|
|
896
|
-
filter: { price: { $lte: 50 } },
|
|
897
|
-
});
|
|
898
|
-
expect(results.length).toBeGreaterThan(0);
|
|
899
|
-
results.forEach(result => {
|
|
900
|
-
expect(Number(result.metadata?.price)).toBeLessThanOrEqual(50);
|
|
901
|
-
});
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
it('should filter with $ne operator', async () => {
|
|
905
|
-
const results = await vectorDB.query({
|
|
906
|
-
indexName: testIndexName,
|
|
907
|
-
queryVector: [1, 0, 0],
|
|
908
|
-
filter: { category: { $ne: 'electronics' } },
|
|
909
|
-
});
|
|
910
|
-
expect(results.length).toBeGreaterThan(0);
|
|
911
|
-
results.forEach(result => {
|
|
912
|
-
expect(result.metadata?.category).not.toBe('electronics');
|
|
913
|
-
});
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
it('filters with $gte, $lt, $lte operators', async () => {
|
|
917
|
-
const results = await vectorDB.query({
|
|
918
|
-
indexName: testIndexName,
|
|
919
|
-
queryVector: [1, 0, 0],
|
|
920
|
-
filter: { price: { $gte: 25, $lte: 30 } },
|
|
921
|
-
});
|
|
922
|
-
expect(results.length).toBe(2);
|
|
923
|
-
results.forEach(result => {
|
|
924
|
-
expect(Number(result.metadata?.price)).toBeLessThanOrEqual(30);
|
|
925
|
-
expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(25);
|
|
926
|
-
});
|
|
927
|
-
});
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
describe('Array Operators', () => {
|
|
931
|
-
it('should filter with $in operator for strings', async () => {
|
|
932
|
-
const results = await vectorDB.query({
|
|
933
|
-
indexName: testIndexName,
|
|
934
|
-
queryVector: [1, 0, 0],
|
|
935
|
-
filter: { category: { $in: ['electronics', 'books'] } },
|
|
936
|
-
});
|
|
937
|
-
expect(results.length).toBeGreaterThan(0);
|
|
938
|
-
results.forEach(result => {
|
|
939
|
-
expect(['electronics', 'books']).toContain(result.metadata?.category);
|
|
940
|
-
});
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
it('should filter with $in operator for numbers', async () => {
|
|
944
|
-
const results = await vectorDB.query({
|
|
945
|
-
indexName: testIndexName,
|
|
946
|
-
queryVector: [1, 0, 0],
|
|
947
|
-
filter: { price: { $in: [50, 75, 1000] } },
|
|
948
|
-
});
|
|
949
|
-
expect(results.length).toBeGreaterThan(0);
|
|
950
|
-
results.forEach(result => {
|
|
951
|
-
expect([50, 75, 1000]).toContain(result.metadata?.price);
|
|
952
|
-
});
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it('should filter with $nin operator', async () => {
|
|
956
|
-
const results = await vectorDB.query({
|
|
957
|
-
indexName: testIndexName,
|
|
958
|
-
queryVector: [1, 0, 0],
|
|
959
|
-
filter: { category: { $nin: ['electronics', 'books'] } },
|
|
960
|
-
});
|
|
961
|
-
expect(results.length).toBeGreaterThan(0);
|
|
962
|
-
results.forEach(result => {
|
|
963
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
964
|
-
});
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
it('should filter with $all operator', async () => {
|
|
968
|
-
const results = await vectorDB.query({
|
|
969
|
-
indexName: testIndexName,
|
|
970
|
-
queryVector: [1, 0, 0],
|
|
971
|
-
filter: { tags: { $all: ['premium', 'new'] } },
|
|
972
|
-
});
|
|
973
|
-
expect(results.length).toBeGreaterThan(0);
|
|
974
|
-
results.forEach(result => {
|
|
975
|
-
expect(result.metadata?.tags).toContain('premium');
|
|
976
|
-
expect(result.metadata?.tags).toContain('new');
|
|
977
|
-
});
|
|
978
|
-
});
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
describe('Logical Operators', () => {
|
|
982
|
-
it('should filter with implict $and', async () => {
|
|
983
|
-
const results = await vectorDB.query({
|
|
984
|
-
indexName: testIndexName,
|
|
985
|
-
queryVector: [1, 0, 0],
|
|
986
|
-
filter: { category: 'electronics', price: { $gt: 700 }, inStock: true },
|
|
987
|
-
});
|
|
988
|
-
expect(results.length).toBeGreaterThan(0);
|
|
989
|
-
results.forEach(result => {
|
|
990
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
991
|
-
expect(Number(result.metadata?.price)).toBeGreaterThan(700);
|
|
992
|
-
expect(result.metadata?.inStock).toBe(true);
|
|
993
|
-
});
|
|
994
|
-
});
|
|
995
|
-
it('should filter with $and operator', async () => {
|
|
996
|
-
const results = await vectorDB.query({
|
|
997
|
-
indexName: testIndexName,
|
|
998
|
-
queryVector: [1, 0, 0],
|
|
999
|
-
filter: { $and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { inStock: true }] },
|
|
1000
|
-
});
|
|
1001
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1002
|
-
results.forEach(result => {
|
|
1003
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1004
|
-
expect(Number(result.metadata?.price)).toBeGreaterThan(700);
|
|
1005
|
-
expect(result.metadata?.inStock).toBe(true);
|
|
1006
|
-
});
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
it('should filter with $or operator', async () => {
|
|
1010
|
-
const results = await vectorDB.query({
|
|
1011
|
-
indexName: testIndexName,
|
|
1012
|
-
queryVector: [1, 0, 0],
|
|
1013
|
-
filter: { $or: [{ price: { $gt: 900 } }, { tags: { $all: ['bestseller'] } }] },
|
|
1014
|
-
});
|
|
1015
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1016
|
-
results.forEach(result => {
|
|
1017
|
-
const condition1 = Number(result.metadata?.price) > 900;
|
|
1018
|
-
const condition2 = result.metadata?.tags?.includes('bestseller');
|
|
1019
|
-
expect(condition1 || condition2).toBe(true);
|
|
1020
|
-
});
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
it('should handle nested logical operators', async () => {
|
|
1024
|
-
const results = await vectorDB.query({
|
|
1025
|
-
indexName: testIndexName,
|
|
1026
|
-
queryVector: [1, 0, 0],
|
|
1027
|
-
filter: {
|
|
1028
|
-
$and: [
|
|
1029
|
-
{
|
|
1030
|
-
$or: [{ category: 'electronics' }, { category: 'books' }],
|
|
1031
|
-
},
|
|
1032
|
-
{ price: { $lt: 100 } },
|
|
1033
|
-
{ inStock: true },
|
|
1034
|
-
],
|
|
1035
|
-
},
|
|
1036
|
-
});
|
|
1037
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1038
|
-
results.forEach(result => {
|
|
1039
|
-
expect(['electronics', 'books']).toContain(result.metadata?.category);
|
|
1040
|
-
expect(Number(result.metadata?.price)).toBeLessThan(100);
|
|
1041
|
-
expect(result.metadata?.inStock).toBe(true);
|
|
1042
|
-
});
|
|
1043
|
-
});
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
describe('Complex Filter Combinations', () => {
|
|
1047
|
-
it('should combine comparison and array operators', async () => {
|
|
1048
|
-
const results = await vectorDB.query({
|
|
1049
|
-
indexName: testIndexName,
|
|
1050
|
-
queryVector: [1, 0, 0],
|
|
1051
|
-
filter: { $and: [{ price: { $gte: 500 } }, { tags: { $in: ['premium', 'refurbished'] } }] },
|
|
1052
|
-
});
|
|
1053
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1054
|
-
results.forEach(result => {
|
|
1055
|
-
expect(Number(result.metadata?.price)).toBeGreaterThanOrEqual(500);
|
|
1056
|
-
expect(result.metadata?.tags?.some(tag => ['premium', 'refurbished'].includes(tag))).toBe(true);
|
|
1057
|
-
});
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
it('should handle multiple conditions on same field', async () => {
|
|
1061
|
-
const results = await vectorDB.query({
|
|
1062
|
-
indexName: testIndexName,
|
|
1063
|
-
queryVector: [1, 0, 0],
|
|
1064
|
-
filter: { $and: [{ price: { $gte: 30 } }, { price: { $lte: 800 } }] },
|
|
1065
|
-
});
|
|
1066
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1067
|
-
results.forEach(result => {
|
|
1068
|
-
const price = Number(result.metadata?.price);
|
|
1069
|
-
expect(price).toBeGreaterThanOrEqual(30);
|
|
1070
|
-
expect(price).toBeLessThanOrEqual(800);
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
it('should handle complex nested conditions', async () => {
|
|
1075
|
-
const results = await vectorDB.query({
|
|
1076
|
-
indexName: testIndexName,
|
|
1077
|
-
queryVector: [1, 0, 0],
|
|
1078
|
-
filter: {
|
|
1079
|
-
$or: [
|
|
1080
|
-
{
|
|
1081
|
-
$and: [{ category: 'electronics' }, { price: { $gt: 700 } }, { tags: { $all: ['premium'] } }],
|
|
1082
|
-
},
|
|
1083
|
-
{
|
|
1084
|
-
$and: [{ category: 'books' }, { price: { $lt: 50 } }, { tags: { $in: ['paperback'] } }],
|
|
1085
|
-
},
|
|
1086
|
-
],
|
|
1087
|
-
},
|
|
1088
|
-
});
|
|
1089
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1090
|
-
results.forEach(result => {
|
|
1091
|
-
const isExpensiveElectronics =
|
|
1092
|
-
result.metadata?.category === 'electronics' &&
|
|
1093
|
-
Number(result.metadata?.price) > 700 &&
|
|
1094
|
-
result.metadata?.tags?.includes('premium');
|
|
1095
|
-
|
|
1096
|
-
const isCheapBook =
|
|
1097
|
-
result.metadata?.category === 'books' &&
|
|
1098
|
-
Number(result.metadata?.price) < 50 &&
|
|
1099
|
-
result.metadata?.tags?.includes('paperback');
|
|
1100
|
-
|
|
1101
|
-
expect(isExpensiveElectronics || isCheapBook).toBe(true);
|
|
1102
|
-
});
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
it('combines existence checks with other operators', async () => {
|
|
1106
|
-
const results = await vectorDB.query({
|
|
1107
|
-
indexName: testIndexName,
|
|
1108
|
-
queryVector: [1, 0, 0],
|
|
1109
|
-
filter: { $and: [{ category: 'clothing' }, { optionalField: { $exists: false } }] },
|
|
1110
|
-
});
|
|
1111
|
-
expect(results.length).toBe(2);
|
|
1112
|
-
expect(results[0]!.metadata!.category).toBe('clothing');
|
|
1113
|
-
expect('optionalField' in results[0]!.metadata!).toBe(false);
|
|
1114
|
-
});
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
describe('Edge Cases', () => {
|
|
1118
|
-
it('should handle numeric comparisons with decimals', async () => {
|
|
1119
|
-
const results = await vectorDB.query({
|
|
1120
|
-
indexName: testIndexName,
|
|
1121
|
-
queryVector: [1, 0, 0],
|
|
1122
|
-
filter: { rating: { $gt: 4.5 } },
|
|
1123
|
-
});
|
|
1124
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1125
|
-
results.forEach(result => {
|
|
1126
|
-
expect(Number(result.metadata?.rating)).toBeGreaterThan(4.5);
|
|
1127
|
-
});
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
it('should handle boolean values', async () => {
|
|
1131
|
-
const results = await vectorDB.query({
|
|
1132
|
-
indexName: testIndexName,
|
|
1133
|
-
queryVector: [1, 0, 0],
|
|
1134
|
-
filter: { inStock: { $eq: false } },
|
|
1135
|
-
});
|
|
1136
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1137
|
-
results.forEach(result => {
|
|
1138
|
-
expect(result.metadata?.inStock).toBe(false);
|
|
1139
|
-
});
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
it('should handle empty array in $in operator', async () => {
|
|
1143
|
-
const results = await vectorDB.query({
|
|
1144
|
-
indexName: testIndexName,
|
|
1145
|
-
queryVector: [1, 0, 0],
|
|
1146
|
-
filter: { category: { $in: [] } },
|
|
1147
|
-
});
|
|
1148
|
-
expect(results).toHaveLength(0);
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
it('should handle single value in $all operator', async () => {
|
|
1152
|
-
const results = await vectorDB.query({
|
|
1153
|
-
indexName: testIndexName,
|
|
1154
|
-
queryVector: [1, 0, 0],
|
|
1155
|
-
filter: { tags: { $all: ['premium'] } },
|
|
1156
|
-
});
|
|
1157
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1158
|
-
results.forEach(result => {
|
|
1159
|
-
expect(result.metadata?.tags).toContain('premium');
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
});
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
describe('Additional Validation Tests', () => {
|
|
1166
|
-
it('should reject non-numeric values in numeric comparisons', async () => {
|
|
1167
|
-
await expect(
|
|
1168
|
-
vectorDB.query({
|
|
1169
|
-
indexName: testIndexName,
|
|
1170
|
-
queryVector: [1, 0, 0],
|
|
1171
|
-
filter: { price: { $gt: '500' } } as any, // string instead of number
|
|
1172
|
-
}),
|
|
1173
|
-
).rejects.toThrow('the $gt operator must be followed by a number');
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
it('should reject invalid types in $in operator', async () => {
|
|
1177
|
-
await expect(
|
|
1178
|
-
vectorDB.query({
|
|
1179
|
-
indexName: testIndexName,
|
|
1180
|
-
queryVector: [1, 0, 0],
|
|
1181
|
-
filter: { price: { $in: [true, false] } }, // booleans instead of numbers
|
|
1182
|
-
}),
|
|
1183
|
-
).rejects.toThrow('the $in operator must be followed by a list of strings or a list of numbers');
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
it('should reject mixed types in $in operator', async () => {
|
|
1187
|
-
await expect(
|
|
1188
|
-
vectorDB.query({
|
|
1189
|
-
indexName: testIndexName,
|
|
1190
|
-
queryVector: [1, 0, 0],
|
|
1191
|
-
filter: { field: { $in: ['string', 123] } }, // mixed string and number
|
|
1192
|
-
}),
|
|
1193
|
-
).rejects.toThrow();
|
|
1194
|
-
});
|
|
1195
|
-
it('should handle undefined filter', async () => {
|
|
1196
|
-
const results1 = await vectorDB.query({
|
|
1197
|
-
indexName: testIndexName,
|
|
1198
|
-
queryVector: [1, 0, 0],
|
|
1199
|
-
filter: undefined,
|
|
1200
|
-
});
|
|
1201
|
-
const results2 = await vectorDB.query({
|
|
1202
|
-
indexName: testIndexName,
|
|
1203
|
-
queryVector: [1, 0, 0],
|
|
1204
|
-
});
|
|
1205
|
-
expect(results1).toEqual(results2);
|
|
1206
|
-
expect(results1.length).toBeGreaterThan(0);
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
it('should handle null filter', async () => {
|
|
1210
|
-
const results = await vectorDB.query({
|
|
1211
|
-
indexName: testIndexName,
|
|
1212
|
-
queryVector: [1, 0, 0],
|
|
1213
|
-
filter: null,
|
|
1214
|
-
});
|
|
1215
|
-
const results2 = await vectorDB.query({
|
|
1216
|
-
indexName: testIndexName,
|
|
1217
|
-
queryVector: [1, 0, 0],
|
|
1218
|
-
});
|
|
1219
|
-
expect(results).toEqual(results2);
|
|
1220
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1221
|
-
});
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
describe('Additional Edge Cases', () => {
|
|
1225
|
-
it('should handle exact boundary conditions', async () => {
|
|
1226
|
-
// Test exact boundary values from our test data
|
|
1227
|
-
const results = await vectorDB.query({
|
|
1228
|
-
indexName: testIndexName,
|
|
1229
|
-
queryVector: [1, 0, 0],
|
|
1230
|
-
filter: { $and: [{ price: { $gte: 25 } }, { price: { $lte: 1000 } }] },
|
|
1231
|
-
});
|
|
1232
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1233
|
-
// Should include both boundary values
|
|
1234
|
-
expect(results.some(r => r.metadata?.price === 25)).toBe(true);
|
|
1235
|
-
expect(results.some(r => r.metadata?.price === 1000)).toBe(true);
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
it('should handle multiple $all conditions on same array field', async () => {
|
|
1239
|
-
const results = await vectorDB.query({
|
|
1240
|
-
indexName: testIndexName,
|
|
1241
|
-
queryVector: [1, 0, 0],
|
|
1242
|
-
filter: { $and: [{ tags: { $all: ['premium'] } }, { tags: { $all: ['new'] } }] },
|
|
1243
|
-
});
|
|
1244
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1245
|
-
results.forEach(result => {
|
|
1246
|
-
expect(result.metadata?.tags).toContain('premium');
|
|
1247
|
-
expect(result.metadata?.tags).toContain('new');
|
|
1248
|
-
});
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
it('should handle multiple array operator combinations', async () => {
|
|
1252
|
-
const results = await vectorDB.query({
|
|
1253
|
-
indexName: testIndexName,
|
|
1254
|
-
queryVector: [1, 0, 0],
|
|
1255
|
-
filter: { $and: [{ tags: { $all: ['premium'] } }, { tags: { $in: ['new', 'refurbished'] } }] },
|
|
1256
|
-
});
|
|
1257
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1258
|
-
results.forEach(result => {
|
|
1259
|
-
expect(result.metadata?.tags).toContain('premium');
|
|
1260
|
-
expect(result.metadata?.tags?.some(tag => ['new', 'refurbished'].includes(tag))).toBe(true);
|
|
1261
|
-
});
|
|
1262
|
-
});
|
|
1263
|
-
});
|
|
1264
|
-
|
|
1265
|
-
describe('Additional Complex Logical Combinations', () => {
|
|
1266
|
-
it('should handle deeply nested $or conditions', async () => {
|
|
1267
|
-
const results = await vectorDB.query({
|
|
1268
|
-
indexName: testIndexName,
|
|
1269
|
-
queryVector: [1, 0, 0],
|
|
1270
|
-
filter: {
|
|
1271
|
-
$or: [
|
|
1272
|
-
{
|
|
1273
|
-
$and: [{ category: 'electronics' }, { $or: [{ price: { $gt: 900 } }, { tags: { $all: ['premium'] } }] }],
|
|
1274
|
-
},
|
|
1275
|
-
{
|
|
1276
|
-
$and: [{ category: 'books' }, { $or: [{ price: { $lt: 30 } }, { tags: { $all: ['bestseller'] } }] }],
|
|
1277
|
-
},
|
|
1278
|
-
],
|
|
1279
|
-
},
|
|
1280
|
-
});
|
|
1281
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1282
|
-
results.forEach(result => {
|
|
1283
|
-
if (result.metadata?.category === 'electronics') {
|
|
1284
|
-
expect(Number(result.metadata?.price) > 900 || result.metadata?.tags?.includes('premium')).toBe(true);
|
|
1285
|
-
} else if (result.metadata?.category === 'books') {
|
|
1286
|
-
expect(Number(result.metadata?.price) < 30 || result.metadata?.tags?.includes('bestseller')).toBe(true);
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
it('should handle multiple field comparisons with same value', async () => {
|
|
1292
|
-
const results = await vectorDB.query({
|
|
1293
|
-
indexName: testIndexName,
|
|
1294
|
-
queryVector: [1, 0, 0],
|
|
1295
|
-
filter: { $or: [{ price: { $gt: 500 } }, { rating: { $gt: 4.5 } }] },
|
|
1296
|
-
});
|
|
1297
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1298
|
-
results.forEach(result => {
|
|
1299
|
-
expect(Number(result.metadata?.price) > 500 || Number(result.metadata?.rating) > 4.5).toBe(true);
|
|
1300
|
-
});
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
it('should handle combination of array and numeric comparisons', async () => {
|
|
1304
|
-
const results = await vectorDB.query({
|
|
1305
|
-
indexName: testIndexName,
|
|
1306
|
-
queryVector: [1, 0, 0],
|
|
1307
|
-
filter: {
|
|
1308
|
-
$and: [
|
|
1309
|
-
{ tags: { $in: ['premium', 'bestseller'] } },
|
|
1310
|
-
{ $or: [{ price: { $gt: 500 } }, { rating: { $gt: 4.5 } }] },
|
|
1311
|
-
],
|
|
1312
|
-
},
|
|
1313
|
-
});
|
|
1314
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1315
|
-
results.forEach(result => {
|
|
1316
|
-
expect(['premium', 'bestseller'].some(tag => result.metadata?.tags?.includes(tag))).toBe(true);
|
|
1317
|
-
expect(Number(result.metadata?.price) > 500 || Number(result.metadata?.rating) > 4.5).toBe(true);
|
|
1318
|
-
});
|
|
1319
|
-
});
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
describe('Performance Edge Cases', () => {
|
|
1323
|
-
it('should handle filters with many conditions', async () => {
|
|
1324
|
-
const results = await vectorDB.query({
|
|
1325
|
-
indexName: testIndexName,
|
|
1326
|
-
queryVector: [1, 0, 0],
|
|
1327
|
-
filter: {
|
|
1328
|
-
$and: Array(10)
|
|
1329
|
-
.fill(null)
|
|
1330
|
-
.map(() => ({
|
|
1331
|
-
$or: [{ price: { $gt: 100 } }, { rating: { $gt: 4.0 } }],
|
|
1332
|
-
})),
|
|
1333
|
-
},
|
|
1334
|
-
});
|
|
1335
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1336
|
-
results.forEach(result => {
|
|
1337
|
-
expect(Number(result.metadata?.price) > 100 || Number(result.metadata?.rating) > 4.0).toBe(true);
|
|
1338
|
-
});
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
it('should handle deeply nested conditions efficiently', async () => {
|
|
1342
|
-
const results = await vectorDB.query({
|
|
1343
|
-
indexName: testIndexName,
|
|
1344
|
-
queryVector: [1, 0, 0],
|
|
1345
|
-
filter: {
|
|
1346
|
-
$or: Array(5)
|
|
1347
|
-
.fill(null)
|
|
1348
|
-
.map(() => ({
|
|
1349
|
-
$and: [{ category: { $in: ['electronics', 'books'] } }, { price: { $gt: 50 } }, { rating: { $gt: 4.0 } }],
|
|
1350
|
-
})),
|
|
1351
|
-
},
|
|
1352
|
-
});
|
|
1353
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1354
|
-
results.forEach(result => {
|
|
1355
|
-
expect(['electronics', 'books']).toContain(result.metadata?.category);
|
|
1356
|
-
expect(Number(result.metadata?.price)).toBeGreaterThan(50);
|
|
1357
|
-
expect(Number(result.metadata?.rating)).toBeGreaterThan(4.0);
|
|
1358
|
-
});
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
it('should handle large number of $or conditions', async () => {
|
|
1362
|
-
const results = await vectorDB.query({
|
|
1363
|
-
indexName: testIndexName,
|
|
1364
|
-
queryVector: [1, 0, 0],
|
|
1365
|
-
filter: {
|
|
1366
|
-
$or: [
|
|
1367
|
-
...Array(5)
|
|
1368
|
-
.fill(null)
|
|
1369
|
-
.map((_, i) => ({
|
|
1370
|
-
price: { $gt: i * 100 },
|
|
1371
|
-
})),
|
|
1372
|
-
...Array(5)
|
|
1373
|
-
.fill(null)
|
|
1374
|
-
.map((_, i) => ({
|
|
1375
|
-
rating: { $gt: 4.0 + i * 0.1 },
|
|
1376
|
-
})),
|
|
1377
|
-
],
|
|
1378
|
-
},
|
|
1379
|
-
});
|
|
1380
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1381
|
-
});
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
describe('Hybrid Search Operations', () => {
|
|
1385
|
-
const testVectors = [
|
|
1386
|
-
[0.9, 0.1, 0.0], // cats (very distinct)
|
|
1387
|
-
[0.1, 0.9, 0.0], // dogs (very distinct)
|
|
1388
|
-
[0.0, 0.0, 0.9], // birds (completely different)
|
|
1389
|
-
];
|
|
1390
|
-
|
|
1391
|
-
const testMetadata = [
|
|
1392
|
-
{ text: 'cats purr and meow', animal: 'cat' },
|
|
1393
|
-
{ text: 'dogs bark and fetch', animal: 'dog' },
|
|
1394
|
-
{ text: 'birds fly and nest', animal: 'bird' },
|
|
1395
|
-
];
|
|
1396
|
-
|
|
1397
|
-
// Create sparse vectors with fixed vocabulary indices
|
|
1398
|
-
const testSparseVectors = [
|
|
1399
|
-
{ indices: [0], values: [1.0] }, // cat terms only
|
|
1400
|
-
{ indices: [1], values: [1.0] }, // dog terms only
|
|
1401
|
-
{ indices: [2], values: [1.0] }, // bird terms only
|
|
1402
|
-
];
|
|
1403
|
-
|
|
1404
|
-
beforeEach(async () => {
|
|
1405
|
-
await vectorDB.createIndex({ indexName: indexNameHybrid, dimension: 3, metric: 'dotproduct' });
|
|
1406
|
-
await waitUntilReady(vectorDB, indexNameHybrid);
|
|
1407
|
-
|
|
1408
|
-
// Upsert with both dense and sparse vectors
|
|
1409
|
-
await vectorDB.upsert({
|
|
1410
|
-
indexName: indexNameHybrid,
|
|
1411
|
-
vectors: testVectors,
|
|
1412
|
-
sparseVectors: testSparseVectors,
|
|
1413
|
-
metadata: testMetadata,
|
|
1414
|
-
});
|
|
1415
|
-
await waitUntilVectorsIndexed(vectorDB, indexNameHybrid, 3);
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
|
-
afterEach(async () => {
|
|
1419
|
-
try {
|
|
1420
|
-
await vectorDB.deleteIndex({ indexName: indexNameHybrid });
|
|
1421
|
-
await waitUntilIndexDeleted(vectorDB, indexNameHybrid);
|
|
1422
|
-
} catch {
|
|
1423
|
-
// Ignore errors if index doesn't exist
|
|
1424
|
-
}
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
it('should combine dense and sparse signals in hybrid search', async () => {
|
|
1428
|
-
// Query vector strongly favors cats
|
|
1429
|
-
const queryVector = [1.0, 0.0, 0.0];
|
|
1430
|
-
// But sparse vector strongly favors dogs
|
|
1431
|
-
const sparseVector = {
|
|
1432
|
-
indices: [1], // Index 1 corresponds to dog-related terms
|
|
1433
|
-
values: [1.0], // Maximum weight for dog terms
|
|
1434
|
-
};
|
|
1435
|
-
|
|
1436
|
-
const results = await vectorDB.query({
|
|
1437
|
-
indexName: indexNameHybrid,
|
|
1438
|
-
queryVector,
|
|
1439
|
-
sparseVector,
|
|
1440
|
-
topK: 2,
|
|
1441
|
-
});
|
|
1442
|
-
|
|
1443
|
-
expect(results).toHaveLength(2);
|
|
1444
|
-
|
|
1445
|
-
// Get results with just vector similarity
|
|
1446
|
-
const vectorResults = await vectorDB.query({
|
|
1447
|
-
indexName: indexNameHybrid,
|
|
1448
|
-
queryVector,
|
|
1449
|
-
topK: 2,
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
// Results should be different when using hybrid search vs just vector
|
|
1453
|
-
expect(results[0].id).not.toBe(vectorResults[0].id);
|
|
1454
|
-
|
|
1455
|
-
// First result should be dog due to sparse vector influence
|
|
1456
|
-
expect(results[0].metadata?.animal).toBe('dog');
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
it('should support sparse vectors as optional parameters', async () => {
|
|
1460
|
-
// Should work with just dense vectors in upsert
|
|
1461
|
-
await vectorDB.upsert({
|
|
1462
|
-
indexName: indexNameHybrid,
|
|
1463
|
-
vectors: [[0.1, 0.2, 0.3]],
|
|
1464
|
-
metadata: [{ test: 'dense only' }],
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
// Should work with just dense vector in query
|
|
1468
|
-
const denseOnlyResults = await vectorDB.query({
|
|
1469
|
-
indexName: indexNameHybrid,
|
|
1470
|
-
queryVector: [0.1, 0.2, 0.3],
|
|
1471
|
-
topK: 1,
|
|
1472
|
-
});
|
|
1473
|
-
expect(denseOnlyResults).toHaveLength(1);
|
|
1474
|
-
|
|
1475
|
-
// Should work with both dense and sparse in query
|
|
1476
|
-
const hybridResults = await vectorDB.query({
|
|
1477
|
-
indexName: indexNameHybrid,
|
|
1478
|
-
queryVector: [0.1, 0.2, 0.3],
|
|
1479
|
-
sparseVector: createSparseVector('test query'),
|
|
1480
|
-
topK: 1,
|
|
1481
|
-
});
|
|
1482
|
-
expect(hybridResults).toHaveLength(1);
|
|
1483
|
-
});
|
|
1484
|
-
});
|
|
1485
|
-
});
|