@mastra/qdrant 0.11.8 → 0.11.9-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/CHANGELOG.md +21 -0
- package/package.json +18 -5
- package/.turbo/turbo-build.log +0 -4
- package/docker-compose.yaml +0 -17
- package/eslint.config.js +0 -6
- package/src/index.ts +0 -2
- package/src/vector/filter.test.ts +0 -857
- package/src/vector/filter.ts +0 -388
- package/src/vector/index.test.ts +0 -977
- package/src/vector/index.ts +0 -396
- package/src/vector/prompt.ts +0 -85
- 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,977 +0,0 @@
|
|
|
1
|
-
// To setup a Qdrant server, run:
|
|
2
|
-
// docker run -p 6333:6333 qdrant/qdrant
|
|
3
|
-
import { createVectorTestSuite } from '@internal/storage-test-utils';
|
|
4
|
-
import type { QueryResult } from '@mastra/core/vector';
|
|
5
|
-
import { describe, it, expect, beforeAll, afterAll, afterEach, vi, beforeEach } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import type { QdrantVectorFilter } from './filter';
|
|
8
|
-
import { QdrantVector } from './index';
|
|
9
|
-
|
|
10
|
-
const dimension = 3;
|
|
11
|
-
|
|
12
|
-
describe('QdrantVector', () => {
|
|
13
|
-
let qdrant: QdrantVector;
|
|
14
|
-
const testCollectionName = 'test-collection-' + Date.now();
|
|
15
|
-
|
|
16
|
-
describe('Index Operations', () => {
|
|
17
|
-
beforeAll(async () => {
|
|
18
|
-
qdrant = new QdrantVector({ url: 'http://localhost:6333/' });
|
|
19
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterAll(async () => {
|
|
23
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
24
|
-
}, 50000);
|
|
25
|
-
|
|
26
|
-
it('should list collections including ours', async () => {
|
|
27
|
-
const indexes = await qdrant.listIndexes();
|
|
28
|
-
expect(indexes).toContain(testCollectionName);
|
|
29
|
-
}, 50000);
|
|
30
|
-
|
|
31
|
-
it('should describe index with correct properties', async () => {
|
|
32
|
-
const stats = await qdrant.describeIndex({ indexName: testCollectionName });
|
|
33
|
-
expect(stats.dimension).toBe(dimension);
|
|
34
|
-
expect(stats.metric).toBe('cosine');
|
|
35
|
-
expect(typeof stats.count).toBe('number');
|
|
36
|
-
}, 50000);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('Vector Operations', () => {
|
|
40
|
-
beforeAll(async () => {
|
|
41
|
-
qdrant = new QdrantVector({ url: 'http://localhost:6333/' });
|
|
42
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
afterAll(async () => {
|
|
46
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
47
|
-
}, 50000);
|
|
48
|
-
|
|
49
|
-
const testVectors = [
|
|
50
|
-
[1.0, 0.0, 0.0],
|
|
51
|
-
[0.0, 1.0, 0.0],
|
|
52
|
-
[0.0, 0.0, 1.0],
|
|
53
|
-
];
|
|
54
|
-
const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
|
|
55
|
-
let vectorIds: string[];
|
|
56
|
-
|
|
57
|
-
it('should upsert vectors with metadata', async () => {
|
|
58
|
-
vectorIds = await qdrant.upsert({ indexName: testCollectionName, vectors: testVectors, metadata: testMetadata });
|
|
59
|
-
expect(vectorIds).toHaveLength(3);
|
|
60
|
-
}, 50000);
|
|
61
|
-
|
|
62
|
-
it('should query vectors and return nearest neighbors', async () => {
|
|
63
|
-
const queryVector = [1.0, 0.1, 0.1];
|
|
64
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector, topK: 3 });
|
|
65
|
-
|
|
66
|
-
expect(results).toHaveLength(3);
|
|
67
|
-
expect(results?.[0]?.score).toBeGreaterThan(0);
|
|
68
|
-
expect(results?.[0]?.metadata).toBeDefined();
|
|
69
|
-
}, 50000);
|
|
70
|
-
|
|
71
|
-
it('should query vectors and return vector in results', async () => {
|
|
72
|
-
const queryVector = [1.0, 0.1, 0.1];
|
|
73
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector, topK: 3, includeVector: true });
|
|
74
|
-
|
|
75
|
-
expect(results).toHaveLength(3);
|
|
76
|
-
expect(results?.[0]?.vector).toBeDefined();
|
|
77
|
-
expect(results?.[0]?.vector).toHaveLength(dimension);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should query vectors with metadata filter', async () => {
|
|
81
|
-
const queryVector = [0.0, 1.0, 0.0];
|
|
82
|
-
const filter: QdrantVectorFilter = {
|
|
83
|
-
label: 'y-axis',
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector, topK: 1, filter });
|
|
87
|
-
|
|
88
|
-
expect(results).toHaveLength(1);
|
|
89
|
-
expect(results?.[0]?.metadata?.label).toBe('y-axis');
|
|
90
|
-
}, 50000);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('Vector update operations', () => {
|
|
94
|
-
const testVectors = [
|
|
95
|
-
[1, 2, 3],
|
|
96
|
-
[4, 5, 6],
|
|
97
|
-
[7, 8, 9],
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
beforeEach(async () => {
|
|
101
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension: 3 });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
afterEach(async () => {
|
|
105
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should update the vector by id', async () => {
|
|
109
|
-
const ids = await qdrant.upsert({ indexName: testCollectionName, vectors: testVectors });
|
|
110
|
-
expect(ids).toHaveLength(3);
|
|
111
|
-
|
|
112
|
-
const idToBeUpdated = ids[0];
|
|
113
|
-
const newVector = [1, 2, 3];
|
|
114
|
-
const newMetaData = {
|
|
115
|
-
test: 'updates',
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const update = {
|
|
119
|
-
vector: newVector,
|
|
120
|
-
metadata: newMetaData,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
await qdrant.updateVector({ indexName: testCollectionName, id: idToBeUpdated, update });
|
|
124
|
-
|
|
125
|
-
const results: QueryResult[] = await qdrant.query({
|
|
126
|
-
indexName: testCollectionName,
|
|
127
|
-
queryVector: newVector,
|
|
128
|
-
topK: 2,
|
|
129
|
-
includeVector: true,
|
|
130
|
-
});
|
|
131
|
-
console.log(results);
|
|
132
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
133
|
-
// not matching the vector in results list because, the stored vector is stored in a normalized form inside qdrant
|
|
134
|
-
// expect(results[0]?.vector).toEqual(newVector);
|
|
135
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should only update the metadata by id', async () => {
|
|
139
|
-
const ids = await qdrant.upsert({ indexName: testCollectionName, vectors: testVectors });
|
|
140
|
-
expect(ids).toHaveLength(3);
|
|
141
|
-
|
|
142
|
-
const idToBeUpdated = ids[0];
|
|
143
|
-
const newMetaData = {
|
|
144
|
-
test: 'updates',
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const update = {
|
|
148
|
-
metadata: newMetaData,
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
await qdrant.updateVector({ indexName: testCollectionName, id: idToBeUpdated, update });
|
|
152
|
-
|
|
153
|
-
const results: QueryResult[] = await qdrant.query({
|
|
154
|
-
indexName: testCollectionName,
|
|
155
|
-
queryVector: testVectors[0],
|
|
156
|
-
topK: 2,
|
|
157
|
-
includeVector: true,
|
|
158
|
-
});
|
|
159
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
160
|
-
// not matching the vector in results list because, the stored vector is stored in a normalized form inside qdrant
|
|
161
|
-
// expect(results[0]?.vector).toEqual(testVectors[0]);
|
|
162
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should only update vector embeddings by id', async () => {
|
|
166
|
-
const ids = await qdrant.upsert({ indexName: testCollectionName, vectors: testVectors });
|
|
167
|
-
expect(ids).toHaveLength(3);
|
|
168
|
-
|
|
169
|
-
const idToBeUpdated = ids[0];
|
|
170
|
-
const newVector = [4, 4, 4];
|
|
171
|
-
|
|
172
|
-
const update = {
|
|
173
|
-
vector: newVector,
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
await qdrant.updateVector({ indexName: testCollectionName, id: idToBeUpdated, update });
|
|
177
|
-
|
|
178
|
-
const results: QueryResult[] = await qdrant.query({
|
|
179
|
-
indexName: testCollectionName,
|
|
180
|
-
queryVector: newVector,
|
|
181
|
-
topK: 2,
|
|
182
|
-
includeVector: true,
|
|
183
|
-
});
|
|
184
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
185
|
-
// not matching the vector in results list because, the stored vector is stored in a normalized form inside qdrant
|
|
186
|
-
// expect(results[0]?.vector).toEqual(newVector);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should throw exception when no updates are given', async () => {
|
|
190
|
-
await expect(qdrant.updateVector({ indexName: testCollectionName, id: 'id', update: {} })).rejects.toThrow(
|
|
191
|
-
'No updates provided',
|
|
192
|
-
);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('should throw error for non-existent index', async () => {
|
|
196
|
-
const nonExistentIndex = 'non-existent-index';
|
|
197
|
-
await expect(
|
|
198
|
-
qdrant.updateVector({ indexName: nonExistentIndex, id: 'test-id', update: { vector: [1, 2, 3] } }),
|
|
199
|
-
).rejects.toThrow();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should throw error for invalid vector dimension', async () => {
|
|
203
|
-
const [id] = await qdrant.upsert({
|
|
204
|
-
indexName: testCollectionName,
|
|
205
|
-
vectors: [[1, 2, 3]],
|
|
206
|
-
metadata: [{ test: 'initial' }],
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
await expect(
|
|
210
|
-
qdrant.updateVector({ indexName: testCollectionName, id, update: { vector: [1, 2] } }), // Wrong dimension
|
|
211
|
-
).rejects.toThrow();
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe('Vector delete operations', () => {
|
|
216
|
-
const testVectors = [
|
|
217
|
-
[1, 2, 3],
|
|
218
|
-
[4, 5, 6],
|
|
219
|
-
[7, 8, 9],
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
beforeEach(async () => {
|
|
223
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension: 3 });
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
afterEach(async () => {
|
|
227
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('should delete the vector by id', async () => {
|
|
231
|
-
const ids = await qdrant.upsert({ indexName: testCollectionName, vectors: testVectors });
|
|
232
|
-
expect(ids).toHaveLength(3);
|
|
233
|
-
const idToBeDeleted = ids[0];
|
|
234
|
-
|
|
235
|
-
await qdrant.deleteVector({ indexName: testCollectionName, id: idToBeDeleted });
|
|
236
|
-
|
|
237
|
-
const results: QueryResult[] = await qdrant.query({
|
|
238
|
-
indexName: testCollectionName,
|
|
239
|
-
queryVector: [1.0, 0.0, 0.0],
|
|
240
|
-
topK: 2,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
expect(results).toHaveLength(2);
|
|
244
|
-
expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('Filter Queries', () => {
|
|
249
|
-
const filterTestVectors = Array(10)
|
|
250
|
-
.fill(null)
|
|
251
|
-
.map(() =>
|
|
252
|
-
Array(dimension)
|
|
253
|
-
.fill(null)
|
|
254
|
-
.map(() => Math.random()),
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
const filterTestMetadata = [
|
|
258
|
-
{
|
|
259
|
-
name: 'item1',
|
|
260
|
-
tags: ['electronics', 'premium'],
|
|
261
|
-
price: 1000,
|
|
262
|
-
inStock: true,
|
|
263
|
-
details: {
|
|
264
|
-
color: 'red',
|
|
265
|
-
sizes: ['S', 'M', 'L'],
|
|
266
|
-
weight: 2.5,
|
|
267
|
-
},
|
|
268
|
-
location: {
|
|
269
|
-
lat: 52.5,
|
|
270
|
-
lon: 13.4,
|
|
271
|
-
},
|
|
272
|
-
stock: {
|
|
273
|
-
quantity: 50,
|
|
274
|
-
locations: [
|
|
275
|
-
{ warehouse: 'A', count: 30 },
|
|
276
|
-
{ warehouse: 'B', count: 20 },
|
|
277
|
-
],
|
|
278
|
-
},
|
|
279
|
-
ratings: [4.5, 4.8, 4.2],
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'item2',
|
|
283
|
-
tags: ['electronics', 'basic'],
|
|
284
|
-
price: 500,
|
|
285
|
-
inStock: false,
|
|
286
|
-
details: {
|
|
287
|
-
color: 'blue',
|
|
288
|
-
sizes: ['M', 'L'],
|
|
289
|
-
weight: 1.8,
|
|
290
|
-
},
|
|
291
|
-
location: {
|
|
292
|
-
lat: 48.2,
|
|
293
|
-
lon: 16.3,
|
|
294
|
-
},
|
|
295
|
-
stock: {
|
|
296
|
-
quantity: 0,
|
|
297
|
-
locations: [],
|
|
298
|
-
},
|
|
299
|
-
ratings: [4.0, 3.8],
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
name: 'item3',
|
|
303
|
-
tags: ['books', 'bestseller'],
|
|
304
|
-
price: 25,
|
|
305
|
-
inStock: true,
|
|
306
|
-
details: {
|
|
307
|
-
color: 'green',
|
|
308
|
-
sizes: ['standard'],
|
|
309
|
-
weight: 0.5,
|
|
310
|
-
},
|
|
311
|
-
location: {
|
|
312
|
-
lat: 40.7,
|
|
313
|
-
lon: -74.0,
|
|
314
|
-
},
|
|
315
|
-
stock: {
|
|
316
|
-
quantity: 100,
|
|
317
|
-
locations: [
|
|
318
|
-
{ warehouse: 'A', count: 50 },
|
|
319
|
-
{ warehouse: 'C', count: 50 },
|
|
320
|
-
],
|
|
321
|
-
},
|
|
322
|
-
ratings: [4.9],
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
name: 'item4',
|
|
326
|
-
tags: [],
|
|
327
|
-
price: null,
|
|
328
|
-
inStock: null,
|
|
329
|
-
details: {
|
|
330
|
-
color: null,
|
|
331
|
-
sizes: [],
|
|
332
|
-
weight: null,
|
|
333
|
-
},
|
|
334
|
-
location: null,
|
|
335
|
-
stock: {
|
|
336
|
-
quantity: null,
|
|
337
|
-
locations: null,
|
|
338
|
-
},
|
|
339
|
-
ratings: null,
|
|
340
|
-
},
|
|
341
|
-
];
|
|
342
|
-
|
|
343
|
-
beforeAll(async () => {
|
|
344
|
-
qdrant = new QdrantVector({ url: 'http://localhost:6333/' });
|
|
345
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension });
|
|
346
|
-
await qdrant.upsert({ indexName: testCollectionName, vectors: filterTestVectors, metadata: filterTestMetadata });
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
afterAll(async () => {
|
|
350
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
351
|
-
}, 50000);
|
|
352
|
-
|
|
353
|
-
describe('Basic Operators', () => {
|
|
354
|
-
it('should filter by exact value match', async () => {
|
|
355
|
-
const filter: QdrantVectorFilter = { name: 'item1' };
|
|
356
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
357
|
-
expect(results).toHaveLength(1);
|
|
358
|
-
expect(results[0]?.metadata?.name).toBe('item1');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('should filter using comparison operators', async () => {
|
|
362
|
-
const filter: QdrantVectorFilter = { price: { $gt: 100, $lt: 600 } };
|
|
363
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
364
|
-
expect(results).toHaveLength(1);
|
|
365
|
-
expect(results[0]?.metadata?.price).toBe(500);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('should filter using array operators', async () => {
|
|
369
|
-
const filter: QdrantVectorFilter = { tags: { $in: ['premium', 'bestseller'] } };
|
|
370
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
371
|
-
expect(results).toHaveLength(2);
|
|
372
|
-
const tags = results.flatMap(r => r.metadata?.tags || []);
|
|
373
|
-
expect(tags).toContain('bestseller');
|
|
374
|
-
expect(tags).toContain('premium');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('should handle null values', async () => {
|
|
378
|
-
const filter: QdrantVectorFilter = { price: null };
|
|
379
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
380
|
-
expect(results).toHaveLength(1);
|
|
381
|
-
expect(results[0]?.metadata?.price).toBeNull();
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('should handle empty arrays', async () => {
|
|
385
|
-
const filter: QdrantVectorFilter = {
|
|
386
|
-
tags: [],
|
|
387
|
-
};
|
|
388
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
389
|
-
const resultsWithMetadata = results.filter(r => Object.keys(r?.metadata || {}).length > 0);
|
|
390
|
-
expect(resultsWithMetadata).toHaveLength(1);
|
|
391
|
-
expect(resultsWithMetadata[0]?.metadata?.tags).toHaveLength(0);
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
describe('Logical Operators', () => {
|
|
396
|
-
it('should combine conditions with $and', async () => {
|
|
397
|
-
const filter: QdrantVectorFilter = {
|
|
398
|
-
$and: [{ tags: { $in: ['electronics'] } }, { price: { $gt: 700 } }],
|
|
399
|
-
};
|
|
400
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
401
|
-
expect(results).toHaveLength(1);
|
|
402
|
-
expect(results[0]?.metadata?.price).toBeGreaterThan(700);
|
|
403
|
-
expect(results[0]?.metadata?.tags).toContain('electronics');
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('should combine conditions with $or', async () => {
|
|
407
|
-
const filter: QdrantVectorFilter = {
|
|
408
|
-
$or: [{ price: { $gt: 900 } }, { tags: { $in: ['bestseller'] } }],
|
|
409
|
-
};
|
|
410
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
411
|
-
expect(results).toHaveLength(2);
|
|
412
|
-
results.forEach(result => {
|
|
413
|
-
expect(result.metadata?.price > 900 || result.metadata?.tags?.includes('bestseller')).toBe(true);
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it('should handle $not operator', async () => {
|
|
418
|
-
const filter: QdrantVectorFilter = {
|
|
419
|
-
$not: { tags: { $in: ['electronics'] } },
|
|
420
|
-
};
|
|
421
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
422
|
-
const resultsWithMetadata = results.filter(r => Object.keys(r?.metadata || {}).length > 0);
|
|
423
|
-
expect(resultsWithMetadata).toHaveLength(2);
|
|
424
|
-
resultsWithMetadata.forEach(result => {
|
|
425
|
-
expect(result.metadata?.tags).not.toContain('electronics');
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('should handle nested logical operators', async () => {
|
|
430
|
-
const filter: QdrantVectorFilter = {
|
|
431
|
-
$and: [
|
|
432
|
-
{ 'details.weight': { $lt: 2.0 } },
|
|
433
|
-
{
|
|
434
|
-
$or: [{ tags: { $in: ['basic'] } }, { tags: { $in: ['bestseller'] } }],
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
};
|
|
438
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
439
|
-
expect(results).toHaveLength(2);
|
|
440
|
-
results.forEach(result => {
|
|
441
|
-
expect(result.metadata?.details?.weight).toBeLessThan(2.0);
|
|
442
|
-
expect(result.metadata?.tags?.includes('basic') || result.metadata?.tags?.includes('bestseller')).toBe(true);
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('should handle empty logical operators', async () => {
|
|
447
|
-
const filter: QdrantVectorFilter = { $and: [] };
|
|
448
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
449
|
-
expect(results.length).toBeGreaterThan(0);
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
describe('Custom Operators', () => {
|
|
454
|
-
it('should filter using $count operator', async () => {
|
|
455
|
-
const filter: QdrantVectorFilter = { 'stock.locations': { $count: { $gt: 1 } } };
|
|
456
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
457
|
-
expect(results).toHaveLength(2);
|
|
458
|
-
results.forEach(result => {
|
|
459
|
-
expect(result.metadata?.stock?.locations?.length).toBeGreaterThan(1);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it('should filter using $geo radius operator', async () => {
|
|
464
|
-
const filter: QdrantVectorFilter = {
|
|
465
|
-
location: {
|
|
466
|
-
$geo: {
|
|
467
|
-
type: 'radius',
|
|
468
|
-
center: { lat: 52.5, lon: 13.4 },
|
|
469
|
-
radius: 10000,
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
474
|
-
expect(results).toHaveLength(1);
|
|
475
|
-
expect(results[0]?.metadata?.location?.lat).toBe(52.5);
|
|
476
|
-
expect(results[0]?.metadata?.location?.lon).toBe(13.4);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
it('should filter using $geo box operator', async () => {
|
|
480
|
-
const filter: QdrantVectorFilter = {
|
|
481
|
-
location: {
|
|
482
|
-
$geo: {
|
|
483
|
-
type: 'box',
|
|
484
|
-
top_left: { lat: 53.0, lon: 13.0 },
|
|
485
|
-
bottom_right: { lat: 52.0, lon: 14.0 },
|
|
486
|
-
},
|
|
487
|
-
},
|
|
488
|
-
};
|
|
489
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
490
|
-
expect(results).toHaveLength(1);
|
|
491
|
-
expect(results[0]?.metadata?.location?.lat).toBe(52.5);
|
|
492
|
-
expect(results[0]?.metadata?.location?.lon).toBe(13.4);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('should filter using $geo polygon operator', async () => {
|
|
496
|
-
const filter: QdrantVectorFilter = {
|
|
497
|
-
location: {
|
|
498
|
-
$geo: {
|
|
499
|
-
type: 'polygon',
|
|
500
|
-
exterior: {
|
|
501
|
-
points: [
|
|
502
|
-
{ lat: 53.0, lon: 13.0 },
|
|
503
|
-
{ lat: 53.0, lon: 14.0 },
|
|
504
|
-
{ lat: 52.0, lon: 14.0 },
|
|
505
|
-
{ lat: 52.0, lon: 13.0 },
|
|
506
|
-
{ lat: 53.0, lon: 13.0 }, // Close the polygon by repeating first point
|
|
507
|
-
],
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
513
|
-
expect(results).toHaveLength(1);
|
|
514
|
-
expect(results[0]?.metadata?.location?.lat).toBe(52.5);
|
|
515
|
-
expect(results[0]?.metadata?.location?.lon).toBe(13.4);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it('should filter using $hasId operator', async () => {
|
|
519
|
-
// First get some IDs from a regular query
|
|
520
|
-
const allResults = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], topK: 2 });
|
|
521
|
-
const targetIds = allResults.map(r => r.id);
|
|
522
|
-
|
|
523
|
-
const filter: QdrantVectorFilter = { $hasId: targetIds };
|
|
524
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
525
|
-
expect(results).toHaveLength(2);
|
|
526
|
-
results.forEach(result => {
|
|
527
|
-
expect(targetIds).toContain(result.id);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it('should filter using $hasVector operator', async () => {
|
|
532
|
-
const filter: QdrantVectorFilter = { $hasVector: '' };
|
|
533
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
534
|
-
expect(results.length).toBeGreaterThan(0);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
it('should filter using $datetime operator', async () => {
|
|
538
|
-
// First upsert a record with a datetime
|
|
539
|
-
const now = new Date();
|
|
540
|
-
const vector = Array(dimension)
|
|
541
|
-
.fill(null)
|
|
542
|
-
.map(() => Math.random());
|
|
543
|
-
const metadata = {
|
|
544
|
-
created_at: now.toISOString(),
|
|
545
|
-
};
|
|
546
|
-
await qdrant.upsert({ indexName: testCollectionName, vectors: [vector], metadata: [metadata] });
|
|
547
|
-
|
|
548
|
-
const filter: QdrantVectorFilter = {
|
|
549
|
-
created_at: {
|
|
550
|
-
$datetime: {
|
|
551
|
-
range: {
|
|
552
|
-
gt: new Date(now.getTime() - 1000), // 1 second before
|
|
553
|
-
lt: new Date(now.getTime() + 1000), // 1 second after
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
};
|
|
558
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
559
|
-
expect(results.length).toBeGreaterThan(0);
|
|
560
|
-
results.forEach(result => {
|
|
561
|
-
expect(new Date(result.metadata?.created_at).getTime()).toBeGreaterThan(now.getTime() - 1000);
|
|
562
|
-
expect(new Date(result.metadata?.created_at).getTime()).toBeLessThan(now.getTime() + 1000);
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
describe('Special Cases', () => {
|
|
568
|
-
it('handles regex patterns in queries', async () => {
|
|
569
|
-
const results = await qdrant.query({
|
|
570
|
-
indexName: testCollectionName,
|
|
571
|
-
queryVector: [1, 0, 0],
|
|
572
|
-
filter: { name: { $regex: 'item' } },
|
|
573
|
-
});
|
|
574
|
-
expect(results.length).toBe(4);
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it('handles array operators in queries', async () => {
|
|
578
|
-
const results = await qdrant.query({
|
|
579
|
-
indexName: testCollectionName,
|
|
580
|
-
queryVector: [1, 0, 0],
|
|
581
|
-
filter: { tags: { $in: ['electronics', 'books'] } },
|
|
582
|
-
});
|
|
583
|
-
expect(results.length).toBe(3);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('handles nested array queries', async () => {
|
|
587
|
-
const results = await qdrant.query({
|
|
588
|
-
indexName: testCollectionName,
|
|
589
|
-
queryVector: [1, 0, 0],
|
|
590
|
-
filter: { 'stock.locations[]': { $nested: { warehouse: 'A', count: { $gt: 20 } } } },
|
|
591
|
-
});
|
|
592
|
-
expect(results.length).toBe(2);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('handles collection-wide operators', async () => {
|
|
596
|
-
// First get some actual IDs from our collection
|
|
597
|
-
const searchResults = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], topK: 2 });
|
|
598
|
-
const ids = searchResults.map(r => r.id);
|
|
599
|
-
|
|
600
|
-
const results = await qdrant.query({
|
|
601
|
-
indexName: testCollectionName,
|
|
602
|
-
queryVector: [1, 0, 0],
|
|
603
|
-
filter: { $hasId: ids, $hasVector: '' },
|
|
604
|
-
});
|
|
605
|
-
expect(results.length).toBe(2);
|
|
606
|
-
});
|
|
607
|
-
it('should handle nested paths', async () => {
|
|
608
|
-
const filter: QdrantVectorFilter = {
|
|
609
|
-
'details.color': 'red',
|
|
610
|
-
'stock.quantity': { $gt: 0 },
|
|
611
|
-
};
|
|
612
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
613
|
-
expect(results).toHaveLength(1);
|
|
614
|
-
expect(results[0]?.metadata?.details?.color).toBe('red');
|
|
615
|
-
expect(results[0]?.metadata?.stock?.quantity).toBeGreaterThan(0);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
it('should handle multiple conditions on same field', async () => {
|
|
619
|
-
const filter: QdrantVectorFilter = {
|
|
620
|
-
price: { $gt: 20, $lt: 30 },
|
|
621
|
-
};
|
|
622
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
623
|
-
expect(results).toHaveLength(1);
|
|
624
|
-
expect(results[0]?.metadata?.price).toBe(25);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('should handle complex combinations', async () => {
|
|
628
|
-
const filter: QdrantVectorFilter = {
|
|
629
|
-
$and: [
|
|
630
|
-
{ 'details.weight': { $lt: 3.0 } },
|
|
631
|
-
{
|
|
632
|
-
$or: [{ price: { $gt: 500 } }, { 'stock.quantity': { $gt: 50 } }],
|
|
633
|
-
},
|
|
634
|
-
{ $not: { tags: { $in: ['basic'] } } },
|
|
635
|
-
],
|
|
636
|
-
};
|
|
637
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
638
|
-
expect(results).toHaveLength(2);
|
|
639
|
-
results.forEach(result => {
|
|
640
|
-
expect(result.metadata?.details?.weight).toBeLessThan(3.0);
|
|
641
|
-
expect(result.metadata?.price > 500 || result.metadata?.stock?.quantity > 50).toBe(true);
|
|
642
|
-
expect(result.metadata?.tags).not.toContain('basic');
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
it('should handle array paths with nested objects', async () => {
|
|
647
|
-
const filter: QdrantVectorFilter = {
|
|
648
|
-
'stock.locations[].warehouse': { $in: ['A'] },
|
|
649
|
-
};
|
|
650
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
651
|
-
expect(results).toHaveLength(2);
|
|
652
|
-
results.forEach(result => {
|
|
653
|
-
expect(result.metadata?.stock?.locations?.some((loc: any) => loc.warehouse === 'A')).toBe(true);
|
|
654
|
-
});
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
it('should handle multiple nested paths with array notation', async () => {
|
|
658
|
-
const filter: QdrantVectorFilter = {
|
|
659
|
-
$and: [{ 'stock.locations[].warehouse': { $in: ['A'] } }, { 'stock.locations[].count': { $gt: 20 } }],
|
|
660
|
-
};
|
|
661
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
662
|
-
expect(results).toHaveLength(2);
|
|
663
|
-
results.forEach(result => {
|
|
664
|
-
const locations = result.metadata?.stock?.locations || [];
|
|
665
|
-
expect(locations.some((loc: any) => loc.warehouse === 'A' && loc.count > 20)).toBe(true);
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('should handle complex date range queries', async () => {
|
|
670
|
-
const now = new Date();
|
|
671
|
-
const vector = Array(dimension)
|
|
672
|
-
.fill(null)
|
|
673
|
-
.map(() => Math.random());
|
|
674
|
-
const metadata = {
|
|
675
|
-
timestamps: {
|
|
676
|
-
created: now.toISOString(),
|
|
677
|
-
updated: new Date(now.getTime() + 1000).toISOString(),
|
|
678
|
-
},
|
|
679
|
-
};
|
|
680
|
-
await qdrant.upsert({ indexName: testCollectionName, vectors: [vector], metadata: [metadata] });
|
|
681
|
-
|
|
682
|
-
const filter: QdrantVectorFilter = {
|
|
683
|
-
$and: [
|
|
684
|
-
{
|
|
685
|
-
'timestamps.created': {
|
|
686
|
-
$gt: new Date(now.getTime() - 1000).toISOString(),
|
|
687
|
-
},
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
'timestamps.updated': {
|
|
691
|
-
$lt: new Date(now.getTime() + 2000).toISOString(),
|
|
692
|
-
},
|
|
693
|
-
},
|
|
694
|
-
],
|
|
695
|
-
};
|
|
696
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
697
|
-
expect(results.length).toBeGreaterThan(0);
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
it('should handle complex combinations with custom operators', async () => {
|
|
701
|
-
const filter: QdrantVectorFilter = {
|
|
702
|
-
$and: [
|
|
703
|
-
{ 'stock.locations': { $count: { $gt: 0 } } },
|
|
704
|
-
{
|
|
705
|
-
$or: [
|
|
706
|
-
{
|
|
707
|
-
location: {
|
|
708
|
-
$geo: {
|
|
709
|
-
type: 'radius',
|
|
710
|
-
center: { lat: 52.5, lon: 13.4 },
|
|
711
|
-
radius: 10000,
|
|
712
|
-
},
|
|
713
|
-
},
|
|
714
|
-
},
|
|
715
|
-
{ tags: { $in: ['bestseller'] } },
|
|
716
|
-
],
|
|
717
|
-
},
|
|
718
|
-
],
|
|
719
|
-
};
|
|
720
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
721
|
-
expect(results.length).toBeGreaterThan(0);
|
|
722
|
-
results.forEach(result => {
|
|
723
|
-
const metadata = result.metadata || {};
|
|
724
|
-
expect(metadata.stock?.locations?.length).toBeGreaterThan(0);
|
|
725
|
-
const location = metadata.location;
|
|
726
|
-
const isNearLocation = location?.lat === 52.5 && location?.lon === 13.4;
|
|
727
|
-
const isBestseller = metadata.tags?.includes('bestseller');
|
|
728
|
-
expect(isNearLocation || isBestseller).toBe(true);
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
describe('Performance Cases', () => {
|
|
734
|
-
it('should handle deep nesting efficiently', async () => {
|
|
735
|
-
const start = Date.now();
|
|
736
|
-
const filter: QdrantVectorFilter = {
|
|
737
|
-
$and: Array(5)
|
|
738
|
-
.fill(null)
|
|
739
|
-
.map(() => ({
|
|
740
|
-
$or: [{ 'details.weight': { $lt: 2.0 } }, { 'stock.quantity': { $gt: 0 } }],
|
|
741
|
-
})),
|
|
742
|
-
};
|
|
743
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter });
|
|
744
|
-
const duration = Date.now() - start;
|
|
745
|
-
expect(duration).toBeLessThan(1000); // Should complete within 1 second
|
|
746
|
-
expect(results.length).toBeGreaterThan(0);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
it('should handle multiple concurrent filtered queries', async () => {
|
|
750
|
-
const filters: QdrantVectorFilter[] = [
|
|
751
|
-
{ price: { $gt: 500 } },
|
|
752
|
-
{ tags: { $in: ['electronics'] } },
|
|
753
|
-
{ 'stock.quantity': { $gt: 0 } },
|
|
754
|
-
];
|
|
755
|
-
const start = Date.now();
|
|
756
|
-
const results = await Promise.all(
|
|
757
|
-
filters.map(filter => qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter })),
|
|
758
|
-
);
|
|
759
|
-
const duration = Date.now() - start;
|
|
760
|
-
expect(duration).toBeLessThan(3000); // Should complete within 3 seconds
|
|
761
|
-
results.forEach(result => {
|
|
762
|
-
expect(result.length).toBeGreaterThan(0);
|
|
763
|
-
});
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
describe('Error Handling', () => {
|
|
768
|
-
const testIndexName = 'test_index_error';
|
|
769
|
-
beforeAll(async () => {
|
|
770
|
-
await qdrant.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
afterAll(async () => {
|
|
774
|
-
await qdrant.deleteIndex({ indexName: testIndexName });
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
it('should handle non-existent index query gracefully', async () => {
|
|
778
|
-
const nonExistentIndex = 'non-existent-index';
|
|
779
|
-
await expect(qdrant.query({ indexName: nonExistentIndex, queryVector: [1, 0, 0] })).rejects.toThrow();
|
|
780
|
-
}, 50000);
|
|
781
|
-
|
|
782
|
-
it('should handle incorrect dimension vectors', async () => {
|
|
783
|
-
const wrongDimVector = [[1, 0]]; // 2D vector for 3D index
|
|
784
|
-
await expect(qdrant.upsert({ indexName: testCollectionName, vectors: wrongDimVector })).rejects.toThrow();
|
|
785
|
-
}, 50000);
|
|
786
|
-
|
|
787
|
-
it('should handle mismatched metadata and vectors length', async () => {
|
|
788
|
-
const vectors = [[1, 2, 3]];
|
|
789
|
-
const metadata = [{}, {}];
|
|
790
|
-
await expect(qdrant.upsert({ indexName: testCollectionName, vectors, metadata })).rejects.toThrow();
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
it('should handle duplicate index creation gracefully', async () => {
|
|
794
|
-
const duplicateIndexName = `duplicate_test`;
|
|
795
|
-
const dimension = 768;
|
|
796
|
-
const infoSpy = vi.spyOn(qdrant['logger'], 'info');
|
|
797
|
-
const warnSpy = vi.spyOn(qdrant['logger'], 'warn');
|
|
798
|
-
try {
|
|
799
|
-
// Create index first time
|
|
800
|
-
await qdrant.createIndex({
|
|
801
|
-
indexName: duplicateIndexName,
|
|
802
|
-
dimension,
|
|
803
|
-
metric: 'cosine',
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
// Try to create with same dimensions - should not throw
|
|
807
|
-
await expect(
|
|
808
|
-
qdrant.createIndex({
|
|
809
|
-
indexName: duplicateIndexName,
|
|
810
|
-
dimension,
|
|
811
|
-
metric: 'cosine',
|
|
812
|
-
}),
|
|
813
|
-
).resolves.not.toThrow();
|
|
814
|
-
|
|
815
|
-
expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('already exists with'));
|
|
816
|
-
|
|
817
|
-
// Try to create with same dimensions and different metric - should not throw
|
|
818
|
-
await expect(
|
|
819
|
-
qdrant.createIndex({
|
|
820
|
-
indexName: duplicateIndexName,
|
|
821
|
-
dimension,
|
|
822
|
-
metric: 'euclidean',
|
|
823
|
-
}),
|
|
824
|
-
).resolves.not.toThrow();
|
|
825
|
-
|
|
826
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Attempted to create index with metric'));
|
|
827
|
-
|
|
828
|
-
// Try to create with different dimensions - should throw
|
|
829
|
-
await expect(
|
|
830
|
-
qdrant.createIndex({
|
|
831
|
-
indexName: duplicateIndexName,
|
|
832
|
-
dimension: dimension + 1,
|
|
833
|
-
metric: 'cosine',
|
|
834
|
-
}),
|
|
835
|
-
).rejects.toThrow(
|
|
836
|
-
`Index "${duplicateIndexName}" already exists with ${dimension} dimensions, but ${dimension + 1} dimensions were requested`,
|
|
837
|
-
);
|
|
838
|
-
} finally {
|
|
839
|
-
infoSpy.mockRestore();
|
|
840
|
-
warnSpy.mockRestore();
|
|
841
|
-
// Cleanup
|
|
842
|
-
await qdrant.deleteIndex({ indexName: duplicateIndexName });
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
describe('Empty/Undefined Filters', () => {
|
|
848
|
-
const filterTestVectors = Array(10)
|
|
849
|
-
.fill(null)
|
|
850
|
-
.map(() =>
|
|
851
|
-
Array(dimension)
|
|
852
|
-
.fill(null)
|
|
853
|
-
.map(() => Math.random()),
|
|
854
|
-
);
|
|
855
|
-
|
|
856
|
-
const filterTestMetadata = [
|
|
857
|
-
{
|
|
858
|
-
name: 'item1',
|
|
859
|
-
tags: ['electronics', 'premium'],
|
|
860
|
-
price: 1000,
|
|
861
|
-
inStock: true,
|
|
862
|
-
details: {
|
|
863
|
-
color: 'red',
|
|
864
|
-
sizes: ['S', 'M', 'L'],
|
|
865
|
-
weight: 2.5,
|
|
866
|
-
},
|
|
867
|
-
location: {
|
|
868
|
-
lat: 52.5,
|
|
869
|
-
lon: 13.4,
|
|
870
|
-
},
|
|
871
|
-
stock: {
|
|
872
|
-
quantity: 50,
|
|
873
|
-
locations: [
|
|
874
|
-
{ warehouse: 'A', count: 30 },
|
|
875
|
-
{ warehouse: 'B', count: 20 },
|
|
876
|
-
],
|
|
877
|
-
},
|
|
878
|
-
ratings: [4.5, 4.8, 4.2],
|
|
879
|
-
},
|
|
880
|
-
];
|
|
881
|
-
|
|
882
|
-
beforeAll(async () => {
|
|
883
|
-
qdrant = new QdrantVector({ url: 'http://localhost:6333/' });
|
|
884
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension });
|
|
885
|
-
await qdrant.upsert({ indexName: testCollectionName, vectors: filterTestVectors, metadata: filterTestMetadata });
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
afterAll(async () => {
|
|
889
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
890
|
-
}, 50000);
|
|
891
|
-
it('should handle undefined filter', async () => {
|
|
892
|
-
const results1 = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter: undefined });
|
|
893
|
-
const results2 = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0] });
|
|
894
|
-
expect(results1).toEqual(results2);
|
|
895
|
-
expect(results1.length).toBeGreaterThan(0);
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
it('should handle empty object filter', async () => {
|
|
899
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter: {} });
|
|
900
|
-
const results2 = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0] });
|
|
901
|
-
expect(results).toEqual(results2);
|
|
902
|
-
expect(results.length).toBeGreaterThan(0);
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
it('should handle null filter', async () => {
|
|
906
|
-
const results = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0], filter: null });
|
|
907
|
-
const results2 = await qdrant.query({ indexName: testCollectionName, queryVector: [1, 0, 0] });
|
|
908
|
-
expect(results).toEqual(results2);
|
|
909
|
-
expect(results.length).toBeGreaterThan(0);
|
|
910
|
-
});
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
describe('Performance Tests', () => {
|
|
914
|
-
beforeAll(async () => {
|
|
915
|
-
qdrant = new QdrantVector({ url: 'http://localhost:6333/' });
|
|
916
|
-
await qdrant.createIndex({ indexName: testCollectionName, dimension });
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
afterAll(async () => {
|
|
920
|
-
await qdrant.deleteIndex({ indexName: testCollectionName });
|
|
921
|
-
}, 50000);
|
|
922
|
-
|
|
923
|
-
it('should handle batch upsert of 1000 vectors', async () => {
|
|
924
|
-
const batchSize = 1000;
|
|
925
|
-
const vectors = Array(batchSize)
|
|
926
|
-
.fill(null)
|
|
927
|
-
.map(() =>
|
|
928
|
-
Array(dimension)
|
|
929
|
-
.fill(null)
|
|
930
|
-
.map(() => Math.random()),
|
|
931
|
-
);
|
|
932
|
-
const metadata = vectors.map((_, i) => ({ id: i }));
|
|
933
|
-
|
|
934
|
-
const start = Date.now();
|
|
935
|
-
const ids = await qdrant.upsert({ indexName: testCollectionName, vectors, metadata });
|
|
936
|
-
const duration = Date.now() - start;
|
|
937
|
-
|
|
938
|
-
expect(ids).toHaveLength(batchSize);
|
|
939
|
-
console.log(`Batch upsert of ${batchSize} vectors took ${duration}ms`);
|
|
940
|
-
}, 300000);
|
|
941
|
-
|
|
942
|
-
it('should perform multiple concurrent queries', async () => {
|
|
943
|
-
const queryVector = [1, 0, 0];
|
|
944
|
-
const numQueries = 10;
|
|
945
|
-
|
|
946
|
-
const start = Date.now();
|
|
947
|
-
const promises = Array(numQueries)
|
|
948
|
-
.fill(null)
|
|
949
|
-
.map(() => qdrant.query({ indexName: testCollectionName, queryVector }));
|
|
950
|
-
|
|
951
|
-
const results = await Promise.all(promises);
|
|
952
|
-
const duration = Date.now() - start;
|
|
953
|
-
|
|
954
|
-
expect(results).toHaveLength(numQueries);
|
|
955
|
-
console.log(`${numQueries} concurrent queries took ${duration}ms`);
|
|
956
|
-
}, 50000);
|
|
957
|
-
});
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
// Metadata filtering tests for Memory system
|
|
961
|
-
describe('Qdrant Metadata Filtering', () => {
|
|
962
|
-
const qdrantVector = new QdrantVector({ url: 'http://localhost:6333/' });
|
|
963
|
-
|
|
964
|
-
createVectorTestSuite({
|
|
965
|
-
vector: qdrantVector,
|
|
966
|
-
createIndex: async (indexName: string) => {
|
|
967
|
-
await qdrantVector.createIndex({ indexName, dimension: 4 });
|
|
968
|
-
},
|
|
969
|
-
deleteIndex: async (indexName: string) => {
|
|
970
|
-
await qdrantVector.deleteIndex({ indexName });
|
|
971
|
-
},
|
|
972
|
-
waitForIndexing: async () => {
|
|
973
|
-
// Qdrant indexes immediately
|
|
974
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
975
|
-
},
|
|
976
|
-
});
|
|
977
|
-
});
|