@mastra/upstash 0.12.3 → 0.12.4-alpha.1
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +46 -0
- package/README.md +98 -0
- package/dist/_tsup-dts-rollup.d.cts +30 -4
- package/dist/_tsup-dts-rollup.d.ts +30 -4
- package/dist/index.cjs +97 -36
- package/dist/index.js +97 -36
- package/package.json +5 -5
- package/src/storage/domains/memory/index.ts +70 -0
- package/src/storage/index.ts +5 -0
- package/src/vector/hybrid.test.ts +1455 -0
- package/src/vector/index.test.ts +2 -2
- package/src/vector/index.ts +40 -41
- package/src/vector/types.ts +26 -0
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
import type { QueryResult } from '@mastra/core/vector';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { UpstashVector } from './';
|
|
7
|
+
|
|
8
|
+
dotenv.config();
|
|
9
|
+
|
|
10
|
+
function waitUntilVectorsIndexed(vector: UpstashVector, indexName: string, expectedCount: number) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const maxAttempts = 30;
|
|
13
|
+
let attempts = 0;
|
|
14
|
+
const interval = setInterval(async () => {
|
|
15
|
+
try {
|
|
16
|
+
const stats = await vector.describeIndex({ indexName });
|
|
17
|
+
if (stats && stats.count >= expectedCount) {
|
|
18
|
+
clearInterval(interval);
|
|
19
|
+
resolve(true);
|
|
20
|
+
}
|
|
21
|
+
attempts++;
|
|
22
|
+
if (attempts >= maxAttempts) {
|
|
23
|
+
clearInterval(interval);
|
|
24
|
+
reject(new Error('Timeout waiting for vectors to be indexed'));
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.log(error);
|
|
28
|
+
}
|
|
29
|
+
}, 5000);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Helper function to create sparse vectors for hybrid index compatibility
|
|
35
|
+
*/
|
|
36
|
+
function _createSparseVector() {
|
|
37
|
+
return {
|
|
38
|
+
indices: [0, 1, 2, 10, 50],
|
|
39
|
+
values: [0.1, 0.2, 0.3, 0.4, 0.5],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* These tests require a real Upstash Vector instance since there is no local Docker alternative.
|
|
45
|
+
* The tests will be skipped in local development where Upstash credentials are not available.
|
|
46
|
+
* In CI/CD environments, these tests will run using the provided Upstash Vector credentials.
|
|
47
|
+
*/
|
|
48
|
+
describe.skipIf(!process.env.UPSTASH_VECTOR_URL || !process.env.UPSTASH_VECTOR_TOKEN)('UpstashVector', () => {
|
|
49
|
+
let vectorStore: UpstashVector;
|
|
50
|
+
const VECTOR_DIMENSION = 1024;
|
|
51
|
+
const testIndexName = 'default';
|
|
52
|
+
const filterIndexName = 'filter-index';
|
|
53
|
+
|
|
54
|
+
beforeAll(() => {
|
|
55
|
+
// Load from environment variables for CI/CD
|
|
56
|
+
const url = process.env.UPSTASH_VECTOR_URL;
|
|
57
|
+
const token = process.env.UPSTASH_VECTOR_TOKEN;
|
|
58
|
+
|
|
59
|
+
if (!url || !token) {
|
|
60
|
+
console.log('Skipping Upstash Vector tests - no credentials available');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
vectorStore = new UpstashVector({ url, token });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(async () => {
|
|
68
|
+
if (!vectorStore) return;
|
|
69
|
+
|
|
70
|
+
// Cleanup: delete test index
|
|
71
|
+
try {
|
|
72
|
+
await vectorStore.deleteIndex({ indexName: testIndexName });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn('Failed to delete test index:', error);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await vectorStore.deleteIndex({ indexName: filterIndexName });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn('Failed to delete filter index:', error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Vector Operations', () => {
|
|
84
|
+
// Helper function to create a normalized vector
|
|
85
|
+
const createVector = (primaryDimension: number, value: number = 1.0): number[] => {
|
|
86
|
+
const vector = new Array(VECTOR_DIMENSION).fill(0);
|
|
87
|
+
vector[primaryDimension] = value;
|
|
88
|
+
// Normalize the vector for cosine similarity
|
|
89
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
90
|
+
return vector.map(val => val / magnitude);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let vectorIds: string[];
|
|
94
|
+
|
|
95
|
+
it('should upsert vectors and query them', async () => {
|
|
96
|
+
// Create and log test vectors
|
|
97
|
+
const testVectors = [createVector(0, 1.0), createVector(1, 1.0), createVector(2, 1.0)];
|
|
98
|
+
|
|
99
|
+
const testMetadata = [{ label: 'first-dimension' }, { label: 'second-dimension' }, { label: 'third-dimension' }];
|
|
100
|
+
|
|
101
|
+
// Upsert vectors
|
|
102
|
+
vectorIds = await vectorStore.upsert({
|
|
103
|
+
indexName: testIndexName,
|
|
104
|
+
vectors: testVectors,
|
|
105
|
+
metadata: testMetadata,
|
|
106
|
+
sparseVectors: testVectors.map(() => _createSparseVector()),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(vectorIds).toHaveLength(3);
|
|
110
|
+
await waitUntilVectorsIndexed(vectorStore, testIndexName, 3);
|
|
111
|
+
|
|
112
|
+
const results = await vectorStore.query({ indexName: testIndexName, queryVector: createVector(0, 0.9), topK: 3 });
|
|
113
|
+
|
|
114
|
+
expect(results).toHaveLength(3);
|
|
115
|
+
if (results.length > 0) {
|
|
116
|
+
expect(results?.[0]?.metadata).toEqual({ label: 'first-dimension' });
|
|
117
|
+
}
|
|
118
|
+
}, 5000000);
|
|
119
|
+
|
|
120
|
+
it('should query vectors and return vector in results', async () => {
|
|
121
|
+
const results = await vectorStore.query({
|
|
122
|
+
indexName: testIndexName,
|
|
123
|
+
queryVector: createVector(0, 0.9),
|
|
124
|
+
topK: 3,
|
|
125
|
+
includeVector: true,
|
|
126
|
+
});
|
|
127
|
+
expect(results).toHaveLength(3);
|
|
128
|
+
results.forEach(result => {
|
|
129
|
+
expect(result.vector).toBeDefined();
|
|
130
|
+
expect(result.vector).toHaveLength(VECTOR_DIMENSION);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('Vector update operations', () => {
|
|
135
|
+
const testVectors = [createVector(0, 1.0), createVector(1, 1.0), createVector(2, 1.0)];
|
|
136
|
+
|
|
137
|
+
const testIndexName = 'test-index';
|
|
138
|
+
|
|
139
|
+
afterEach(async () => {
|
|
140
|
+
await vectorStore.deleteIndex({ indexName: testIndexName });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should update the vector by id', async () => {
|
|
144
|
+
const ids = await vectorStore.upsert({
|
|
145
|
+
indexName: testIndexName,
|
|
146
|
+
vectors: testVectors,
|
|
147
|
+
sparseVectors: testVectors.map(() => _createSparseVector()),
|
|
148
|
+
});
|
|
149
|
+
expect(ids).toHaveLength(3);
|
|
150
|
+
|
|
151
|
+
const idToBeUpdated = ids[0];
|
|
152
|
+
const newVector = createVector(0, 4.0);
|
|
153
|
+
const newMetaData = {
|
|
154
|
+
test: 'updates',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const update = {
|
|
158
|
+
vector: newVector,
|
|
159
|
+
sparseVector: _createSparseVector(),
|
|
160
|
+
metadata: newMetaData,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
await vectorStore.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
164
|
+
|
|
165
|
+
await waitUntilVectorsIndexed(vectorStore, testIndexName, 3);
|
|
166
|
+
|
|
167
|
+
const results: QueryResult[] = await vectorStore.query({
|
|
168
|
+
indexName: testIndexName,
|
|
169
|
+
queryVector: newVector,
|
|
170
|
+
topK: 2,
|
|
171
|
+
includeVector: true,
|
|
172
|
+
});
|
|
173
|
+
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
174
|
+
expect(results[0]?.vector).toEqual(newVector);
|
|
175
|
+
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
176
|
+
}, 500000);
|
|
177
|
+
|
|
178
|
+
it('should only update the metadata by id', async () => {
|
|
179
|
+
const ids = await vectorStore.upsert({
|
|
180
|
+
indexName: testIndexName,
|
|
181
|
+
vectors: testVectors,
|
|
182
|
+
sparseVectors: testVectors.map(() => _createSparseVector()),
|
|
183
|
+
});
|
|
184
|
+
expect(ids).toHaveLength(3);
|
|
185
|
+
|
|
186
|
+
const newMetaData = {
|
|
187
|
+
test: 'updates',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const update = {
|
|
191
|
+
metadata: newMetaData,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
await expect(vectorStore.updateVector({ indexName: testIndexName, id: 'id', update })).rejects.toThrow(
|
|
195
|
+
'Both vector and metadata must be provided for an update',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should only update vector embeddings by id', async () => {
|
|
200
|
+
const ids = await vectorStore.upsert({
|
|
201
|
+
indexName: testIndexName,
|
|
202
|
+
vectors: testVectors,
|
|
203
|
+
sparseVectors: testVectors.map(() => _createSparseVector()),
|
|
204
|
+
});
|
|
205
|
+
expect(ids).toHaveLength(3);
|
|
206
|
+
|
|
207
|
+
const idToBeUpdated = ids[0];
|
|
208
|
+
const newVector = createVector(0, 4.0);
|
|
209
|
+
|
|
210
|
+
const update = {
|
|
211
|
+
vector: newVector,
|
|
212
|
+
sparseVector: _createSparseVector(),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await vectorStore.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
216
|
+
|
|
217
|
+
await waitUntilVectorsIndexed(vectorStore, testIndexName, 3);
|
|
218
|
+
|
|
219
|
+
const results: QueryResult[] = await vectorStore.query({
|
|
220
|
+
indexName: testIndexName,
|
|
221
|
+
queryVector: newVector,
|
|
222
|
+
topK: 2,
|
|
223
|
+
includeVector: true,
|
|
224
|
+
});
|
|
225
|
+
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
226
|
+
expect(results[0]?.vector).toEqual(newVector);
|
|
227
|
+
}, 500000);
|
|
228
|
+
|
|
229
|
+
it('should throw exception when no updates are given', async () => {
|
|
230
|
+
await expect(vectorStore.updateVector({ indexName: testIndexName, id: 'id', update: {} })).rejects.toThrow(
|
|
231
|
+
'No update data provided',
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Vector delete operations', () => {
|
|
237
|
+
const testVectors = [createVector(0, 1.0), createVector(1, 1.0), createVector(2, 1.0)];
|
|
238
|
+
|
|
239
|
+
afterEach(async () => {
|
|
240
|
+
await vectorStore.deleteIndex({ indexName: testIndexName });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should delete the vector by id', async () => {
|
|
244
|
+
const ids = await vectorStore.upsert({
|
|
245
|
+
indexName: testIndexName,
|
|
246
|
+
vectors: testVectors,
|
|
247
|
+
sparseVectors: testVectors.map(() => _createSparseVector()),
|
|
248
|
+
});
|
|
249
|
+
expect(ids).toHaveLength(3);
|
|
250
|
+
const idToBeDeleted = ids[0];
|
|
251
|
+
|
|
252
|
+
await vectorStore.deleteVector({ indexName: testIndexName, id: idToBeDeleted });
|
|
253
|
+
|
|
254
|
+
const results: QueryResult[] = await vectorStore.query({
|
|
255
|
+
indexName: testIndexName,
|
|
256
|
+
queryVector: createVector(0, 1.0),
|
|
257
|
+
topK: 2,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(results).toHaveLength(2);
|
|
261
|
+
expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
describe('Index Operations', () => {
|
|
266
|
+
const createVector = (primaryDimension: number, value: number = 1.0): number[] => {
|
|
267
|
+
const vector = new Array(VECTOR_DIMENSION).fill(0);
|
|
268
|
+
vector[primaryDimension] = value;
|
|
269
|
+
// Normalize the vector for cosine similarity
|
|
270
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
271
|
+
return vector.map(val => val / magnitude);
|
|
272
|
+
};
|
|
273
|
+
it('should create and list an index', async () => {
|
|
274
|
+
// since, we do not have to create index explictly in case of upstash. Upserts are enough
|
|
275
|
+
// for testing the listIndexes() function
|
|
276
|
+
// await vectorStore.createIndex({ indexName: testIndexName, dimension: 3, metric: 'cosine' });
|
|
277
|
+
const ids = await vectorStore.upsert({
|
|
278
|
+
indexName: testIndexName,
|
|
279
|
+
vectors: [createVector(0, 1.0)],
|
|
280
|
+
sparseVectors: [_createSparseVector()],
|
|
281
|
+
});
|
|
282
|
+
expect(ids).toHaveLength(1);
|
|
283
|
+
const indexes = await vectorStore.listIndexes();
|
|
284
|
+
expect(indexes).toEqual([testIndexName]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should describe an index correctly', async () => {
|
|
288
|
+
const stats = await vectorStore.describeIndex({ indexName: 'mastra_default' });
|
|
289
|
+
expect(stats).toEqual({
|
|
290
|
+
dimension: 1024,
|
|
291
|
+
metric: 'cosine',
|
|
292
|
+
count: 0,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('Error Handling', () => {
|
|
298
|
+
const testIndexName = 'test_index_error';
|
|
299
|
+
beforeAll(async () => {
|
|
300
|
+
await vectorStore.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
afterAll(async () => {
|
|
304
|
+
await vectorStore.deleteIndex({ indexName: testIndexName });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should handle invalid dimension vectors', async () => {
|
|
308
|
+
await expect(
|
|
309
|
+
vectorStore.upsert({ indexName: testIndexName, vectors: [[1.0, 0.0]] }), // Wrong dimensions
|
|
310
|
+
).rejects.toThrow();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should handle querying with wrong dimensions', async () => {
|
|
314
|
+
await expect(
|
|
315
|
+
vectorStore.query({ indexName: testIndexName, queryVector: [1.0, 0.0] }), // Wrong dimensions
|
|
316
|
+
).rejects.toThrow();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('Filter Tests', () => {
|
|
321
|
+
const createVector = (dim: number) => new Array(VECTOR_DIMENSION).fill(0).map((_, i) => (i === dim ? 1 : 0));
|
|
322
|
+
|
|
323
|
+
const testData = [
|
|
324
|
+
{
|
|
325
|
+
id: '1',
|
|
326
|
+
vector: createVector(0),
|
|
327
|
+
metadata: {
|
|
328
|
+
name: 'Istanbul',
|
|
329
|
+
population: 15460000,
|
|
330
|
+
location: {
|
|
331
|
+
continent: 'Asia',
|
|
332
|
+
coordinates: {
|
|
333
|
+
latitude: 41.0082,
|
|
334
|
+
longitude: 28.9784,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
tags: ['historic', 'coastal', 'metropolitan'],
|
|
338
|
+
industries: ['Tourism', 'Finance', 'Technology'],
|
|
339
|
+
founded: 330,
|
|
340
|
+
isCapital: false,
|
|
341
|
+
lastCensus: null,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: '2',
|
|
346
|
+
vector: createVector(1),
|
|
347
|
+
metadata: {
|
|
348
|
+
name: 'Berlin',
|
|
349
|
+
population: 3669495,
|
|
350
|
+
location: {
|
|
351
|
+
continent: 'Europe',
|
|
352
|
+
coordinates: {
|
|
353
|
+
latitude: 52.52,
|
|
354
|
+
longitude: 13.405,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
tags: ['historic', 'cultural', 'metropolitan'],
|
|
358
|
+
industries: ['Technology', 'Arts', 'Tourism'],
|
|
359
|
+
founded: 1237,
|
|
360
|
+
isCapital: true,
|
|
361
|
+
lastCensus: 2021,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: '3',
|
|
366
|
+
vector: createVector(2),
|
|
367
|
+
metadata: {
|
|
368
|
+
name: 'San Francisco',
|
|
369
|
+
population: 873965,
|
|
370
|
+
location: {
|
|
371
|
+
continent: 'North America',
|
|
372
|
+
coordinates: {
|
|
373
|
+
latitude: 37.7749,
|
|
374
|
+
longitude: -122.4194,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
tags: ['coastal', 'tech', 'metropolitan'],
|
|
378
|
+
industries: ['Technology', 'Finance', 'Tourism'],
|
|
379
|
+
founded: 1776,
|
|
380
|
+
isCapital: false,
|
|
381
|
+
lastCensus: 2020,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: '4',
|
|
386
|
+
vector: createVector(3),
|
|
387
|
+
metadata: {
|
|
388
|
+
name: "City's Name",
|
|
389
|
+
description: 'Contains "quotes"',
|
|
390
|
+
population: 0,
|
|
391
|
+
temperature: -10,
|
|
392
|
+
microscopicDetail: 1e-10,
|
|
393
|
+
isCapital: false,
|
|
394
|
+
tags: ['nothing'],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
beforeAll(async () => {
|
|
400
|
+
await vectorStore.createIndex({ indexName: filterIndexName, dimension: VECTOR_DIMENSION });
|
|
401
|
+
await vectorStore.upsert({
|
|
402
|
+
indexName: filterIndexName,
|
|
403
|
+
vectors: testData.map(d => d.vector),
|
|
404
|
+
metadata: testData.map(d => d.metadata),
|
|
405
|
+
ids: testData.map(d => d.id),
|
|
406
|
+
sparseVectors: testData.map(() => _createSparseVector()),
|
|
407
|
+
});
|
|
408
|
+
// Wait for indexing
|
|
409
|
+
await waitUntilVectorsIndexed(vectorStore, filterIndexName, testData.length);
|
|
410
|
+
}, 50000);
|
|
411
|
+
|
|
412
|
+
describe('Basic Operators', () => {
|
|
413
|
+
it('should filter by exact match', async () => {
|
|
414
|
+
const results = await vectorStore.query({
|
|
415
|
+
indexName: filterIndexName,
|
|
416
|
+
queryVector: createVector(0),
|
|
417
|
+
filter: { name: 'Istanbul' },
|
|
418
|
+
});
|
|
419
|
+
expect(results).toHaveLength(1);
|
|
420
|
+
expect(results[0]?.metadata?.name).toBe('Istanbul');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should filter by not equal', async () => {
|
|
424
|
+
const results = await vectorStore.query({
|
|
425
|
+
indexName: filterIndexName,
|
|
426
|
+
queryVector: createVector(0),
|
|
427
|
+
filter: { name: { $ne: 'Berlin' } },
|
|
428
|
+
});
|
|
429
|
+
expect(results).toHaveLength(3);
|
|
430
|
+
results.forEach(result => {
|
|
431
|
+
expect(result.metadata?.name).not.toBe('Berlin');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should filter by greater than', async () => {
|
|
436
|
+
const results = await vectorStore.query({
|
|
437
|
+
indexName: filterIndexName,
|
|
438
|
+
queryVector: createVector(0),
|
|
439
|
+
filter: { population: { $gt: 1000000 } },
|
|
440
|
+
});
|
|
441
|
+
expect(results).toHaveLength(2);
|
|
442
|
+
results.forEach(result => {
|
|
443
|
+
expect(result.metadata?.population).toBeGreaterThan(1000000);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should filter by less than or equal', async () => {
|
|
448
|
+
const results = await vectorStore.query({
|
|
449
|
+
indexName: filterIndexName,
|
|
450
|
+
queryVector: createVector(0),
|
|
451
|
+
filter: { founded: { $lte: 1500 } },
|
|
452
|
+
});
|
|
453
|
+
expect(results).toHaveLength(2);
|
|
454
|
+
results.forEach(result => {
|
|
455
|
+
expect(result.metadata?.founded).toBeLessThanOrEqual(1500);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe('Array Operations', () => {
|
|
461
|
+
it('should filter by array contains', async () => {
|
|
462
|
+
const results = await vectorStore.query({
|
|
463
|
+
indexName: filterIndexName,
|
|
464
|
+
queryVector: createVector(0),
|
|
465
|
+
topK: 10,
|
|
466
|
+
filter: { tags: { $contains: 'historic' } },
|
|
467
|
+
});
|
|
468
|
+
expect(results).toHaveLength(2);
|
|
469
|
+
results.forEach(result => {
|
|
470
|
+
expect(result.metadata?.tags).toContain('historic');
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should filter by array not contains', async () => {
|
|
475
|
+
const results = await vectorStore.query({
|
|
476
|
+
indexName: filterIndexName,
|
|
477
|
+
queryVector: createVector(0),
|
|
478
|
+
filter: { tags: { $not: { $contains: 'tech' } } },
|
|
479
|
+
});
|
|
480
|
+
expect(results).toHaveLength(3);
|
|
481
|
+
results.forEach(result => {
|
|
482
|
+
expect(result.metadata?.tags?.find(tag => tag === 'tech')).toBeUndefined();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should filter by in array', async () => {
|
|
487
|
+
const results = await vectorStore.query({
|
|
488
|
+
indexName: filterIndexName,
|
|
489
|
+
queryVector: createVector(0),
|
|
490
|
+
filter: { 'location.continent': { $in: ['Asia', 'Europe'] } },
|
|
491
|
+
});
|
|
492
|
+
expect(results).toHaveLength(2);
|
|
493
|
+
results.forEach(result => {
|
|
494
|
+
expect(['Asia', 'Europe']).toContain(result.metadata?.location?.continent);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should filter by not in array', async () => {
|
|
499
|
+
const results = await vectorStore.query({
|
|
500
|
+
indexName: filterIndexName,
|
|
501
|
+
queryVector: createVector(0),
|
|
502
|
+
filter: { name: { $nin: ['Berlin', 'Istanbul'] } },
|
|
503
|
+
});
|
|
504
|
+
expect(results).toHaveLength(2);
|
|
505
|
+
results.forEach(result => {
|
|
506
|
+
expect(['Berlin', 'Istanbul']).not.toContain(result.metadata?.name);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('Array Indexing', () => {
|
|
512
|
+
it('should filter by first array element', async () => {
|
|
513
|
+
const results = await vectorStore.query({
|
|
514
|
+
indexName: filterIndexName,
|
|
515
|
+
queryVector: createVector(0),
|
|
516
|
+
filter: { 'industries[0]': 'Tourism' },
|
|
517
|
+
});
|
|
518
|
+
expect(results).toHaveLength(1);
|
|
519
|
+
expect(results[0]?.metadata?.industries?.[0]).toBe('Tourism');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should filter by last array element', async () => {
|
|
523
|
+
const results = await vectorStore.query({
|
|
524
|
+
indexName: filterIndexName,
|
|
525
|
+
queryVector: createVector(0),
|
|
526
|
+
filter: { 'industries[#-1]': 'Technology' },
|
|
527
|
+
});
|
|
528
|
+
expect(results).toHaveLength(1);
|
|
529
|
+
expect(results[0]?.metadata?.industries?.slice(-1)[0]).toBe('Technology');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should combine first and last element filters', async () => {
|
|
533
|
+
const results = await vectorStore.query({
|
|
534
|
+
indexName: filterIndexName,
|
|
535
|
+
queryVector: createVector(0),
|
|
536
|
+
filter: { 'industries[0]': 'Tourism', 'tags[#-1]': 'metropolitan' },
|
|
537
|
+
});
|
|
538
|
+
expect(results).toHaveLength(1);
|
|
539
|
+
const result = results[0]?.metadata;
|
|
540
|
+
expect(result?.industries?.[0]).toBe('Tourism');
|
|
541
|
+
expect(result?.tags?.slice(-1)[0]).toBe('metropolitan');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
describe('Nested Fields', () => {
|
|
546
|
+
it('should filter by nested field', async () => {
|
|
547
|
+
const results = await vectorStore.query({
|
|
548
|
+
indexName: filterIndexName,
|
|
549
|
+
queryVector: createVector(0),
|
|
550
|
+
filter: { 'location.continent': 'Asia' },
|
|
551
|
+
});
|
|
552
|
+
expect(results).toHaveLength(1);
|
|
553
|
+
expect(results[0]?.metadata?.location?.continent).toBe('Asia');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should filter by deeply nested field with comparison', async () => {
|
|
557
|
+
const results = await vectorStore.query({
|
|
558
|
+
indexName: filterIndexName,
|
|
559
|
+
queryVector: createVector(0),
|
|
560
|
+
filter: { 'location.coordinates.latitude': { $gt: 40 } },
|
|
561
|
+
});
|
|
562
|
+
expect(results).toHaveLength(2);
|
|
563
|
+
results.forEach(result => {
|
|
564
|
+
expect(result.metadata?.location?.coordinates?.latitude).toBeGreaterThan(40);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should combine nested and array filters', async () => {
|
|
569
|
+
const results = await vectorStore.query({
|
|
570
|
+
indexName: filterIndexName,
|
|
571
|
+
queryVector: createVector(0),
|
|
572
|
+
filter: { 'location.coordinates.latitude': { $gt: 40 }, 'industries[0]': 'Tourism' },
|
|
573
|
+
});
|
|
574
|
+
expect(results).toHaveLength(1);
|
|
575
|
+
const result = results[0]?.metadata;
|
|
576
|
+
expect(result?.location?.coordinates?.latitude).toBeGreaterThan(40);
|
|
577
|
+
expect(result?.industries?.[0]).toBe('Tourism');
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe('Logical Operators', () => {
|
|
582
|
+
it('should combine conditions with AND', async () => {
|
|
583
|
+
const results = await vectorStore.query({
|
|
584
|
+
indexName: filterIndexName,
|
|
585
|
+
queryVector: createVector(0),
|
|
586
|
+
filter: { $and: [{ population: { $gt: 1000000 } }, { isCapital: true }] },
|
|
587
|
+
});
|
|
588
|
+
expect(results).toHaveLength(1);
|
|
589
|
+
const result = results[0]?.metadata;
|
|
590
|
+
expect(result?.population).toBeGreaterThan(1000000);
|
|
591
|
+
expect(result?.isCapital).toBe(true);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should combine conditions with OR', async () => {
|
|
595
|
+
const results = await vectorStore.query({
|
|
596
|
+
indexName: filterIndexName,
|
|
597
|
+
queryVector: createVector(0),
|
|
598
|
+
filter: { $or: [{ 'location.continent': 'Asia' }, { 'location.continent': 'Europe' }] },
|
|
599
|
+
});
|
|
600
|
+
expect(results).toHaveLength(2);
|
|
601
|
+
results.forEach(result => {
|
|
602
|
+
expect(['Asia', 'Europe']).toContain(result.metadata?.location?.continent);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('should handle NOT operator', async () => {
|
|
607
|
+
const results = await vectorStore.query({
|
|
608
|
+
indexName: filterIndexName,
|
|
609
|
+
queryVector: createVector(0),
|
|
610
|
+
filter: { $not: { isCapital: true } },
|
|
611
|
+
});
|
|
612
|
+
expect(results).toHaveLength(3);
|
|
613
|
+
results.forEach(result => {
|
|
614
|
+
expect(result.metadata?.isCapital).not.toBe(true);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should handle NOT with comparison operators', async () => {
|
|
619
|
+
const results = await vectorStore.query({
|
|
620
|
+
indexName: filterIndexName,
|
|
621
|
+
queryVector: createVector(0),
|
|
622
|
+
filter: { population: { $not: { $lt: 1000000 } } },
|
|
623
|
+
});
|
|
624
|
+
expect(results).toHaveLength(2);
|
|
625
|
+
results.forEach(result => {
|
|
626
|
+
expect(result.metadata?.population).toBeGreaterThanOrEqual(1000000);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('should handle NOT with contains operator', async () => {
|
|
631
|
+
const results = await vectorStore.query({
|
|
632
|
+
indexName: filterIndexName,
|
|
633
|
+
queryVector: createVector(0),
|
|
634
|
+
filter: { tags: { $not: { $contains: 'tech' } } },
|
|
635
|
+
});
|
|
636
|
+
expect(results).toHaveLength(3);
|
|
637
|
+
results.forEach(result => {
|
|
638
|
+
expect(result.metadata?.tags?.find(tag => tag === 'tech')).toBeUndefined();
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('should handle NOT with regex operator', async () => {
|
|
643
|
+
const results = await vectorStore.query({
|
|
644
|
+
indexName: filterIndexName,
|
|
645
|
+
queryVector: createVector(0),
|
|
646
|
+
filter: { name: { $not: { $regex: '*bul' } } },
|
|
647
|
+
});
|
|
648
|
+
expect(results).toHaveLength(3);
|
|
649
|
+
results.forEach(result => {
|
|
650
|
+
expect(result.metadata?.name).not.toMatch(/bul$/);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should handle NOR operator', async () => {
|
|
655
|
+
const results = await vectorStore.query({
|
|
656
|
+
indexName: filterIndexName,
|
|
657
|
+
queryVector: createVector(0),
|
|
658
|
+
filter: { $nor: [{ 'location.continent': 'Asia' }, { 'location.continent': 'Europe' }] },
|
|
659
|
+
});
|
|
660
|
+
expect(results).toHaveLength(1);
|
|
661
|
+
results.forEach(result => {
|
|
662
|
+
expect(['Asia', 'Europe']).not.toContain(result.metadata?.location?.continent);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should handle NOR with multiple conditions', async () => {
|
|
667
|
+
const results = await vectorStore.query({
|
|
668
|
+
indexName: filterIndexName,
|
|
669
|
+
queryVector: createVector(0),
|
|
670
|
+
filter: { $nor: [{ population: { $gt: 10000000 } }, { isCapital: true }, { tags: { $contains: 'tech' } }] },
|
|
671
|
+
});
|
|
672
|
+
expect(results).toHaveLength(1);
|
|
673
|
+
const result = results[0]?.metadata;
|
|
674
|
+
expect(result?.population).toBeLessThanOrEqual(10000000);
|
|
675
|
+
expect(result?.isCapital).not.toBe(true);
|
|
676
|
+
expect(result?.tags).not.toContain('tech');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('should handle ALL operator with simple values', async () => {
|
|
680
|
+
const results = await vectorStore.query({
|
|
681
|
+
indexName: filterIndexName,
|
|
682
|
+
queryVector: createVector(0),
|
|
683
|
+
filter: { industries: { $all: ['Tourism', 'Finance'] } },
|
|
684
|
+
});
|
|
685
|
+
expect(results).toHaveLength(2);
|
|
686
|
+
results.forEach(result => {
|
|
687
|
+
expect(result.metadata?.industries).toContain('Tourism');
|
|
688
|
+
expect(result.metadata?.industries).toContain('Finance');
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('should handle ALL operator with empty array', async () => {
|
|
693
|
+
const results = await vectorStore.query({
|
|
694
|
+
indexName: filterIndexName,
|
|
695
|
+
queryVector: createVector(0),
|
|
696
|
+
filter: { tags: { $all: [] } },
|
|
697
|
+
});
|
|
698
|
+
expect(results.length).toBeGreaterThan(0);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('should handle NOT with nested logical operators', async () => {
|
|
702
|
+
const results = await vectorStore.query({
|
|
703
|
+
indexName: filterIndexName,
|
|
704
|
+
queryVector: createVector(0),
|
|
705
|
+
filter: { $not: { $and: [{ population: { $lt: 1000000 } }, { isCapital: true }] } },
|
|
706
|
+
});
|
|
707
|
+
expect(results).toHaveLength(4);
|
|
708
|
+
results.forEach(result => {
|
|
709
|
+
const metadata = result.metadata;
|
|
710
|
+
expect(metadata?.population >= 1000000 || metadata?.isCapital !== true).toBe(true);
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should handle NOR with nested path conditions', async () => {
|
|
715
|
+
const results = await vectorStore.query({
|
|
716
|
+
indexName: filterIndexName,
|
|
717
|
+
queryVector: createVector(0),
|
|
718
|
+
filter: {
|
|
719
|
+
$nor: [
|
|
720
|
+
{ 'location.coordinates.latitude': { $lt: 40 } },
|
|
721
|
+
{ 'location.coordinates.longitude': { $gt: 100 } },
|
|
722
|
+
],
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
expect(results).toHaveLength(2);
|
|
726
|
+
results.forEach(result => {
|
|
727
|
+
const coords = result.metadata?.location?.coordinates;
|
|
728
|
+
expect(coords?.latitude >= 40 || coords?.longitude <= 100).toBe(true);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should handle exists with nested paths', async () => {
|
|
733
|
+
const results = await vectorStore.query({
|
|
734
|
+
indexName: filterIndexName,
|
|
735
|
+
queryVector: createVector(0),
|
|
736
|
+
filter: {
|
|
737
|
+
$and: [
|
|
738
|
+
{ 'location.coordinates.latitude': { $exists: true } },
|
|
739
|
+
{ 'location.coordinates.longitude': { $exists: true } },
|
|
740
|
+
],
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
expect(results).toHaveLength(3);
|
|
744
|
+
results.forEach(result => {
|
|
745
|
+
expect(result.metadata?.location?.coordinates?.latitude).toBeDefined();
|
|
746
|
+
expect(result.metadata?.location?.coordinates?.longitude).toBeDefined();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should handle complex NOT combinations', async () => {
|
|
751
|
+
const results = await vectorStore.query({
|
|
752
|
+
indexName: filterIndexName,
|
|
753
|
+
queryVector: createVector(0),
|
|
754
|
+
filter: {
|
|
755
|
+
$not: {
|
|
756
|
+
$or: [
|
|
757
|
+
{ 'location.continent': 'Asia' },
|
|
758
|
+
{ population: { $lt: 1000000 } },
|
|
759
|
+
{ tags: { $contains: 'tech' } },
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
expect(results).toHaveLength(1);
|
|
765
|
+
const result = results[0]?.metadata;
|
|
766
|
+
expect(result?.location?.continent).not.toBe('Asia');
|
|
767
|
+
expect(result?.population).toBeGreaterThanOrEqual(1000000);
|
|
768
|
+
expect(result?.tags).not.toContain('tech');
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('should handle NOR with regex patterns', async () => {
|
|
772
|
+
const results = await vectorStore.query({
|
|
773
|
+
indexName: filterIndexName,
|
|
774
|
+
queryVector: createVector(0),
|
|
775
|
+
filter: {
|
|
776
|
+
$nor: [{ name: { $regex: '*bul' } }, { name: { $regex: '*lin' } }, { name: { $regex: '*cisco' } }],
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
expect(results).toHaveLength(1);
|
|
780
|
+
expect(results[0]?.metadata?.name).toBe("City's Name");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('should handle NOR with mixed operator types', async () => {
|
|
784
|
+
const results = await vectorStore.query({
|
|
785
|
+
indexName: filterIndexName,
|
|
786
|
+
queryVector: createVector(0),
|
|
787
|
+
filter: {
|
|
788
|
+
$nor: [
|
|
789
|
+
{ population: { $gt: 5000000 } },
|
|
790
|
+
{ tags: { $contains: 'tech' } },
|
|
791
|
+
{ 'location.coordinates.latitude': { $lt: 38 } },
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
expect(results).toHaveLength(1);
|
|
796
|
+
const result = results[0]?.metadata;
|
|
797
|
+
expect(result?.population).toBeLessThanOrEqual(5000000);
|
|
798
|
+
expect(result?.tags).not.toContain('tech');
|
|
799
|
+
expect(result?.location?.coordinates?.latitude).toBeGreaterThanOrEqual(38);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('should handle NOR with exists operator', async () => {
|
|
803
|
+
const results = await vectorStore.query({
|
|
804
|
+
indexName: filterIndexName,
|
|
805
|
+
queryVector: createVector(0),
|
|
806
|
+
filter: { $nor: [{ lastCensus: { $exists: true } }, { population: { $exists: false } }] },
|
|
807
|
+
});
|
|
808
|
+
expect(results).toHaveLength(1);
|
|
809
|
+
const result = results[0]?.metadata;
|
|
810
|
+
expect(result?.lastCensus).toBeUndefined();
|
|
811
|
+
expect(result?.population).toBeDefined();
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('should handle ALL with mixed value types', async () => {
|
|
815
|
+
const results = await vectorStore.query({
|
|
816
|
+
indexName: filterIndexName,
|
|
817
|
+
queryVector: createVector(0),
|
|
818
|
+
filter: { $and: [{ tags: { $contains: 'coastal' } }, { tags: { $contains: 'metropolitan' } }] },
|
|
819
|
+
});
|
|
820
|
+
expect(results).toHaveLength(2);
|
|
821
|
+
results.forEach(result => {
|
|
822
|
+
const tags = result.metadata?.tags || [];
|
|
823
|
+
expect(tags).toContain('coastal');
|
|
824
|
+
expect(tags).toContain('metropolitan');
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should handle ALL with nested array conditions', async () => {
|
|
829
|
+
const results = await vectorStore.query({
|
|
830
|
+
indexName: filterIndexName,
|
|
831
|
+
queryVector: createVector(0),
|
|
832
|
+
filter: { $and: [{ industries: { $all: ['Tourism', 'Finance'] } }, { tags: { $all: ['metropolitan'] } }] },
|
|
833
|
+
});
|
|
834
|
+
expect(results).toHaveLength(2);
|
|
835
|
+
results.forEach(result => {
|
|
836
|
+
expect(result.metadata?.industries).toContain('Tourism');
|
|
837
|
+
expect(result.metadata?.industries).toContain('Finance');
|
|
838
|
+
expect(result.metadata?.tags).toContain('metropolitan');
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should handle ALL with complex conditions', async () => {
|
|
843
|
+
const results = await vectorStore.query({
|
|
844
|
+
indexName: filterIndexName,
|
|
845
|
+
queryVector: createVector(0),
|
|
846
|
+
filter: {
|
|
847
|
+
$or: [{ industries: { $all: ['Tourism', 'Finance'] } }, { tags: { $all: ['tech', 'metropolitan'] } }],
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
expect(results).toHaveLength(2);
|
|
851
|
+
results.forEach(result => {
|
|
852
|
+
const hasAllIndustries =
|
|
853
|
+
result.metadata?.industries?.includes('Tourism') && result.metadata?.industries?.includes('Finance');
|
|
854
|
+
const hasAllTags = result.metadata?.tags?.includes('tech') && result.metadata?.tags?.includes('metropolitan');
|
|
855
|
+
expect(hasAllIndustries || hasAllTags).toBe(true);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('should handle ALL with single item array', async () => {
|
|
860
|
+
const results = await vectorStore.query({
|
|
861
|
+
indexName: filterIndexName,
|
|
862
|
+
queryVector: createVector(0),
|
|
863
|
+
filter: { industries: { $all: ['Technology'] } },
|
|
864
|
+
});
|
|
865
|
+
expect(results).toHaveLength(3);
|
|
866
|
+
results.forEach(result => {
|
|
867
|
+
expect(result.metadata?.industries).toContain('Technology');
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('should handle complex nested conditions', async () => {
|
|
872
|
+
const results = await vectorStore.query({
|
|
873
|
+
indexName: filterIndexName,
|
|
874
|
+
queryVector: createVector(0),
|
|
875
|
+
filter: {
|
|
876
|
+
$and: [
|
|
877
|
+
{ population: { $gt: 1000000 } },
|
|
878
|
+
{
|
|
879
|
+
$or: [{ 'location.continent': 'Asia' }, { industries: { $contains: 'Technology' } }],
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
expect(results).toHaveLength(2);
|
|
885
|
+
results.forEach(result => {
|
|
886
|
+
const metadata = result.metadata;
|
|
887
|
+
expect(metadata?.population).toBeGreaterThan(1000000);
|
|
888
|
+
expect(metadata?.location?.continent === 'Asia' || metadata?.industries?.includes('Technology')).toBe(true);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
describe('Edge Cases', () => {
|
|
894
|
+
describe('Empty Conditions', () => {
|
|
895
|
+
it('should handle empty AND array', async () => {
|
|
896
|
+
const results = await vectorStore.query({
|
|
897
|
+
indexName: filterIndexName,
|
|
898
|
+
queryVector: createVector(0),
|
|
899
|
+
filter: { $and: [] },
|
|
900
|
+
});
|
|
901
|
+
expect(results.length).toBeGreaterThan(0);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('should handle empty OR array', async () => {
|
|
905
|
+
const results = await vectorStore.query({
|
|
906
|
+
indexName: filterIndexName,
|
|
907
|
+
queryVector: createVector(0),
|
|
908
|
+
filter: { $or: [] },
|
|
909
|
+
});
|
|
910
|
+
expect(results.length).toBe(0);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('should handle empty IN array', async () => {
|
|
914
|
+
const results = await vectorStore.query({
|
|
915
|
+
indexName: filterIndexName,
|
|
916
|
+
queryVector: createVector(0),
|
|
917
|
+
filter: { tags: { $in: [] } },
|
|
918
|
+
});
|
|
919
|
+
expect(results.length).toBe(0);
|
|
920
|
+
});
|
|
921
|
+
it('should handle empty IN array', async () => {
|
|
922
|
+
const results = await vectorStore.query({
|
|
923
|
+
indexName: filterIndexName,
|
|
924
|
+
queryVector: createVector(0),
|
|
925
|
+
filter: { tags: [] },
|
|
926
|
+
});
|
|
927
|
+
expect(results.length).toBe(0);
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
describe('Null/Undefined Values', () => {
|
|
932
|
+
it('should handle null values', async () => {
|
|
933
|
+
await expect(
|
|
934
|
+
vectorStore.query({
|
|
935
|
+
indexName: filterIndexName,
|
|
936
|
+
queryVector: createVector(0),
|
|
937
|
+
filter: { lastCensus: null },
|
|
938
|
+
}),
|
|
939
|
+
).rejects.toThrow();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('should handle null in arrays', async () => {
|
|
943
|
+
await expect(
|
|
944
|
+
vectorStore.query({
|
|
945
|
+
indexName: filterIndexName,
|
|
946
|
+
queryVector: createVector(0),
|
|
947
|
+
filter: { tags: { $in: [null, 'historic'] } },
|
|
948
|
+
}),
|
|
949
|
+
).rejects.toThrow();
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
describe('Special Characters', () => {
|
|
954
|
+
it('should handle strings with quotes', async () => {
|
|
955
|
+
const results = await vectorStore.query({
|
|
956
|
+
indexName: filterIndexName,
|
|
957
|
+
queryVector: createVector(0),
|
|
958
|
+
filter: { name: "City's Name" },
|
|
959
|
+
});
|
|
960
|
+
expect(results).toHaveLength(1);
|
|
961
|
+
expect(results[0]?.metadata?.name).toBe("City's Name");
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('should handle strings with double quotes', async () => {
|
|
965
|
+
const results = await vectorStore.query({
|
|
966
|
+
indexName: filterIndexName,
|
|
967
|
+
queryVector: createVector(0),
|
|
968
|
+
filter: { description: 'Contains "quotes"' },
|
|
969
|
+
});
|
|
970
|
+
expect(results).toHaveLength(1);
|
|
971
|
+
expect(results[0]?.metadata?.description).toBe('Contains "quotes"');
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
describe('Number Formats', () => {
|
|
976
|
+
it('should handle zero', async () => {
|
|
977
|
+
const results = await vectorStore.query({
|
|
978
|
+
indexName: filterIndexName,
|
|
979
|
+
queryVector: createVector(0),
|
|
980
|
+
filter: { population: 0 },
|
|
981
|
+
});
|
|
982
|
+
expect(results).toHaveLength(1);
|
|
983
|
+
expect(results[0]?.metadata?.population).toBe(0);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it('should handle negative numbers', async () => {
|
|
987
|
+
const results = await vectorStore.query({
|
|
988
|
+
indexName: filterIndexName,
|
|
989
|
+
queryVector: createVector(0),
|
|
990
|
+
filter: { temperature: -10 },
|
|
991
|
+
});
|
|
992
|
+
expect(results).toHaveLength(1);
|
|
993
|
+
expect(results[0]?.metadata?.temperature).toBe(-10);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('should handle decimal numbers', async () => {
|
|
997
|
+
const results = await vectorStore.query({
|
|
998
|
+
indexName: filterIndexName,
|
|
999
|
+
queryVector: createVector(0),
|
|
1000
|
+
filter: { 'location.coordinates.latitude': 41.0082 },
|
|
1001
|
+
});
|
|
1002
|
+
expect(results).toHaveLength(1);
|
|
1003
|
+
expect(results[0]?.metadata?.location?.coordinates?.latitude).toBe(41.0082);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it('should handle scientific notation', async () => {
|
|
1007
|
+
const results = await vectorStore.query({
|
|
1008
|
+
indexName: filterIndexName,
|
|
1009
|
+
queryVector: createVector(0),
|
|
1010
|
+
filter: { microscopicDetail: 1e-10 },
|
|
1011
|
+
});
|
|
1012
|
+
expect(results).toHaveLength(1);
|
|
1013
|
+
expect(results[0]?.metadata?.microscopicDetail).toBe(1e-10);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('should handle escaped quotes in strings', async () => {
|
|
1017
|
+
const results = await vectorStore.query({
|
|
1018
|
+
indexName: filterIndexName,
|
|
1019
|
+
queryVector: createVector(0),
|
|
1020
|
+
filter: { description: { $regex: '*"quotes"*' } },
|
|
1021
|
+
});
|
|
1022
|
+
expect(results).toHaveLength(1);
|
|
1023
|
+
expect(results[0]?.metadata?.description).toBe('Contains "quotes"');
|
|
1024
|
+
});
|
|
1025
|
+
it('should handle undefined filter', async () => {
|
|
1026
|
+
const results1 = await vectorStore.query({
|
|
1027
|
+
indexName: filterIndexName,
|
|
1028
|
+
queryVector: createVector(0),
|
|
1029
|
+
filter: undefined,
|
|
1030
|
+
});
|
|
1031
|
+
const results2 = await vectorStore.query({
|
|
1032
|
+
indexName: filterIndexName,
|
|
1033
|
+
queryVector: createVector(0),
|
|
1034
|
+
});
|
|
1035
|
+
expect(results1).toEqual(results2);
|
|
1036
|
+
expect(results1.length).toBeGreaterThan(0);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it('should handle empty object filter', async () => {
|
|
1040
|
+
const results = await vectorStore.query({
|
|
1041
|
+
indexName: filterIndexName,
|
|
1042
|
+
queryVector: createVector(0),
|
|
1043
|
+
filter: {},
|
|
1044
|
+
});
|
|
1045
|
+
const results2 = await vectorStore.query({
|
|
1046
|
+
indexName: filterIndexName,
|
|
1047
|
+
queryVector: createVector(0),
|
|
1048
|
+
});
|
|
1049
|
+
expect(results).toEqual(results2);
|
|
1050
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('should handle null filter', async () => {
|
|
1054
|
+
const results = await vectorStore.query({
|
|
1055
|
+
indexName: filterIndexName,
|
|
1056
|
+
queryVector: createVector(0),
|
|
1057
|
+
filter: null,
|
|
1058
|
+
});
|
|
1059
|
+
const results2 = await vectorStore.query({
|
|
1060
|
+
indexName: filterIndexName,
|
|
1061
|
+
queryVector: createVector(0),
|
|
1062
|
+
});
|
|
1063
|
+
expect(results).toEqual(results2);
|
|
1064
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
describe('Pattern Matching', () => {
|
|
1070
|
+
it('should match start of string', async () => {
|
|
1071
|
+
const results = await vectorStore.query({
|
|
1072
|
+
indexName: filterIndexName,
|
|
1073
|
+
queryVector: createVector(0),
|
|
1074
|
+
filter: { name: { $regex: 'San*' } },
|
|
1075
|
+
});
|
|
1076
|
+
expect(results).toHaveLength(1);
|
|
1077
|
+
expect(results[0]?.metadata?.name).toBe('San Francisco');
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('should match end of string', async () => {
|
|
1081
|
+
const results = await vectorStore.query({
|
|
1082
|
+
indexName: filterIndexName,
|
|
1083
|
+
queryVector: createVector(0),
|
|
1084
|
+
filter: { name: { $regex: '*in' } },
|
|
1085
|
+
});
|
|
1086
|
+
expect(results).toHaveLength(1);
|
|
1087
|
+
expect(results[0]?.metadata?.name).toBe('Berlin');
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('should handle negated pattern', async () => {
|
|
1091
|
+
const results = await vectorStore.query({
|
|
1092
|
+
indexName: filterIndexName,
|
|
1093
|
+
queryVector: createVector(0),
|
|
1094
|
+
filter: { name: { $not: { $regex: 'A*' } } },
|
|
1095
|
+
});
|
|
1096
|
+
expect(results).toHaveLength(4);
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
describe('Field Existence', () => {
|
|
1101
|
+
it('should check field exists', async () => {
|
|
1102
|
+
const results = await vectorStore.query({
|
|
1103
|
+
indexName: filterIndexName,
|
|
1104
|
+
queryVector: createVector(0),
|
|
1105
|
+
filter: { 'location.coordinates': { $exists: true } },
|
|
1106
|
+
});
|
|
1107
|
+
expect(results).toHaveLength(3);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('should check field does not exist', async () => {
|
|
1111
|
+
const results = await vectorStore.query({
|
|
1112
|
+
indexName: filterIndexName,
|
|
1113
|
+
queryVector: createVector(0),
|
|
1114
|
+
filter: { unknownField: { $exists: false } },
|
|
1115
|
+
});
|
|
1116
|
+
expect(results).toHaveLength(4);
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
describe('Performance Tests', () => {
|
|
1121
|
+
it('should reject large arrays', async () => {
|
|
1122
|
+
const largeArray = Array.from({ length: 1000 }, (_, i) => `value${i}`);
|
|
1123
|
+
await expect(
|
|
1124
|
+
vectorStore.query({
|
|
1125
|
+
indexName: filterIndexName,
|
|
1126
|
+
queryVector: createVector(0),
|
|
1127
|
+
filter: { tags: { $in: largeArray } },
|
|
1128
|
+
}),
|
|
1129
|
+
).rejects.toThrow();
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('should handle deep nesting', async () => {
|
|
1133
|
+
const deepFilter = {
|
|
1134
|
+
$and: [
|
|
1135
|
+
{ 'a.b.c.d.e': 1 },
|
|
1136
|
+
{
|
|
1137
|
+
$or: [
|
|
1138
|
+
{ 'f.g.h.i.j': 2 },
|
|
1139
|
+
{
|
|
1140
|
+
$and: [{ 'k.l.m.n.o': 3 }, { 'p.q.r.s.t': 4 }],
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
};
|
|
1146
|
+
const start = Date.now();
|
|
1147
|
+
const results = await vectorStore.query({
|
|
1148
|
+
indexName: filterIndexName,
|
|
1149
|
+
queryVector: createVector(0),
|
|
1150
|
+
filter: deepFilter,
|
|
1151
|
+
});
|
|
1152
|
+
const duration = Date.now() - start;
|
|
1153
|
+
expect(duration).toBeLessThan(1000);
|
|
1154
|
+
expect(Array.isArray(results)).toBe(true);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it('should handle complex combinations', async () => {
|
|
1158
|
+
const complexFilter = {
|
|
1159
|
+
$and: Array(10)
|
|
1160
|
+
.fill(null)
|
|
1161
|
+
.map((_, i) => ({
|
|
1162
|
+
$or: [
|
|
1163
|
+
{ [`field${i}`]: { $gt: i } },
|
|
1164
|
+
{ [`array${i}`]: { $contains: `value${i}` } },
|
|
1165
|
+
{ [`nested${i}.field`]: { $in: [`value${i}`, `other${i}`] } },
|
|
1166
|
+
],
|
|
1167
|
+
})),
|
|
1168
|
+
};
|
|
1169
|
+
const start = Date.now();
|
|
1170
|
+
const results = await vectorStore.query({
|
|
1171
|
+
indexName: filterIndexName,
|
|
1172
|
+
queryVector: createVector(0),
|
|
1173
|
+
filter: complexFilter,
|
|
1174
|
+
});
|
|
1175
|
+
const duration = Date.now() - start;
|
|
1176
|
+
expect(duration).toBeLessThan(1000);
|
|
1177
|
+
expect(Array.isArray(results)).toBe(true);
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
describe('Error Cases', () => {
|
|
1182
|
+
it('should reject invalid operators', async () => {
|
|
1183
|
+
await expect(
|
|
1184
|
+
vectorStore.query({
|
|
1185
|
+
indexName: filterIndexName,
|
|
1186
|
+
queryVector: createVector(0),
|
|
1187
|
+
filter: { field: { $invalidOp: 'value' } as any },
|
|
1188
|
+
}),
|
|
1189
|
+
).rejects.toThrow();
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it('should reject empty brackets', async () => {
|
|
1193
|
+
await expect(
|
|
1194
|
+
vectorStore.query({
|
|
1195
|
+
indexName: filterIndexName,
|
|
1196
|
+
queryVector: createVector(0),
|
|
1197
|
+
filter: { 'industries[]': 'Tourism' },
|
|
1198
|
+
}),
|
|
1199
|
+
).rejects.toThrow();
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('should reject unclosed brackets', async () => {
|
|
1203
|
+
await expect(
|
|
1204
|
+
vectorStore.query({
|
|
1205
|
+
indexName: filterIndexName,
|
|
1206
|
+
queryVector: createVector(0),
|
|
1207
|
+
filter: { 'industries[': 'Tourism' },
|
|
1208
|
+
}),
|
|
1209
|
+
).rejects.toThrow();
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it('should handle invalid array syntax by returning empty results', async () => {
|
|
1213
|
+
const results = await vectorStore.query({
|
|
1214
|
+
indexName: filterIndexName,
|
|
1215
|
+
queryVector: createVector(0),
|
|
1216
|
+
filter: { 'industries#-1]': 'Tourism' },
|
|
1217
|
+
});
|
|
1218
|
+
expect(results).toHaveLength(0);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it('should reject invalid field paths', async () => {
|
|
1222
|
+
await expect(
|
|
1223
|
+
vectorStore.query({
|
|
1224
|
+
indexName: filterIndexName,
|
|
1225
|
+
queryVector: createVector(0),
|
|
1226
|
+
filter: { '.invalidPath': 'value' },
|
|
1227
|
+
}),
|
|
1228
|
+
).rejects.toThrow();
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('should handle malformed complex queries by returning all results', async () => {
|
|
1232
|
+
// Upstash treats malformed logical operators as non-filtering conditions
|
|
1233
|
+
// rather than throwing errors
|
|
1234
|
+
const results = await vectorStore.query({
|
|
1235
|
+
indexName: filterIndexName,
|
|
1236
|
+
queryVector: createVector(0),
|
|
1237
|
+
filter: { $and: { not: 'an array' } as any },
|
|
1238
|
+
});
|
|
1239
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
describe('Hybrid Vector Operations (Sparse + Dense)', () => {
|
|
1244
|
+
const hybridIndexName = `mastra-hybrid-${Date.now()}-${Math.random().toString(36).substring(2)}`;
|
|
1245
|
+
|
|
1246
|
+
// Helper function to create a normalized vector
|
|
1247
|
+
const createVector = (primaryDimension: number, value: number = 1.0): number[] => {
|
|
1248
|
+
const vector = new Array(VECTOR_DIMENSION).fill(0);
|
|
1249
|
+
vector[primaryDimension] = value;
|
|
1250
|
+
// Normalize the vector for cosine similarity
|
|
1251
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
1252
|
+
return vector.map(val => val / magnitude);
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
afterEach(async () => {
|
|
1256
|
+
try {
|
|
1257
|
+
await vectorStore.deleteIndex({ indexName: hybridIndexName });
|
|
1258
|
+
} catch {
|
|
1259
|
+
// Index might not exist
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it('should upsert vectors with sparse vectors', async () => {
|
|
1264
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1265
|
+
const sparseVectors = [
|
|
1266
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1267
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1268
|
+
];
|
|
1269
|
+
const metadata = [{ type: 'sparse-test-1' }, { type: 'sparse-test-2' }];
|
|
1270
|
+
|
|
1271
|
+
const ids = await vectorStore.upsert({
|
|
1272
|
+
indexName: hybridIndexName,
|
|
1273
|
+
vectors,
|
|
1274
|
+
sparseVectors,
|
|
1275
|
+
metadata,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
expect(ids).toHaveLength(2);
|
|
1279
|
+
expect(ids[0]).toBeDefined();
|
|
1280
|
+
expect(ids[1]).toBeDefined();
|
|
1281
|
+
|
|
1282
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1283
|
+
}, 30000);
|
|
1284
|
+
|
|
1285
|
+
it('should query with sparse vector for hybrid search', async () => {
|
|
1286
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1287
|
+
const sparseVectors = [
|
|
1288
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1289
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1290
|
+
];
|
|
1291
|
+
const metadata = [{ type: 'hybrid-query-test-1' }, { type: 'hybrid-query-test-2' }];
|
|
1292
|
+
|
|
1293
|
+
await vectorStore.upsert({
|
|
1294
|
+
indexName: hybridIndexName,
|
|
1295
|
+
vectors,
|
|
1296
|
+
sparseVectors,
|
|
1297
|
+
metadata,
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1301
|
+
|
|
1302
|
+
const results = await vectorStore.query({
|
|
1303
|
+
indexName: hybridIndexName,
|
|
1304
|
+
queryVector: createVector(0, 0.9),
|
|
1305
|
+
sparseVector: { indices: [1, 5], values: [0.9, 0.7] },
|
|
1306
|
+
topK: 2,
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
expect(results).toHaveLength(2);
|
|
1310
|
+
expect(results[0]?.metadata).toBeDefined();
|
|
1311
|
+
expect(results[0]?.score).toBeGreaterThan(0);
|
|
1312
|
+
}, 30000);
|
|
1313
|
+
|
|
1314
|
+
it('should query with fusion algorithm', async () => {
|
|
1315
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1316
|
+
const sparseVectors = [
|
|
1317
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1318
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1319
|
+
];
|
|
1320
|
+
|
|
1321
|
+
await vectorStore.upsert({
|
|
1322
|
+
indexName: hybridIndexName,
|
|
1323
|
+
vectors,
|
|
1324
|
+
sparseVectors,
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1328
|
+
|
|
1329
|
+
const { FusionAlgorithm } = await import('@upstash/vector');
|
|
1330
|
+
|
|
1331
|
+
const results = await vectorStore.query({
|
|
1332
|
+
indexName: hybridIndexName,
|
|
1333
|
+
queryVector: createVector(0, 0.9),
|
|
1334
|
+
sparseVector: { indices: [1, 5], values: [0.9, 0.7] },
|
|
1335
|
+
fusionAlgorithm: FusionAlgorithm.RRF,
|
|
1336
|
+
topK: 2,
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
expect(results).toHaveLength(2);
|
|
1340
|
+
expect(results[0]?.score).toBeGreaterThan(0);
|
|
1341
|
+
}, 30000);
|
|
1342
|
+
|
|
1343
|
+
it('should work with dense-only queries (backward compatibility)', async () => {
|
|
1344
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1345
|
+
const sparseVectors = [
|
|
1346
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1347
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1348
|
+
];
|
|
1349
|
+
|
|
1350
|
+
await vectorStore.upsert({
|
|
1351
|
+
indexName: hybridIndexName,
|
|
1352
|
+
vectors,
|
|
1353
|
+
sparseVectors,
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1357
|
+
|
|
1358
|
+
const results = await vectorStore.query({
|
|
1359
|
+
indexName: hybridIndexName,
|
|
1360
|
+
queryVector: createVector(0, 0.9),
|
|
1361
|
+
topK: 2,
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
expect(results).toHaveLength(2);
|
|
1365
|
+
expect(results[0]?.score).toBeGreaterThan(0);
|
|
1366
|
+
}, 30000);
|
|
1367
|
+
|
|
1368
|
+
it('should support QueryMode for dense-only queries', async () => {
|
|
1369
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1370
|
+
const sparseVectors = [
|
|
1371
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1372
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1373
|
+
];
|
|
1374
|
+
|
|
1375
|
+
await vectorStore.upsert({
|
|
1376
|
+
indexName: hybridIndexName,
|
|
1377
|
+
vectors,
|
|
1378
|
+
sparseVectors,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1382
|
+
|
|
1383
|
+
const { QueryMode } = await import('@upstash/vector');
|
|
1384
|
+
|
|
1385
|
+
const results = await vectorStore.query({
|
|
1386
|
+
indexName: hybridIndexName,
|
|
1387
|
+
queryVector: createVector(0, 0.9),
|
|
1388
|
+
queryMode: QueryMode.DENSE,
|
|
1389
|
+
topK: 2,
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
expect(results).toHaveLength(2);
|
|
1393
|
+
expect(results[0]?.score).toBeGreaterThan(0);
|
|
1394
|
+
}, 30000);
|
|
1395
|
+
|
|
1396
|
+
it('should support QueryMode for sparse-only queries', async () => {
|
|
1397
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1398
|
+
const sparseVectors = [
|
|
1399
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1400
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1401
|
+
];
|
|
1402
|
+
|
|
1403
|
+
await vectorStore.upsert({
|
|
1404
|
+
indexName: hybridIndexName,
|
|
1405
|
+
vectors,
|
|
1406
|
+
sparseVectors,
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1410
|
+
|
|
1411
|
+
const { QueryMode } = await import('@upstash/vector');
|
|
1412
|
+
|
|
1413
|
+
const results = await vectorStore.query({
|
|
1414
|
+
indexName: hybridIndexName,
|
|
1415
|
+
queryVector: createVector(0, 0.9),
|
|
1416
|
+
sparseVector: { indices: [1, 5], values: [0.9, 0.7] },
|
|
1417
|
+
queryMode: QueryMode.SPARSE,
|
|
1418
|
+
topK: 2,
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
expect(results).toHaveLength(2);
|
|
1422
|
+
expect(results[0]?.score).toBeGreaterThan(0);
|
|
1423
|
+
}, 30000);
|
|
1424
|
+
|
|
1425
|
+
it('should support QueryMode for hybrid queries', async () => {
|
|
1426
|
+
const vectors = [createVector(0, 1.0), createVector(1, 1.0)];
|
|
1427
|
+
const sparseVectors = [
|
|
1428
|
+
{ indices: [1, 5, 10], values: [0.8, 0.6, 0.4] },
|
|
1429
|
+
{ indices: [2, 6, 11], values: [0.7, 0.5, 0.3] },
|
|
1430
|
+
];
|
|
1431
|
+
|
|
1432
|
+
await vectorStore.upsert({
|
|
1433
|
+
indexName: hybridIndexName,
|
|
1434
|
+
vectors,
|
|
1435
|
+
sparseVectors,
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
await waitUntilVectorsIndexed(vectorStore, hybridIndexName, 2);
|
|
1439
|
+
|
|
1440
|
+
const { QueryMode } = await import('@upstash/vector');
|
|
1441
|
+
|
|
1442
|
+
const results = await vectorStore.query({
|
|
1443
|
+
indexName: hybridIndexName,
|
|
1444
|
+
queryVector: createVector(0, 0.9),
|
|
1445
|
+
sparseVector: { indices: [1, 5], values: [0.9, 0.7] },
|
|
1446
|
+
queryMode: QueryMode.HYBRID,
|
|
1447
|
+
topK: 2,
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
expect(results).toHaveLength(2);
|
|
1451
|
+
expect(results[0]?.score).toBeGreaterThan(0);
|
|
1452
|
+
}, 30000);
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1455
|
+
});
|