@mastra/vectorize 0.1.0-alpha.29
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 +262 -0
- package/LICENSE +44 -0
- package/README.md +39 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +163 -0
- package/package.json +32 -0
- package/src/index.ts +1 -0
- package/src/vector/filter.test.ts +198 -0
- package/src/vector/filter.ts +68 -0
- package/src/vector/index.test.ts +679 -0
- package/src/vector/index.ts +150 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { CloudflareVector } from './';
|
|
5
|
+
|
|
6
|
+
function waitUntilReady(vector: CloudflareVector, indexName: string) {
|
|
7
|
+
return new Promise(resolve => {
|
|
8
|
+
const interval = setInterval(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const stats = await vector.describeIndex(indexName);
|
|
11
|
+
if (!!stats) {
|
|
12
|
+
clearInterval(interval);
|
|
13
|
+
resolve(true);
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.log(error);
|
|
17
|
+
}
|
|
18
|
+
}, 5000);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function waitUntilVectorsIndexed(vector: CloudflareVector, indexName: string, expectedCount: number) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const maxAttempts = 30;
|
|
25
|
+
let attempts = 0;
|
|
26
|
+
const interval = setInterval(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const stats = await vector.describeIndex(indexName);
|
|
29
|
+
if (stats && stats.count >= expectedCount) {
|
|
30
|
+
clearInterval(interval);
|
|
31
|
+
resolve(true);
|
|
32
|
+
}
|
|
33
|
+
attempts++;
|
|
34
|
+
if (attempts >= maxAttempts) {
|
|
35
|
+
clearInterval(interval);
|
|
36
|
+
reject(new Error('Timeout waiting for vectors to be indexed'));
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.log(error);
|
|
40
|
+
}
|
|
41
|
+
}, 5000);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function waitForMetadataIndexes(vector: CloudflareVector, indexName: string, expectedCount: number) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const maxAttempts = 30;
|
|
48
|
+
let attempts = 0;
|
|
49
|
+
const interval = setInterval(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const indexes = await vector.listMetadataIndexes(indexName);
|
|
52
|
+
if (indexes && indexes.length === expectedCount) {
|
|
53
|
+
clearInterval(interval);
|
|
54
|
+
resolve(true);
|
|
55
|
+
}
|
|
56
|
+
attempts++;
|
|
57
|
+
if (attempts >= maxAttempts) {
|
|
58
|
+
clearInterval(interval);
|
|
59
|
+
reject(new Error('Timeout waiting for metadata indexes to be created'));
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.log(error);
|
|
63
|
+
}
|
|
64
|
+
}, 5000);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('CloudflareVector', () => {
|
|
69
|
+
let vectorDB: CloudflareVector;
|
|
70
|
+
const VECTOR_DIMENSION = 1536;
|
|
71
|
+
const testIndexName = `default-${randomUUID()}`;
|
|
72
|
+
const testIndexName2 = `default-${randomUUID()}`;
|
|
73
|
+
|
|
74
|
+
// Helper function to create a normalized vector
|
|
75
|
+
const createVector = (primaryDimension: number, value: number = 1.0): number[] => {
|
|
76
|
+
const vector = new Array(VECTOR_DIMENSION).fill(0);
|
|
77
|
+
vector[primaryDimension] = value;
|
|
78
|
+
// Normalize the vector for cosine similarity
|
|
79
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
80
|
+
return vector.map(val => val / magnitude);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
beforeAll(() => {
|
|
84
|
+
// Load from environment variables for CI/CD
|
|
85
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
86
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
87
|
+
|
|
88
|
+
if (!accountId || !apiToken) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'Missing required environment variables: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_VECTORIZE_ID',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
vectorDB = new CloudflareVector({ accountId, apiToken });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterAll(async () => {
|
|
98
|
+
try {
|
|
99
|
+
await vectorDB.deleteIndex(testIndexName);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn('Failed to delete test index:', error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Index Operations', () => {
|
|
106
|
+
const tempIndexName = 'test_temp_index';
|
|
107
|
+
|
|
108
|
+
it('should create and list indexes', async () => {
|
|
109
|
+
await vectorDB.createIndex(tempIndexName, VECTOR_DIMENSION, 'cosine');
|
|
110
|
+
await waitUntilReady(vectorDB, tempIndexName);
|
|
111
|
+
const indexes = await vectorDB.listIndexes();
|
|
112
|
+
expect(indexes).toContain(tempIndexName);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should describe an index correctly', async () => {
|
|
116
|
+
const stats = await vectorDB.describeIndex(tempIndexName);
|
|
117
|
+
expect(stats).toEqual({
|
|
118
|
+
dimension: VECTOR_DIMENSION,
|
|
119
|
+
metric: 'cosine',
|
|
120
|
+
count: 0,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should delete an index', async () => {
|
|
125
|
+
await vectorDB.deleteIndex(tempIndexName);
|
|
126
|
+
const indexes = await vectorDB.listIndexes();
|
|
127
|
+
expect(indexes).not.toContain(tempIndexName);
|
|
128
|
+
});
|
|
129
|
+
}, 30000);
|
|
130
|
+
|
|
131
|
+
describe('Vector Operations', () => {
|
|
132
|
+
let vectorIds: string[];
|
|
133
|
+
it('should create index before operations', async () => {
|
|
134
|
+
await vectorDB.createIndex(testIndexName, VECTOR_DIMENSION, 'cosine');
|
|
135
|
+
await waitUntilReady(vectorDB, testIndexName);
|
|
136
|
+
const indexes = await vectorDB.listIndexes();
|
|
137
|
+
expect(indexes).toContain(testIndexName);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should insert vectors and query them', async () => {
|
|
141
|
+
const testVectors = [createVector(0, 1.0), createVector(1, 1.0), createVector(2, 1.0)];
|
|
142
|
+
|
|
143
|
+
const testMetadata = [{ label: 'first-dimension' }, { label: 'second-dimension' }, { label: 'third-dimension' }];
|
|
144
|
+
|
|
145
|
+
vectorIds = await vectorDB.upsert(testIndexName, testVectors, testMetadata);
|
|
146
|
+
expect(vectorIds).toHaveLength(3);
|
|
147
|
+
|
|
148
|
+
await waitUntilVectorsIndexed(vectorDB, testIndexName, 3);
|
|
149
|
+
const stats = await vectorDB.describeIndex(testIndexName);
|
|
150
|
+
expect(stats.count).toBeGreaterThan(0);
|
|
151
|
+
|
|
152
|
+
const results = await vectorDB.query(testIndexName, createVector(0, 0.9), 3);
|
|
153
|
+
expect(results).toHaveLength(3);
|
|
154
|
+
|
|
155
|
+
if (results.length > 0) {
|
|
156
|
+
expect(results[0].metadata).toEqual({ label: 'first-dimension' });
|
|
157
|
+
}
|
|
158
|
+
}, 30000);
|
|
159
|
+
|
|
160
|
+
it('should query vectors and return vector in results', async () => {
|
|
161
|
+
const results = await vectorDB.query(testIndexName, createVector(0, 0.9), 3, undefined, true);
|
|
162
|
+
|
|
163
|
+
expect(results).toHaveLength(3);
|
|
164
|
+
|
|
165
|
+
for (const result of results) {
|
|
166
|
+
expect(result.vector).toBeDefined();
|
|
167
|
+
expect(result.vector).toHaveLength(VECTOR_DIMENSION);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}, 30000);
|
|
171
|
+
|
|
172
|
+
describe('Error Handling', () => {
|
|
173
|
+
it('should handle invalid dimension vectors', async () => {
|
|
174
|
+
await expect(vectorDB.upsert(testIndexName, [[1.0, 0.0]])).rejects.toThrow();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle querying with wrong dimensions', async () => {
|
|
178
|
+
await expect(vectorDB.query(testIndexName, [1.0, 0.0])).rejects.toThrow();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle non-existent index operations', async () => {
|
|
182
|
+
const nonExistentIndex = 'non_existent_index';
|
|
183
|
+
await expect(vectorDB.query(nonExistentIndex, createVector(0, 1.0))).rejects.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('rejects queries with filter keys longer than 512 characters', async () => {
|
|
187
|
+
const longKey = 'a'.repeat(513);
|
|
188
|
+
const filter = { [longKey]: 'value' };
|
|
189
|
+
|
|
190
|
+
await expect(vectorDB.query(testIndexName, createVector(0, 0.9), 10, filter)).rejects.toThrow();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects queries with filter keys containing invalid characters', async () => {
|
|
194
|
+
const invalidFilters = [
|
|
195
|
+
{ 'field"name': 'value' }, // Contains "
|
|
196
|
+
{ $field: 'value' }, // Contains $
|
|
197
|
+
{ '': 'value' }, // Empty key
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
for (const filter of invalidFilters) {
|
|
201
|
+
await expect(vectorDB.query(testIndexName, createVector(0, 0.9), 10, filter)).rejects.toThrow();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('allows queries with valid range operator combinations', async () => {
|
|
206
|
+
const validFilters = [
|
|
207
|
+
{ field: { $gt: 5, $lt: 10 } },
|
|
208
|
+
{ field: { $gte: 0, $lte: 100 } },
|
|
209
|
+
{ field: { $gt: 5, $lte: 10 } },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const filter of validFilters) {
|
|
213
|
+
await expect(vectorDB.query(testIndexName, createVector(0, 0.9), 10, filter)).resolves.not.toThrow();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('rejects queries with empty object field values', async () => {
|
|
218
|
+
const emptyFilters = { field: {} };
|
|
219
|
+
await expect(vectorDB.query(testIndexName, createVector(0, 0.9), 10, emptyFilters)).rejects.toThrow();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('rejects oversized filter queries', async () => {
|
|
223
|
+
const largeFilter = {
|
|
224
|
+
field1: { $in: Array(1000).fill('test') },
|
|
225
|
+
field2: { $in: Array(1000).fill(123) },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await expect(vectorDB.query(testIndexName, createVector(0, 0.9), 10, largeFilter)).rejects.toThrow();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('rejects queries with array values in comparison operators', async () => {
|
|
232
|
+
await expect(
|
|
233
|
+
vectorDB.query(testIndexName, createVector(0, 0.9), 10, {
|
|
234
|
+
field: { $gt: [] },
|
|
235
|
+
}),
|
|
236
|
+
).rejects.toThrow();
|
|
237
|
+
|
|
238
|
+
await expect(
|
|
239
|
+
vectorDB.query(testIndexName, createVector(0, 0.9), 10, {
|
|
240
|
+
field: { $lt: [1, 2, 3] },
|
|
241
|
+
}),
|
|
242
|
+
).rejects.toThrow();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('Metadata Filter Tests', () => {
|
|
247
|
+
beforeAll(async () => {
|
|
248
|
+
await vectorDB.createIndex(testIndexName2, VECTOR_DIMENSION, 'cosine');
|
|
249
|
+
await waitUntilReady(vectorDB, testIndexName2);
|
|
250
|
+
|
|
251
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'price', 'number');
|
|
252
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'category', 'string');
|
|
253
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'rating', 'number');
|
|
254
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'nested.number', 'number');
|
|
255
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'nested.string', 'string');
|
|
256
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'nested.boolean', 'boolean');
|
|
257
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'isActive', 'boolean');
|
|
258
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'code', 'string');
|
|
259
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'optionalField', 'string');
|
|
260
|
+
await vectorDB.createMetadataIndex(testIndexName2, 'mixedField', 'string');
|
|
261
|
+
|
|
262
|
+
await waitForMetadataIndexes(vectorDB, testIndexName2, 10);
|
|
263
|
+
|
|
264
|
+
// Create all test vectors and metadata at once
|
|
265
|
+
const vectors = [
|
|
266
|
+
// Base test vectors
|
|
267
|
+
createVector(0, 1.0),
|
|
268
|
+
createVector(1, 1.0),
|
|
269
|
+
createVector(2, 1.0),
|
|
270
|
+
createVector(3, 1.0),
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const metadata = [
|
|
274
|
+
// Base test metadata
|
|
275
|
+
{
|
|
276
|
+
price: 100,
|
|
277
|
+
category: 'electronics',
|
|
278
|
+
rating: 4.5,
|
|
279
|
+
nested: {
|
|
280
|
+
number: 100,
|
|
281
|
+
string: 'premium',
|
|
282
|
+
boolean: true,
|
|
283
|
+
},
|
|
284
|
+
isActive: true,
|
|
285
|
+
mixedField: 'string value',
|
|
286
|
+
code: 'A123',
|
|
287
|
+
optionalField: 'exists',
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
price: 200,
|
|
291
|
+
category: 'electronics',
|
|
292
|
+
rating: 3.8,
|
|
293
|
+
nested: {
|
|
294
|
+
number: 200,
|
|
295
|
+
string: 'premium',
|
|
296
|
+
boolean: false,
|
|
297
|
+
},
|
|
298
|
+
isActive: false,
|
|
299
|
+
mixedField: 10,
|
|
300
|
+
code: 'B456',
|
|
301
|
+
optionalField: null,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
price: 150,
|
|
305
|
+
category: 'accessories',
|
|
306
|
+
rating: 4.2,
|
|
307
|
+
nested: {
|
|
308
|
+
number: 150,
|
|
309
|
+
string: 'premium',
|
|
310
|
+
boolean: true,
|
|
311
|
+
},
|
|
312
|
+
isActive: false,
|
|
313
|
+
mixedField: false,
|
|
314
|
+
code: 'C789',
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
price: 75,
|
|
318
|
+
category: 'accessories',
|
|
319
|
+
rating: 0,
|
|
320
|
+
nested: {
|
|
321
|
+
number: 75,
|
|
322
|
+
string: 'basic',
|
|
323
|
+
boolean: false,
|
|
324
|
+
},
|
|
325
|
+
isActive: false,
|
|
326
|
+
mixedField: true,
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
await vectorDB.upsert(testIndexName2, vectors, metadata);
|
|
331
|
+
await waitUntilVectorsIndexed(vectorDB, testIndexName2, vectors.length);
|
|
332
|
+
|
|
333
|
+
const stats = await vectorDB.describeIndex(testIndexName2);
|
|
334
|
+
expect(stats.count).toBe(vectors.length);
|
|
335
|
+
}, 300000);
|
|
336
|
+
|
|
337
|
+
afterAll(async () => {
|
|
338
|
+
const currentMetadata = await vectorDB.listMetadataIndexes(testIndexName2);
|
|
339
|
+
for (const { propertyName } of currentMetadata) {
|
|
340
|
+
await vectorDB.deleteMetadataIndex(testIndexName2, propertyName as string);
|
|
341
|
+
}
|
|
342
|
+
await vectorDB.deleteIndex(testIndexName2);
|
|
343
|
+
}, 300000);
|
|
344
|
+
|
|
345
|
+
describe('Basic Equality Operators', () => {
|
|
346
|
+
it('filters with $eq operator', async () => {
|
|
347
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
348
|
+
category: 'electronics',
|
|
349
|
+
});
|
|
350
|
+
expect(results.length).toBe(2);
|
|
351
|
+
results.forEach(result => {
|
|
352
|
+
expect(result.metadata?.category).toBe('electronics');
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('filters with $ne operator', async () => {
|
|
357
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
358
|
+
category: { $ne: 'electronics' },
|
|
359
|
+
});
|
|
360
|
+
expect(results.length).toBe(2);
|
|
361
|
+
results.forEach(result => {
|
|
362
|
+
expect(result.metadata?.category).not.toBe('electronics');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('Numeric Comparison Operators', () => {
|
|
368
|
+
it('filters with $gt operator', async () => {
|
|
369
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
370
|
+
price: { $gt: 150 },
|
|
371
|
+
});
|
|
372
|
+
expect(results.length).toBe(1);
|
|
373
|
+
results.forEach(result => {
|
|
374
|
+
expect(Number(result.metadata?.price)).toBeGreaterThan(150);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('filters with $gte operator', async () => {
|
|
379
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
380
|
+
price: { $gte: 100 },
|
|
381
|
+
});
|
|
382
|
+
expect(results.length).toBe(3);
|
|
383
|
+
results.forEach(result => {
|
|
384
|
+
const price = Number(result.metadata?.price);
|
|
385
|
+
expect(price).toBeGreaterThanOrEqual(100);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('filters with $lt operator', async () => {
|
|
390
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
391
|
+
price: { $lt: 150 },
|
|
392
|
+
});
|
|
393
|
+
expect(results.length).toBe(2);
|
|
394
|
+
results.forEach(result => {
|
|
395
|
+
const price = Number(result.metadata?.price);
|
|
396
|
+
expect(price).toBeLessThan(150);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('filters with $lte operator', async () => {
|
|
401
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
402
|
+
price: { $lte: 150 },
|
|
403
|
+
});
|
|
404
|
+
expect(results.length).toBe(3);
|
|
405
|
+
results.forEach(result => {
|
|
406
|
+
const price = Number(result.metadata?.price);
|
|
407
|
+
expect(price).toBeLessThanOrEqual(150);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Array Operators', () => {
|
|
413
|
+
it('filters with $in operator for exact matches', async () => {
|
|
414
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
415
|
+
category: { $in: ['electronics'] },
|
|
416
|
+
});
|
|
417
|
+
expect(results.length).toBe(2);
|
|
418
|
+
results.forEach(result => {
|
|
419
|
+
expect(result.metadata?.category).toContain('electronics');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('filters with $nin operator', async () => {
|
|
424
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
425
|
+
category: { $nin: ['electronics'] },
|
|
426
|
+
});
|
|
427
|
+
expect(results.length).toBe(2);
|
|
428
|
+
results.forEach(result => {
|
|
429
|
+
expect(result.metadata?.category).not.toContain('electronics');
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('Boolean Operations', () => {
|
|
435
|
+
it('filters with boolean values', async () => {
|
|
436
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
437
|
+
isActive: true,
|
|
438
|
+
});
|
|
439
|
+
expect(results.length).toBe(1);
|
|
440
|
+
expect(results[0]?.metadata?.isActive).toBe(true);
|
|
441
|
+
}, 5000);
|
|
442
|
+
|
|
443
|
+
it('filters with $ne on boolean values', async () => {
|
|
444
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
445
|
+
isActive: { $ne: true },
|
|
446
|
+
});
|
|
447
|
+
expect(results.length).toBe(3);
|
|
448
|
+
results.forEach(result => {
|
|
449
|
+
expect(result.metadata?.isActive).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
}, 5000);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('Nested Field Operations', () => {
|
|
455
|
+
it('filters on nested fields with comparison operators', async () => {
|
|
456
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
457
|
+
'nested.number': { $gt: 100 },
|
|
458
|
+
});
|
|
459
|
+
expect(results.length).toBe(2);
|
|
460
|
+
results.forEach(result => {
|
|
461
|
+
expect(result.metadata?.nested?.number).toBeGreaterThan(100);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('combines nested field filters with top-level filters', async () => {
|
|
466
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
467
|
+
'nested.number': { $lt: 200 },
|
|
468
|
+
category: 'electronics',
|
|
469
|
+
});
|
|
470
|
+
expect(results.length).toBe(1);
|
|
471
|
+
expect(results[0]?.metadata?.nested?.number).toBeLessThan(200);
|
|
472
|
+
expect(results[0]?.metadata?.category).toBe('electronics');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('handles nested string equality', async () => {
|
|
476
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
477
|
+
'nested.string': 'premium',
|
|
478
|
+
});
|
|
479
|
+
expect(results.length).toBe(3);
|
|
480
|
+
results.forEach(result => {
|
|
481
|
+
expect(result.metadata?.nested?.string).toBe('premium');
|
|
482
|
+
});
|
|
483
|
+
}, 5000);
|
|
484
|
+
|
|
485
|
+
it('combines nested numeric and boolean conditions', async () => {
|
|
486
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
487
|
+
'nested.number': { $gt: 100 },
|
|
488
|
+
'nested.boolean': true,
|
|
489
|
+
});
|
|
490
|
+
expect(results.length).toBe(1);
|
|
491
|
+
expect(results[0]?.metadata?.nested?.number).toBeGreaterThan(100);
|
|
492
|
+
expect(results[0]?.metadata?.nested?.boolean).toBe(true);
|
|
493
|
+
}, 5000);
|
|
494
|
+
|
|
495
|
+
it('handles multiple nested field comparisons', async () => {
|
|
496
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
497
|
+
'nested.string': 'premium',
|
|
498
|
+
'nested.number': { $lt: 200 },
|
|
499
|
+
'nested.boolean': true,
|
|
500
|
+
});
|
|
501
|
+
expect(results.length).toBe(2);
|
|
502
|
+
const result = results[0]?.metadata?.nested;
|
|
503
|
+
expect(result?.string).toBe('premium');
|
|
504
|
+
expect(result?.number).toBeLessThan(200);
|
|
505
|
+
expect(result?.boolean).toBe(true);
|
|
506
|
+
}, 5000);
|
|
507
|
+
|
|
508
|
+
it('handles $in with nested string values', async () => {
|
|
509
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
510
|
+
'nested.string': { $in: ['premium', 'basic'] },
|
|
511
|
+
});
|
|
512
|
+
expect(results.length).toBe(4);
|
|
513
|
+
results.forEach(result => {
|
|
514
|
+
expect(['premium', 'basic']).toContain(result.metadata?.nested?.string);
|
|
515
|
+
});
|
|
516
|
+
}, 5000);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('String Operations', () => {
|
|
520
|
+
it('handles string numbers in numeric comparisons', async () => {
|
|
521
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
522
|
+
price: { $gt: '150' }, // String number
|
|
523
|
+
});
|
|
524
|
+
expect(results.length).toBe(1);
|
|
525
|
+
expect(Number(results[0]?.metadata?.price)).toBeGreaterThan(150);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('handles mixed numeric and string comparisons', async () => {
|
|
529
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
530
|
+
price: { $gt: 100 },
|
|
531
|
+
category: { $in: ['electronics'] },
|
|
532
|
+
});
|
|
533
|
+
expect(results.length).toBe(1);
|
|
534
|
+
expect(Number(results[0]?.metadata?.price)).toBeGreaterThan(100);
|
|
535
|
+
expect(results[0]?.metadata?.category).toBe('electronics');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('Filter Validation and Edge Cases', () => {
|
|
540
|
+
it('handles numeric zero values correctly', async () => {
|
|
541
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
542
|
+
rating: { $eq: 0 },
|
|
543
|
+
});
|
|
544
|
+
expect(results.length).toBe(1);
|
|
545
|
+
expect(results[0]?.metadata?.rating).toBe(0);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('handles multiple conditions on same field', async () => {
|
|
549
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
550
|
+
price: { $gt: 75, $lt: 200 },
|
|
551
|
+
});
|
|
552
|
+
expect(results.length).toBe(2);
|
|
553
|
+
results.forEach(result => {
|
|
554
|
+
const price = Number(result.metadata?.price);
|
|
555
|
+
expect(price).toBeGreaterThan(75);
|
|
556
|
+
expect(price).toBeLessThan(200);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('handles exact numeric equality', async () => {
|
|
561
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
562
|
+
price: { $eq: 100 },
|
|
563
|
+
});
|
|
564
|
+
expect(results.length).toBe(1);
|
|
565
|
+
expect(results[0]?.metadata?.price).toBe(100);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('handles boundary conditions in ranges', async () => {
|
|
569
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
570
|
+
price: { $gte: 75, $lte: 75 },
|
|
571
|
+
});
|
|
572
|
+
expect(results.length).toBe(1);
|
|
573
|
+
expect(results[0]?.metadata?.price).toBe(75);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('String Range Queries', () => {
|
|
578
|
+
it('handles lexicographical ordering in string range queries', async () => {
|
|
579
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
580
|
+
code: { $gt: 'A123', $lt: 'C789' },
|
|
581
|
+
});
|
|
582
|
+
expect(results.length).toBe(1);
|
|
583
|
+
expect(results[0]?.metadata?.code).toBe('B456');
|
|
584
|
+
}, 5000);
|
|
585
|
+
|
|
586
|
+
it('handles string range queries with special characters', async () => {
|
|
587
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
588
|
+
code: { $gte: 'A', $lt: 'C' },
|
|
589
|
+
});
|
|
590
|
+
expect(results.length).toBe(2);
|
|
591
|
+
results.forEach(result => {
|
|
592
|
+
expect(result.metadata?.code).toMatch(/^[AB]/);
|
|
593
|
+
});
|
|
594
|
+
}, 5000);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
describe('Null and Special Values', () => {
|
|
598
|
+
it('handles $in with null values', async () => {
|
|
599
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
600
|
+
optionalField: { $in: [null, 'exists'] },
|
|
601
|
+
});
|
|
602
|
+
expect(results.length).toBe(1);
|
|
603
|
+
}, 5000);
|
|
604
|
+
|
|
605
|
+
it('handles $ne with null values', async () => {
|
|
606
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
607
|
+
optionalField: { $ne: null },
|
|
608
|
+
});
|
|
609
|
+
expect(results.length).toBe(4);
|
|
610
|
+
expect(results[0]?.metadata?.optionalField).toBe('exists');
|
|
611
|
+
}, 5000);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe('Mixed Type Arrays and Values', () => {
|
|
615
|
+
it('handles $in with mixed type arrays', async () => {
|
|
616
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
617
|
+
mixedField: { $in: ['string value', 10, null] },
|
|
618
|
+
});
|
|
619
|
+
expect(results.length).toBe(2);
|
|
620
|
+
}, 5000);
|
|
621
|
+
|
|
622
|
+
it('combines different types of filters', async () => {
|
|
623
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {
|
|
624
|
+
mixedField: { $in: ['string value', true] },
|
|
625
|
+
price: { $eq: 100 },
|
|
626
|
+
});
|
|
627
|
+
expect(results.length).toBe(1);
|
|
628
|
+
}, 5000);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe('Filter Size and Structure Validation', () => {
|
|
632
|
+
it('handles filters approaching size limit', async () => {
|
|
633
|
+
// Create a filter that's close to but under 2048 bytes
|
|
634
|
+
const longString = 'a'.repeat(400);
|
|
635
|
+
const filter = {
|
|
636
|
+
category: { $in: [longString, longString.slice(0, 100)] },
|
|
637
|
+
price: { $gt: 0, $lt: 1000 },
|
|
638
|
+
'nested.string': longString.slice(0, 200),
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
await expect(vectorDB.query(testIndexName2, createVector(0, 1.0), 10, filter)).resolves.toBeDefined();
|
|
642
|
+
}, 5000);
|
|
643
|
+
|
|
644
|
+
it('handles valid range query combinations', async () => {
|
|
645
|
+
const validRangeCombinations = [
|
|
646
|
+
{ price: { $gt: 0, $lt: 1000 } },
|
|
647
|
+
{ price: { $gte: 100, $lte: 200 } },
|
|
648
|
+
{ price: { $gt: 0, $lte: 1000 } },
|
|
649
|
+
{ price: { $gte: 0, $lt: 1000 } },
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
for (const filter of validRangeCombinations) {
|
|
653
|
+
await expect(vectorDB.query(testIndexName2, createVector(0, 1.0), 10, filter)).resolves.toBeDefined();
|
|
654
|
+
}
|
|
655
|
+
}, 5000);
|
|
656
|
+
|
|
657
|
+
it('should handle undefined filter', async () => {
|
|
658
|
+
const results1 = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, undefined);
|
|
659
|
+
const results2 = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10);
|
|
660
|
+
expect(results1).toEqual(results2);
|
|
661
|
+
expect(results1.length).toBeGreaterThan(0);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('should handle empty object filter', async () => {
|
|
665
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, {});
|
|
666
|
+
const results2 = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10);
|
|
667
|
+
expect(results).toEqual(results2);
|
|
668
|
+
expect(results.length).toBeGreaterThan(0);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('should handle null filter', async () => {
|
|
672
|
+
const results = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10, null as any);
|
|
673
|
+
const results2 = await vectorDB.query(testIndexName2, createVector(0, 1.0), 10);
|
|
674
|
+
expect(results).toEqual(results2);
|
|
675
|
+
expect(results.length).toBeGreaterThan(0);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
}, 3000000);
|
|
679
|
+
});
|