@mastra/pg 0.14.5 → 0.14.6-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 +18 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/operations/index.d.ts.map +1 -1
- package/package.json +18 -5
- package/.turbo/turbo-build.log +0 -4
- package/docker-compose.perf.yaml +0 -21
- package/docker-compose.yaml +0 -14
- package/eslint.config.js +0 -6
- package/src/index.ts +0 -3
- package/src/storage/domains/legacy-evals/index.ts +0 -151
- package/src/storage/domains/memory/index.ts +0 -1028
- package/src/storage/domains/operations/index.ts +0 -368
- package/src/storage/domains/scores/index.ts +0 -297
- package/src/storage/domains/traces/index.ts +0 -160
- package/src/storage/domains/utils.ts +0 -12
- package/src/storage/domains/workflows/index.ts +0 -291
- package/src/storage/index.test.ts +0 -11
- package/src/storage/index.ts +0 -514
- package/src/storage/test-utils.ts +0 -377
- package/src/vector/filter.test.ts +0 -967
- package/src/vector/filter.ts +0 -136
- package/src/vector/index.test.ts +0 -2729
- package/src/vector/index.ts +0 -926
- package/src/vector/performance.helpers.ts +0 -286
- package/src/vector/prompt.ts +0 -101
- package/src/vector/sql-builder.ts +0 -358
- package/src/vector/types.ts +0 -16
- package/src/vector/vector.performance.test.ts +0 -367
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -17
- package/vitest.config.ts +0 -12
- package/vitest.perf.config.ts +0 -8
package/src/vector/index.test.ts
DELETED
|
@@ -1,2729 +0,0 @@
|
|
|
1
|
-
import { createVectorTestSuite } from '@internal/storage-test-utils';
|
|
2
|
-
import type { QueryResult } from '@mastra/core/vector';
|
|
3
|
-
import * as pg from 'pg';
|
|
4
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
-
|
|
6
|
-
import { PgVector } from '.';
|
|
7
|
-
|
|
8
|
-
describe('PgVector', () => {
|
|
9
|
-
let vectorDB: PgVector;
|
|
10
|
-
const testIndexName = 'test_vectors';
|
|
11
|
-
const testIndexName2 = 'test_vectors1';
|
|
12
|
-
const connectionString = process.env.DB_URL || 'postgresql://postgres:postgres@localhost:5434/mastra';
|
|
13
|
-
|
|
14
|
-
beforeAll(async () => {
|
|
15
|
-
// Initialize PgVector
|
|
16
|
-
vectorDB = new PgVector({ connectionString });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('Public Fields Access', () => {
|
|
20
|
-
let testDB: PgVector;
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
testDB = new PgVector({ connectionString });
|
|
23
|
-
});
|
|
24
|
-
afterAll(async () => {
|
|
25
|
-
try {
|
|
26
|
-
await testDB.disconnect();
|
|
27
|
-
} catch {}
|
|
28
|
-
});
|
|
29
|
-
it('should expose pool field as public', () => {
|
|
30
|
-
expect(testDB.pool).toBeDefined();
|
|
31
|
-
expect(typeof testDB.pool).toBe('object');
|
|
32
|
-
expect(testDB.pool.connect).toBeDefined();
|
|
33
|
-
expect(typeof testDB.pool.connect).toBe('function');
|
|
34
|
-
expect(testDB.pool).toBeInstanceOf(pg.Pool);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('pool provides a working client connection', async () => {
|
|
38
|
-
const pool = testDB.pool;
|
|
39
|
-
const client = await pool.connect();
|
|
40
|
-
expect(typeof client.query).toBe('function');
|
|
41
|
-
expect(typeof client.release).toBe('function');
|
|
42
|
-
client.release();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should allow direct database connections via public pool field', async () => {
|
|
46
|
-
const client = await testDB.pool.connect();
|
|
47
|
-
try {
|
|
48
|
-
const result = await client.query('SELECT 1 as test');
|
|
49
|
-
expect(result.rows[0].test).toBe(1);
|
|
50
|
-
} finally {
|
|
51
|
-
client.release();
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should provide access to pool configuration via public pool field', () => {
|
|
56
|
-
expect(testDB.pool.options).toBeDefined();
|
|
57
|
-
expect(testDB.pool.options.connectionString).toBe(connectionString);
|
|
58
|
-
expect(testDB.pool.options.max).toBeDefined();
|
|
59
|
-
expect(testDB.pool.options.idleTimeoutMillis).toBeDefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should allow pool monitoring via public pool field', () => {
|
|
63
|
-
expect(testDB.pool.totalCount).toBeDefined();
|
|
64
|
-
expect(testDB.pool.idleCount).toBeDefined();
|
|
65
|
-
expect(testDB.pool.waitingCount).toBeDefined();
|
|
66
|
-
expect(typeof testDB.pool.totalCount).toBe('number');
|
|
67
|
-
expect(typeof testDB.pool.idleCount).toBe('number');
|
|
68
|
-
expect(typeof testDB.pool.waitingCount).toBe('number');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should allow executing raw SQL via public pool field', async () => {
|
|
72
|
-
const client = await testDB.pool.connect();
|
|
73
|
-
try {
|
|
74
|
-
// Test a simple vector-related query
|
|
75
|
-
const result = await client.query('SELECT version()');
|
|
76
|
-
expect(result.rows[0].version).toBeDefined();
|
|
77
|
-
expect(typeof result.rows[0].version).toBe('string');
|
|
78
|
-
} finally {
|
|
79
|
-
client.release();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should maintain proper connection lifecycle via public pool field', async () => {
|
|
84
|
-
const initialIdleCount = testDB.pool.idleCount;
|
|
85
|
-
const initialTotalCount = testDB.pool.totalCount;
|
|
86
|
-
|
|
87
|
-
const client = await testDB.pool.connect();
|
|
88
|
-
|
|
89
|
-
// After connecting, total count should be >= initial, idle count should be less
|
|
90
|
-
expect(testDB.pool.totalCount).toBeGreaterThanOrEqual(initialTotalCount);
|
|
91
|
-
expect(testDB.pool.idleCount).toBeLessThanOrEqual(initialIdleCount);
|
|
92
|
-
|
|
93
|
-
client.release();
|
|
94
|
-
|
|
95
|
-
// After releasing, idle count should return to at least initial value
|
|
96
|
-
expect(testDB.pool.idleCount).toBeGreaterThanOrEqual(initialIdleCount);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('allows performing a transaction', async () => {
|
|
100
|
-
const client = await testDB.pool.connect();
|
|
101
|
-
try {
|
|
102
|
-
await client.query('BEGIN');
|
|
103
|
-
const { rows } = await client.query('SELECT 2 as value');
|
|
104
|
-
expect(rows[0].value).toBe(2);
|
|
105
|
-
await client.query('COMMIT');
|
|
106
|
-
} finally {
|
|
107
|
-
client.release();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
it('releases client on query error', async () => {
|
|
111
|
-
const client = await testDB.pool.connect();
|
|
112
|
-
try {
|
|
113
|
-
await expect(client.query('SELECT * FROM not_a_real_table')).rejects.toThrow();
|
|
114
|
-
} finally {
|
|
115
|
-
client.release();
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('can use getPool() to query metadata for filter options (user scenario)', async () => {
|
|
120
|
-
// Insert vectors with metadata
|
|
121
|
-
await testDB.createIndex({ indexName: 'filter_test', dimension: 2 });
|
|
122
|
-
await testDB.upsert({
|
|
123
|
-
indexName: 'filter_test',
|
|
124
|
-
vectors: [
|
|
125
|
-
[0.1, 0.2],
|
|
126
|
-
[0.3, 0.4],
|
|
127
|
-
[0.5, 0.6],
|
|
128
|
-
],
|
|
129
|
-
metadata: [
|
|
130
|
-
{ category: 'A', color: 'red' },
|
|
131
|
-
{ category: 'B', color: 'blue' },
|
|
132
|
-
{ category: 'A', color: 'green' },
|
|
133
|
-
],
|
|
134
|
-
ids: ['id1', 'id2', 'id3'],
|
|
135
|
-
});
|
|
136
|
-
// Use the pool to query unique categories
|
|
137
|
-
const { tableName } = testDB['getTableName']('filter_test');
|
|
138
|
-
const res = await testDB.pool.query(
|
|
139
|
-
`SELECT DISTINCT metadata->>'category' AS category FROM ${tableName} ORDER BY category`,
|
|
140
|
-
);
|
|
141
|
-
expect(res.rows.map(r => r.category).sort()).toEqual(['A', 'B']);
|
|
142
|
-
// Clean up
|
|
143
|
-
await testDB.deleteIndex({ indexName: 'filter_test' });
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should throw error when pool is used after disconnect', async () => {
|
|
147
|
-
await testDB.disconnect();
|
|
148
|
-
expect(testDB.pool.connect()).rejects.toThrow();
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
afterAll(async () => {
|
|
153
|
-
// Clean up test tables
|
|
154
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
155
|
-
await vectorDB.disconnect();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// --- Validation tests ---
|
|
159
|
-
describe('Validation', () => {
|
|
160
|
-
it('throws if connectionString is empty', () => {
|
|
161
|
-
expect(() => new PgVector({ connectionString: '' })).toThrow(
|
|
162
|
-
/connectionString must be provided and cannot be empty/,
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
it('does not throw on non-empty connection string', () => {
|
|
166
|
-
expect(() => new PgVector({ connectionString })).not.toThrow();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Index Management Tests
|
|
171
|
-
describe('Index Management', () => {
|
|
172
|
-
describe('createIndex', () => {
|
|
173
|
-
afterAll(async () => {
|
|
174
|
-
await vectorDB.deleteIndex({ indexName: testIndexName2 });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should create a new vector table with specified dimensions', async () => {
|
|
178
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
179
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName });
|
|
180
|
-
expect(stats?.dimension).toBe(3);
|
|
181
|
-
expect(stats?.count).toBe(0);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should create index with specified metric', async () => {
|
|
185
|
-
await vectorDB.createIndex({ indexName: testIndexName2, dimension: 3, metric: 'euclidean' });
|
|
186
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName2 });
|
|
187
|
-
expect(stats.metric).toBe('euclidean');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should throw error if dimension is invalid', async () => {
|
|
191
|
-
await expect(vectorDB.createIndex({ indexName: 'testIndexNameFail', dimension: 0 })).rejects.toThrow();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should create index with flat type', async () => {
|
|
195
|
-
await vectorDB.createIndex({
|
|
196
|
-
indexName: testIndexName2,
|
|
197
|
-
dimension: 3,
|
|
198
|
-
metric: 'cosine',
|
|
199
|
-
indexConfig: { type: 'flat' },
|
|
200
|
-
});
|
|
201
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName2 });
|
|
202
|
-
expect(stats.type).toBe('flat');
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should create index with hnsw type', async () => {
|
|
206
|
-
await vectorDB.createIndex({
|
|
207
|
-
indexName: testIndexName2,
|
|
208
|
-
dimension: 3,
|
|
209
|
-
metric: 'cosine',
|
|
210
|
-
indexConfig: { type: 'hnsw', hnsw: { m: 16, efConstruction: 64 } }, // Any reasonable values work
|
|
211
|
-
});
|
|
212
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName2 });
|
|
213
|
-
expect(stats.type).toBe('hnsw');
|
|
214
|
-
expect(stats.config.m).toBe(16);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('should create index with ivfflat type and lists', async () => {
|
|
218
|
-
await vectorDB.createIndex({
|
|
219
|
-
indexName: testIndexName2,
|
|
220
|
-
dimension: 3,
|
|
221
|
-
metric: 'cosine',
|
|
222
|
-
indexConfig: { type: 'ivfflat', ivf: { lists: 100 } },
|
|
223
|
-
});
|
|
224
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName2 });
|
|
225
|
-
expect(stats.type).toBe('ivfflat');
|
|
226
|
-
expect(stats.config.lists).toBe(100);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe('listIndexes', () => {
|
|
231
|
-
const indexName = 'test_query_3';
|
|
232
|
-
beforeAll(async () => {
|
|
233
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
afterAll(async () => {
|
|
237
|
-
await vectorDB.deleteIndex({ indexName });
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should list all vector tables', async () => {
|
|
241
|
-
const indexes = await vectorDB.listIndexes();
|
|
242
|
-
expect(indexes).toContain(indexName);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('should not return created index in list if it is deleted', async () => {
|
|
246
|
-
await vectorDB.deleteIndex({ indexName });
|
|
247
|
-
const indexes = await vectorDB.listIndexes();
|
|
248
|
-
expect(indexes).not.toContain(indexName);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('describeIndex', () => {
|
|
253
|
-
const indexName = 'test_query_4';
|
|
254
|
-
beforeAll(async () => {
|
|
255
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
afterAll(async () => {
|
|
259
|
-
await vectorDB.deleteIndex({ indexName });
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('should return correct index stats', async () => {
|
|
263
|
-
await vectorDB.createIndex({ indexName, dimension: 3, metric: 'cosine' });
|
|
264
|
-
const vectors = [
|
|
265
|
-
[1, 2, 3],
|
|
266
|
-
[4, 5, 6],
|
|
267
|
-
];
|
|
268
|
-
await vectorDB.upsert({ indexName, vectors });
|
|
269
|
-
|
|
270
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
271
|
-
expect(stats).toEqual({
|
|
272
|
-
type: 'ivfflat',
|
|
273
|
-
config: {
|
|
274
|
-
lists: 100,
|
|
275
|
-
},
|
|
276
|
-
dimension: 3,
|
|
277
|
-
count: 2,
|
|
278
|
-
metric: 'cosine',
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should throw error for non-existent index', async () => {
|
|
283
|
-
await expect(vectorDB.describeIndex({ indexName: 'non_existent' })).rejects.toThrow();
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe('buildIndex', () => {
|
|
288
|
-
const indexName = 'test_build_index';
|
|
289
|
-
beforeAll(async () => {
|
|
290
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
afterAll(async () => {
|
|
294
|
-
await vectorDB.deleteIndex({ indexName });
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('should build index with specified metric and config', async () => {
|
|
298
|
-
await vectorDB.buildIndex({
|
|
299
|
-
indexName,
|
|
300
|
-
metric: 'cosine',
|
|
301
|
-
indexConfig: { type: 'hnsw', hnsw: { m: 16, efConstruction: 64 } },
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
305
|
-
expect(stats.type).toBe('hnsw');
|
|
306
|
-
expect(stats.metric).toBe('cosine');
|
|
307
|
-
expect(stats.config.m).toBe(16);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should build ivfflat index with specified lists', async () => {
|
|
311
|
-
await vectorDB.buildIndex({
|
|
312
|
-
indexName,
|
|
313
|
-
metric: 'euclidean',
|
|
314
|
-
indexConfig: { type: 'ivfflat', ivf: { lists: 100 } },
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
318
|
-
expect(stats.type).toBe('ivfflat');
|
|
319
|
-
expect(stats.metric).toBe('euclidean');
|
|
320
|
-
expect(stats.config.lists).toBe(100);
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
// Vector Operations Tests
|
|
326
|
-
describe('Vector Operations', () => {
|
|
327
|
-
describe('upsert', () => {
|
|
328
|
-
beforeEach(async () => {
|
|
329
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
afterEach(async () => {
|
|
333
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('should insert new vectors', async () => {
|
|
337
|
-
const vectors = [
|
|
338
|
-
[1, 2, 3],
|
|
339
|
-
[4, 5, 6],
|
|
340
|
-
];
|
|
341
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors });
|
|
342
|
-
|
|
343
|
-
expect(ids).toHaveLength(2);
|
|
344
|
-
const stats = await vectorDB.describeIndex({ indexName: testIndexName });
|
|
345
|
-
expect(stats.count).toBe(2);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('should update existing vectors', async () => {
|
|
349
|
-
const vectors = [[1, 2, 3]];
|
|
350
|
-
const metadata = [{ test: 'initial' }];
|
|
351
|
-
const [id] = await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
|
|
352
|
-
|
|
353
|
-
const updatedVectors = [[4, 5, 6]];
|
|
354
|
-
const updatedMetadata = [{ test: 'updated' }];
|
|
355
|
-
await vectorDB.upsert({
|
|
356
|
-
indexName: testIndexName,
|
|
357
|
-
vectors: updatedVectors,
|
|
358
|
-
metadata: updatedMetadata,
|
|
359
|
-
ids: [id!],
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector: [4, 5, 6], topK: 1 });
|
|
363
|
-
expect(results[0]?.id).toBe(id);
|
|
364
|
-
expect(results[0]?.metadata).toEqual({ test: 'updated' });
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it('should handle metadata correctly', async () => {
|
|
368
|
-
const vectors = [[1, 2, 3]];
|
|
369
|
-
const metadata = [{ test: 'value', num: 123 }];
|
|
370
|
-
|
|
371
|
-
await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
|
|
372
|
-
const results = await vectorDB.query({ indexName: testIndexName, queryVector: [1, 2, 3], topK: 1 });
|
|
373
|
-
|
|
374
|
-
expect(results[0]?.metadata).toEqual(metadata[0]);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('should throw error if vector dimensions dont match', async () => {
|
|
378
|
-
const vectors = [[1, 2, 3, 4]]; // 4D vector for 3D index
|
|
379
|
-
await expect(vectorDB.upsert({ indexName: testIndexName, vectors })).rejects.toThrow(
|
|
380
|
-
`Vector dimension mismatch: Index "${testIndexName}" expects 3 dimensions but got 4 dimensions. ` +
|
|
381
|
-
`Either use a matching embedding model or delete and recreate the index with the new dimension.`,
|
|
382
|
-
);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
describe('updates', () => {
|
|
387
|
-
const testVectors = [
|
|
388
|
-
[1, 2, 3],
|
|
389
|
-
[4, 5, 6],
|
|
390
|
-
[7, 8, 9],
|
|
391
|
-
];
|
|
392
|
-
|
|
393
|
-
beforeEach(async () => {
|
|
394
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
afterEach(async () => {
|
|
398
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('should update the vector by id', async () => {
|
|
402
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
403
|
-
expect(ids).toHaveLength(3);
|
|
404
|
-
|
|
405
|
-
const idToBeUpdated = ids[0];
|
|
406
|
-
const newVector = [1, 2, 3];
|
|
407
|
-
const newMetaData = {
|
|
408
|
-
test: 'updates',
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const update = {
|
|
412
|
-
vector: newVector,
|
|
413
|
-
metadata: newMetaData,
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
417
|
-
|
|
418
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
419
|
-
indexName: testIndexName,
|
|
420
|
-
queryVector: newVector,
|
|
421
|
-
topK: 2,
|
|
422
|
-
includeVector: true,
|
|
423
|
-
});
|
|
424
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
425
|
-
expect(results[0]?.vector).toEqual(newVector);
|
|
426
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('should only update the metadata by id', async () => {
|
|
430
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
431
|
-
expect(ids).toHaveLength(3);
|
|
432
|
-
|
|
433
|
-
const idToBeUpdated = ids[0];
|
|
434
|
-
const newMetaData = {
|
|
435
|
-
test: 'updates',
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const update = {
|
|
439
|
-
metadata: newMetaData,
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
443
|
-
|
|
444
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
445
|
-
indexName: testIndexName,
|
|
446
|
-
queryVector: testVectors[0],
|
|
447
|
-
topK: 2,
|
|
448
|
-
includeVector: true,
|
|
449
|
-
});
|
|
450
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
451
|
-
expect(results[0]?.vector).toEqual(testVectors[0]);
|
|
452
|
-
expect(results[0]?.metadata).toEqual(newMetaData);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
it('should only update vector embeddings by id', async () => {
|
|
456
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
457
|
-
expect(ids).toHaveLength(3);
|
|
458
|
-
|
|
459
|
-
const idToBeUpdated = ids[0];
|
|
460
|
-
const newVector = [4, 4, 4];
|
|
461
|
-
|
|
462
|
-
const update = {
|
|
463
|
-
vector: newVector,
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
await vectorDB.updateVector({ indexName: testIndexName, id: idToBeUpdated, update });
|
|
467
|
-
|
|
468
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
469
|
-
indexName: testIndexName,
|
|
470
|
-
queryVector: newVector,
|
|
471
|
-
topK: 2,
|
|
472
|
-
includeVector: true,
|
|
473
|
-
});
|
|
474
|
-
expect(results[0]?.id).toBe(idToBeUpdated);
|
|
475
|
-
expect(results[0]?.vector).toEqual(newVector);
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it('should throw exception when no updates are given', async () => {
|
|
479
|
-
await expect(vectorDB.updateVector({ indexName: testIndexName, id: 'id', update: {} })).rejects.toThrow(
|
|
480
|
-
'No updates provided',
|
|
481
|
-
);
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
describe('deletes', () => {
|
|
486
|
-
const testVectors = [
|
|
487
|
-
[1, 2, 3],
|
|
488
|
-
[4, 5, 6],
|
|
489
|
-
[7, 8, 9],
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
beforeEach(async () => {
|
|
493
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
afterEach(async () => {
|
|
497
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it('should delete the vector by id', async () => {
|
|
501
|
-
const ids = await vectorDB.upsert({ indexName: testIndexName, vectors: testVectors });
|
|
502
|
-
expect(ids).toHaveLength(3);
|
|
503
|
-
const idToBeDeleted = ids[0];
|
|
504
|
-
|
|
505
|
-
await vectorDB.deleteVector({ indexName: testIndexName, id: idToBeDeleted });
|
|
506
|
-
|
|
507
|
-
const results: QueryResult[] = await vectorDB.query({
|
|
508
|
-
indexName: testIndexName,
|
|
509
|
-
queryVector: [1.0, 0.0, 0.0],
|
|
510
|
-
topK: 2,
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
expect(results).toHaveLength(2);
|
|
514
|
-
expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
describe('Basic Query Operations', () => {
|
|
519
|
-
['flat', 'hnsw', 'ivfflat'].forEach(indexType => {
|
|
520
|
-
const indexName = `test_query_2_${indexType}`;
|
|
521
|
-
beforeAll(async () => {
|
|
522
|
-
try {
|
|
523
|
-
await vectorDB.deleteIndex({ indexName });
|
|
524
|
-
} catch {
|
|
525
|
-
// Ignore if doesn't exist
|
|
526
|
-
}
|
|
527
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
beforeEach(async () => {
|
|
531
|
-
await vectorDB.truncateIndex({ indexName });
|
|
532
|
-
const vectors = [
|
|
533
|
-
[1, 0, 0],
|
|
534
|
-
[0.8, 0.2, 0],
|
|
535
|
-
[0, 1, 0],
|
|
536
|
-
];
|
|
537
|
-
const metadata = [
|
|
538
|
-
{ type: 'a', value: 1 },
|
|
539
|
-
{ type: 'b', value: 2 },
|
|
540
|
-
{ type: 'c', value: 3 },
|
|
541
|
-
];
|
|
542
|
-
await vectorDB.upsert({ indexName, vectors, metadata });
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
afterAll(async () => {
|
|
546
|
-
await vectorDB.deleteIndex({ indexName });
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('should return closest vectors', async () => {
|
|
550
|
-
const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0], topK: 1 });
|
|
551
|
-
expect(results).toHaveLength(1);
|
|
552
|
-
expect(results[0]?.vector).toBe(undefined);
|
|
553
|
-
expect(results[0]?.score).toBeCloseTo(1, 5);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('should return vector with result', async () => {
|
|
557
|
-
const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0], topK: 1, includeVector: true });
|
|
558
|
-
expect(results).toHaveLength(1);
|
|
559
|
-
expect(results[0]?.vector).toStrictEqual([1, 0, 0]);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it('should respect topK parameter', async () => {
|
|
563
|
-
const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0], topK: 2 });
|
|
564
|
-
expect(results).toHaveLength(2);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('should handle filters correctly', async () => {
|
|
568
|
-
const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0], topK: 10, filter: { type: 'a' } });
|
|
569
|
-
|
|
570
|
-
expect(results).toHaveLength(1);
|
|
571
|
-
results.forEach(result => {
|
|
572
|
-
expect(result?.metadata?.type).toBe('a');
|
|
573
|
-
});
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// Advanced Query and Filter Tests
|
|
580
|
-
describe('Advanced Query and Filter Operations', () => {
|
|
581
|
-
const indexName = 'test_query_filters';
|
|
582
|
-
beforeAll(async () => {
|
|
583
|
-
try {
|
|
584
|
-
await vectorDB.deleteIndex({ indexName });
|
|
585
|
-
} catch {
|
|
586
|
-
// Ignore if doesn't exist
|
|
587
|
-
}
|
|
588
|
-
await vectorDB.createIndex({ indexName, dimension: 3 });
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
beforeEach(async () => {
|
|
592
|
-
await vectorDB.truncateIndex({ indexName });
|
|
593
|
-
const vectors = [
|
|
594
|
-
[1, 0.1, 0],
|
|
595
|
-
[0.9, 0.2, 0],
|
|
596
|
-
[0.95, 0.1, 0],
|
|
597
|
-
[0.85, 0.2, 0],
|
|
598
|
-
[0.9, 0.1, 0],
|
|
599
|
-
];
|
|
600
|
-
|
|
601
|
-
const metadata = [
|
|
602
|
-
{
|
|
603
|
-
category: 'electronics',
|
|
604
|
-
price: 100,
|
|
605
|
-
tags: ['new', 'premium'],
|
|
606
|
-
active: true,
|
|
607
|
-
ratings: [4.5, 4.8, 4.2], // Array of numbers
|
|
608
|
-
stock: [
|
|
609
|
-
{ location: 'A', count: 25 },
|
|
610
|
-
{ location: 'B', count: 15 },
|
|
611
|
-
], // Array of objects
|
|
612
|
-
reviews: [
|
|
613
|
-
{ user: 'alice', score: 5, verified: true },
|
|
614
|
-
{ user: 'bob', score: 4, verified: true },
|
|
615
|
-
{ user: 'charlie', score: 3, verified: false },
|
|
616
|
-
], // Complex array objects
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
category: 'books',
|
|
620
|
-
price: 50,
|
|
621
|
-
tags: ['used'],
|
|
622
|
-
active: true,
|
|
623
|
-
ratings: [3.8, 4.0, 4.1],
|
|
624
|
-
stock: [
|
|
625
|
-
{ location: 'A', count: 10 },
|
|
626
|
-
{ location: 'C', count: 30 },
|
|
627
|
-
],
|
|
628
|
-
reviews: [
|
|
629
|
-
{ user: 'dave', score: 4, verified: true },
|
|
630
|
-
{ user: 'eve', score: 5, verified: false },
|
|
631
|
-
],
|
|
632
|
-
},
|
|
633
|
-
{ category: 'electronics', price: 75, tags: ['refurbished'], active: false },
|
|
634
|
-
{ category: 'books', price: 25, tags: ['used', 'sale'], active: true },
|
|
635
|
-
{ category: 'clothing', price: 60, tags: ['new'], active: true },
|
|
636
|
-
];
|
|
637
|
-
|
|
638
|
-
await vectorDB.upsert({ indexName, vectors, metadata });
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
afterAll(async () => {
|
|
642
|
-
await vectorDB.deleteIndex({ indexName });
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// Numeric Comparison Tests
|
|
646
|
-
describe('Comparison Operators', () => {
|
|
647
|
-
it('should handle numeric string comparisons', async () => {
|
|
648
|
-
// Insert a record with numeric string
|
|
649
|
-
await vectorDB.upsert({ indexName, vectors: [[1, 0.1, 0]], metadata: [{ numericString: '123' }] });
|
|
650
|
-
|
|
651
|
-
const results = await vectorDB.query({
|
|
652
|
-
indexName,
|
|
653
|
-
queryVector: [1, 0, 0],
|
|
654
|
-
filter: { numericString: { $gt: '100' } },
|
|
655
|
-
});
|
|
656
|
-
expect(results.length).toBeGreaterThan(0);
|
|
657
|
-
expect(results[0]?.metadata?.numericString).toBe('123');
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
it('should filter with $gt operator', async () => {
|
|
661
|
-
const results = await vectorDB.query({
|
|
662
|
-
indexName,
|
|
663
|
-
queryVector: [1, 0, 0],
|
|
664
|
-
filter: { price: { $gt: 75 } },
|
|
665
|
-
});
|
|
666
|
-
expect(results).toHaveLength(1);
|
|
667
|
-
expect(results[0]?.metadata?.price).toBe(100);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it('should filter with $lte operator', async () => {
|
|
671
|
-
const results = await vectorDB.query({
|
|
672
|
-
indexName,
|
|
673
|
-
queryVector: [1, 0, 0],
|
|
674
|
-
filter: { price: { $lte: 50 } },
|
|
675
|
-
});
|
|
676
|
-
expect(results).toHaveLength(2);
|
|
677
|
-
results.forEach(result => {
|
|
678
|
-
expect(result.metadata?.price).toBeLessThanOrEqual(50);
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
it('should filter with lt operator', async () => {
|
|
683
|
-
const results = await vectorDB.query({
|
|
684
|
-
indexName,
|
|
685
|
-
queryVector: [1, 0, 0],
|
|
686
|
-
filter: { price: { $lt: 60 } },
|
|
687
|
-
});
|
|
688
|
-
expect(results).toHaveLength(2);
|
|
689
|
-
results.forEach(result => {
|
|
690
|
-
expect(result.metadata?.price).toBeLessThan(60);
|
|
691
|
-
});
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
it('should filter with gte operator', async () => {
|
|
695
|
-
const results = await vectorDB.query({
|
|
696
|
-
indexName,
|
|
697
|
-
queryVector: [1, 0, 0],
|
|
698
|
-
filter: { price: { $gte: 75 } },
|
|
699
|
-
});
|
|
700
|
-
expect(results).toHaveLength(2);
|
|
701
|
-
results.forEach(result => {
|
|
702
|
-
expect(result.metadata?.price).toBeGreaterThanOrEqual(75);
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it('should filter with ne operator', async () => {
|
|
707
|
-
const results = await vectorDB.query({
|
|
708
|
-
indexName,
|
|
709
|
-
queryVector: [1, 0, 0],
|
|
710
|
-
filter: { category: { $ne: 'electronics' } },
|
|
711
|
-
});
|
|
712
|
-
expect(results.length).toBeGreaterThan(0);
|
|
713
|
-
results.forEach(result => {
|
|
714
|
-
expect(result.metadata?.category).not.toBe('electronics');
|
|
715
|
-
});
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
it('should filter with $gt and $lte operator', async () => {
|
|
719
|
-
const results = await vectorDB.query({
|
|
720
|
-
indexName,
|
|
721
|
-
queryVector: [1, 0, 0],
|
|
722
|
-
filter: { price: { $gt: 70, $lte: 100 } },
|
|
723
|
-
});
|
|
724
|
-
expect(results).toHaveLength(2);
|
|
725
|
-
results.forEach(result => {
|
|
726
|
-
expect(result.metadata?.price).toBeGreaterThan(70);
|
|
727
|
-
expect(result.metadata?.price).toBeLessThanOrEqual(100);
|
|
728
|
-
});
|
|
729
|
-
});
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
// Array Operator Tests
|
|
733
|
-
describe('Array Operators', () => {
|
|
734
|
-
it('should filter with $in operator for scalar field', async () => {
|
|
735
|
-
const results = await vectorDB.query({
|
|
736
|
-
indexName,
|
|
737
|
-
queryVector: [1, 0, 0],
|
|
738
|
-
filter: { category: { $in: ['electronics', 'clothing'] } },
|
|
739
|
-
});
|
|
740
|
-
expect(results).toHaveLength(3);
|
|
741
|
-
results.forEach(result => {
|
|
742
|
-
expect(['electronics', 'clothing']).toContain(result.metadata?.category);
|
|
743
|
-
});
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it('should filter with $in operator for array field', async () => {
|
|
747
|
-
// Insert a record with tags as array
|
|
748
|
-
await vectorDB.upsert({
|
|
749
|
-
indexName,
|
|
750
|
-
vectors: [[2, 0.2, 0]],
|
|
751
|
-
metadata: [{ tags: ['featured', 'sale', 'new'] }],
|
|
752
|
-
});
|
|
753
|
-
const results = await vectorDB.query({
|
|
754
|
-
indexName,
|
|
755
|
-
queryVector: [1, 0, 0],
|
|
756
|
-
filter: { tags: { $in: ['sale', 'clearance'] } },
|
|
757
|
-
});
|
|
758
|
-
expect(results.length).toBeGreaterThan(0);
|
|
759
|
-
results.forEach(result => {
|
|
760
|
-
expect(result.metadata?.tags.some((tag: string) => ['sale', 'clearance'].includes(tag))).toBe(true);
|
|
761
|
-
});
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
it('should filter with $nin operator for scalar field', async () => {
|
|
765
|
-
const results = await vectorDB.query({
|
|
766
|
-
indexName,
|
|
767
|
-
queryVector: [1, 0, 0],
|
|
768
|
-
filter: { category: { $nin: ['electronics', 'books'] } },
|
|
769
|
-
});
|
|
770
|
-
expect(results.length).toBeGreaterThan(0);
|
|
771
|
-
results.forEach(result => {
|
|
772
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
773
|
-
});
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it('should filter with $nin operator for array field', async () => {
|
|
777
|
-
// Insert a record with tags as array
|
|
778
|
-
await vectorDB.upsert({
|
|
779
|
-
indexName,
|
|
780
|
-
vectors: [[2, 0.3, 0]],
|
|
781
|
-
metadata: [{ tags: ['clearance', 'used'] }],
|
|
782
|
-
});
|
|
783
|
-
const results = await vectorDB.query({
|
|
784
|
-
indexName,
|
|
785
|
-
queryVector: [1, 0, 0],
|
|
786
|
-
filter: { tags: { $nin: ['new', 'sale'] } },
|
|
787
|
-
});
|
|
788
|
-
expect(results.length).toBeGreaterThan(0);
|
|
789
|
-
results.forEach(result => {
|
|
790
|
-
expect(result.metadata?.tags.every((tag: string) => !['new', 'sale'].includes(tag))).toBe(true);
|
|
791
|
-
});
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
it('should handle empty arrays in in/nin operators', async () => {
|
|
795
|
-
// Should return no results for empty IN
|
|
796
|
-
const resultsIn = await vectorDB.query({
|
|
797
|
-
indexName,
|
|
798
|
-
queryVector: [1, 0, 0],
|
|
799
|
-
filter: { category: { $in: [] } },
|
|
800
|
-
});
|
|
801
|
-
expect(resultsIn).toHaveLength(0);
|
|
802
|
-
|
|
803
|
-
// Should return all results for empty NIN
|
|
804
|
-
const resultsNin = await vectorDB.query({
|
|
805
|
-
indexName,
|
|
806
|
-
queryVector: [1, 0, 0],
|
|
807
|
-
filter: { category: { $nin: [] } },
|
|
808
|
-
});
|
|
809
|
-
expect(resultsNin.length).toBeGreaterThan(0);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it('should filter with array $contains operator', async () => {
|
|
813
|
-
const results = await vectorDB.query({
|
|
814
|
-
indexName,
|
|
815
|
-
queryVector: [1, 0.1, 0],
|
|
816
|
-
filter: { tags: { $contains: ['new'] } },
|
|
817
|
-
});
|
|
818
|
-
expect(results.length).toBeGreaterThan(0);
|
|
819
|
-
results.forEach(result => {
|
|
820
|
-
expect(result.metadata?.tags).toContain('new');
|
|
821
|
-
});
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
it('should filter with $contains operator for string substring', async () => {
|
|
825
|
-
const results = await vectorDB.query({
|
|
826
|
-
indexName,
|
|
827
|
-
queryVector: [1, 0, 0],
|
|
828
|
-
filter: { category: { $contains: 'lectro' } },
|
|
829
|
-
});
|
|
830
|
-
expect(results.length).toBeGreaterThan(0);
|
|
831
|
-
results.forEach(result => {
|
|
832
|
-
expect(result.metadata?.category).toContain('lectro');
|
|
833
|
-
});
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
it('should not match deep object containment with $contains', async () => {
|
|
837
|
-
// Insert a record with a nested object
|
|
838
|
-
await vectorDB.upsert({
|
|
839
|
-
indexName,
|
|
840
|
-
vectors: [[1, 0.1, 0]],
|
|
841
|
-
metadata: [{ details: { color: 'red', size: 'large' }, category: 'clothing' }],
|
|
842
|
-
});
|
|
843
|
-
// $contains does NOT support deep object containment in Postgres
|
|
844
|
-
const results = await vectorDB.query({
|
|
845
|
-
indexName,
|
|
846
|
-
queryVector: [1, 0.1, 0],
|
|
847
|
-
filter: { details: { $contains: { color: 'red' } } },
|
|
848
|
-
});
|
|
849
|
-
expect(results.length).toBe(0);
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
it('should fallback to direct equality for non-array, non-string', async () => {
|
|
853
|
-
// Insert a record with a numeric field
|
|
854
|
-
await vectorDB.upsert({
|
|
855
|
-
indexName,
|
|
856
|
-
vectors: [[1, 0.2, 0]],
|
|
857
|
-
metadata: [{ price: 123 }],
|
|
858
|
-
});
|
|
859
|
-
const results = await vectorDB.query({
|
|
860
|
-
indexName,
|
|
861
|
-
queryVector: [1, 0, 0],
|
|
862
|
-
filter: { price: { $contains: 123 } },
|
|
863
|
-
});
|
|
864
|
-
expect(results.length).toBeGreaterThan(0);
|
|
865
|
-
results.forEach(result => {
|
|
866
|
-
expect(result.metadata?.price).toBe(123);
|
|
867
|
-
});
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
it('should filter with $elemMatch operator', async () => {
|
|
871
|
-
const results = await vectorDB.query({
|
|
872
|
-
indexName,
|
|
873
|
-
queryVector: [1, 0, 0],
|
|
874
|
-
filter: { tags: { $elemMatch: { $in: ['new', 'premium'] } } },
|
|
875
|
-
});
|
|
876
|
-
expect(results.length).toBeGreaterThan(0);
|
|
877
|
-
results.forEach(result => {
|
|
878
|
-
expect(result.metadata?.tags.some(tag => ['new', 'premium'].includes(tag))).toBe(true);
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
it('should filter with $elemMatch using equality', async () => {
|
|
883
|
-
const results = await vectorDB.query({
|
|
884
|
-
indexName,
|
|
885
|
-
queryVector: [1, 0, 0],
|
|
886
|
-
filter: { tags: { $elemMatch: { $eq: 'sale' } } },
|
|
887
|
-
});
|
|
888
|
-
expect(results).toHaveLength(1);
|
|
889
|
-
expect(results[0]?.metadata?.tags).toContain('sale');
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
it('should filter with $elemMatch using multiple conditions', async () => {
|
|
893
|
-
const results = await vectorDB.query({
|
|
894
|
-
indexName,
|
|
895
|
-
queryVector: [1, 0, 0],
|
|
896
|
-
filter: { ratings: { $elemMatch: { $gt: 4, $lt: 4.5 } } },
|
|
897
|
-
});
|
|
898
|
-
expect(results.length).toBeGreaterThan(0);
|
|
899
|
-
results.forEach(result => {
|
|
900
|
-
expect(Array.isArray(result.metadata?.ratings)).toBe(true);
|
|
901
|
-
expect(result.metadata?.ratings.some(rating => rating > 4 && rating < 4.5)).toBe(true);
|
|
902
|
-
});
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
it('should handle complex $elemMatch conditions', async () => {
|
|
906
|
-
const results = await vectorDB.query({
|
|
907
|
-
indexName,
|
|
908
|
-
queryVector: [1, 0, 0],
|
|
909
|
-
filter: { stock: { $elemMatch: { location: 'A', count: { $gt: 20 } } } },
|
|
910
|
-
});
|
|
911
|
-
expect(results.length).toBeGreaterThan(0);
|
|
912
|
-
results.forEach(result => {
|
|
913
|
-
const matchingStock = result.metadata?.stock.find(s => s.location === 'A' && s.count > 20);
|
|
914
|
-
expect(matchingStock).toBeDefined();
|
|
915
|
-
});
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
it('should filter with $elemMatch on nested numeric fields', async () => {
|
|
919
|
-
const results = await vectorDB.query({
|
|
920
|
-
indexName,
|
|
921
|
-
queryVector: [1, 0, 0],
|
|
922
|
-
filter: { reviews: { $elemMatch: { score: { $gt: 4 } } } },
|
|
923
|
-
});
|
|
924
|
-
expect(results.length).toBeGreaterThan(0);
|
|
925
|
-
results.forEach(result => {
|
|
926
|
-
expect(result.metadata?.reviews.some(r => r.score > 4)).toBe(true);
|
|
927
|
-
});
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
it('should filter with $elemMatch on multiple nested fields', async () => {
|
|
931
|
-
const results = await vectorDB.query({
|
|
932
|
-
indexName,
|
|
933
|
-
queryVector: [1, 0, 0],
|
|
934
|
-
filter: { reviews: { $elemMatch: { score: { $gte: 4 }, verified: true } } },
|
|
935
|
-
});
|
|
936
|
-
expect(results.length).toBeGreaterThan(0);
|
|
937
|
-
results.forEach(result => {
|
|
938
|
-
expect(result.metadata?.reviews.some(r => r.score >= 4 && r.verified)).toBe(true);
|
|
939
|
-
});
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
it('should filter with $elemMatch on exact string match', async () => {
|
|
943
|
-
const results = await vectorDB.query({
|
|
944
|
-
indexName,
|
|
945
|
-
queryVector: [1, 0, 0],
|
|
946
|
-
filter: { reviews: { $elemMatch: { user: 'alice' } } },
|
|
947
|
-
});
|
|
948
|
-
expect(results).toHaveLength(1);
|
|
949
|
-
expect(results[0].metadata?.reviews.some(r => r.user === 'alice')).toBe(true);
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
it('should handle $elemMatch with no matches', async () => {
|
|
953
|
-
const results = await vectorDB.query({
|
|
954
|
-
indexName,
|
|
955
|
-
queryVector: [1, 0, 0],
|
|
956
|
-
filter: { reviews: { $elemMatch: { score: 10 } } },
|
|
957
|
-
});
|
|
958
|
-
expect(results).toHaveLength(0);
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
it('should filter with $all operator', async () => {
|
|
962
|
-
const results = await vectorDB.query({
|
|
963
|
-
indexName,
|
|
964
|
-
queryVector: [1, 0, 0],
|
|
965
|
-
filter: { tags: { $all: ['used', 'sale'] } },
|
|
966
|
-
});
|
|
967
|
-
expect(results).toHaveLength(1);
|
|
968
|
-
results.forEach(result => {
|
|
969
|
-
expect(result.metadata?.tags).toContain('used');
|
|
970
|
-
expect(result.metadata?.tags).toContain('sale');
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
it('should filter with $all using single value', async () => {
|
|
975
|
-
const results = await vectorDB.query({
|
|
976
|
-
indexName,
|
|
977
|
-
queryVector: [1, 0, 0],
|
|
978
|
-
filter: { tags: { $all: ['new'] } },
|
|
979
|
-
});
|
|
980
|
-
expect(results.length).toBeGreaterThan(0);
|
|
981
|
-
results.forEach(result => {
|
|
982
|
-
expect(result.metadata?.tags).toContain('new');
|
|
983
|
-
});
|
|
984
|
-
});
|
|
985
|
-
|
|
986
|
-
it('should handle empty array for $all', async () => {
|
|
987
|
-
const results = await vectorDB.query({
|
|
988
|
-
indexName,
|
|
989
|
-
queryVector: [1, 0, 0],
|
|
990
|
-
filter: { tags: { $all: [] } },
|
|
991
|
-
});
|
|
992
|
-
expect(results).toHaveLength(0);
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
it('should handle non-array field $all', async () => {
|
|
996
|
-
// First insert a record with non-array field
|
|
997
|
-
await vectorDB.upsert({ indexName, vectors: [[1, 0.1, 0]], metadata: [{ tags: 'not-an-array' }] });
|
|
998
|
-
|
|
999
|
-
const results = await vectorDB.query({
|
|
1000
|
-
indexName,
|
|
1001
|
-
queryVector: [1, 0, 0],
|
|
1002
|
-
filter: { tags: { $all: ['value'] } },
|
|
1003
|
-
});
|
|
1004
|
-
expect(results).toHaveLength(0);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
// Contains Operator Tests
|
|
1008
|
-
it('should filter with contains operator for exact field match', async () => {
|
|
1009
|
-
const results = await vectorDB.query({
|
|
1010
|
-
indexName,
|
|
1011
|
-
queryVector: [1, 0.1, 0],
|
|
1012
|
-
filter: { category: { $contains: 'electronics' } },
|
|
1013
|
-
});
|
|
1014
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1015
|
-
results.forEach(result => {
|
|
1016
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1017
|
-
});
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
// it('should filter with $objectContains operator for nested objects', async () => {
|
|
1021
|
-
// // First insert a record with nested object
|
|
1022
|
-
// await vectorDB.upsert({
|
|
1023
|
-
// indexName,
|
|
1024
|
-
// vectors: [[1, 0.1, 0]],
|
|
1025
|
-
// metadata: [
|
|
1026
|
-
// {
|
|
1027
|
-
// details: { color: 'red', size: 'large' },
|
|
1028
|
-
// category: 'clothing',
|
|
1029
|
-
// },
|
|
1030
|
-
// ],
|
|
1031
|
-
// });
|
|
1032
|
-
|
|
1033
|
-
// const results = await vectorDB.query({
|
|
1034
|
-
// indexName,
|
|
1035
|
-
// queryVector: [1, 0.1, 0],
|
|
1036
|
-
// filter: { details: { $objectContains: { color: 'red' } } },
|
|
1037
|
-
// });
|
|
1038
|
-
// expect(results.length).toBeGreaterThan(0);
|
|
1039
|
-
// results.forEach(result => {
|
|
1040
|
-
// expect(result.metadata?.details.color).toBe('red');
|
|
1041
|
-
// });
|
|
1042
|
-
// });
|
|
1043
|
-
|
|
1044
|
-
// String Pattern Tests
|
|
1045
|
-
it('should handle exact string matches', async () => {
|
|
1046
|
-
const results = await vectorDB.query({
|
|
1047
|
-
indexName,
|
|
1048
|
-
queryVector: [1, 0, 0],
|
|
1049
|
-
filter: { category: 'electronics' },
|
|
1050
|
-
});
|
|
1051
|
-
expect(results).toHaveLength(2);
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
it('should handle case-sensitive string matches', async () => {
|
|
1055
|
-
const results = await vectorDB.query({
|
|
1056
|
-
indexName,
|
|
1057
|
-
queryVector: [1, 0, 0],
|
|
1058
|
-
filter: { category: 'ELECTRONICS' },
|
|
1059
|
-
});
|
|
1060
|
-
expect(results).toHaveLength(0);
|
|
1061
|
-
});
|
|
1062
|
-
it('should filter arrays by size', async () => {
|
|
1063
|
-
const results = await vectorDB.query({
|
|
1064
|
-
indexName,
|
|
1065
|
-
queryVector: [1, 0, 0],
|
|
1066
|
-
filter: { ratings: { $size: 3 } },
|
|
1067
|
-
});
|
|
1068
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1069
|
-
results.forEach(result => {
|
|
1070
|
-
expect(result.metadata?.ratings).toHaveLength(3);
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
const noResults = await vectorDB.query({
|
|
1074
|
-
indexName,
|
|
1075
|
-
queryVector: [1, 0, 0],
|
|
1076
|
-
filter: { ratings: { $size: 10 } },
|
|
1077
|
-
});
|
|
1078
|
-
expect(noResults).toHaveLength(0);
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
|
-
it('should handle $size with nested arrays', async () => {
|
|
1082
|
-
await vectorDB.upsert({ indexName, vectors: [[1, 0.1, 0]], metadata: [{ nested: { array: [1, 2, 3, 4] } }] });
|
|
1083
|
-
const results = await vectorDB.query({
|
|
1084
|
-
indexName,
|
|
1085
|
-
queryVector: [1, 0, 0],
|
|
1086
|
-
filter: { 'nested.array': { $size: 4 } },
|
|
1087
|
-
});
|
|
1088
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1089
|
-
results.forEach(result => {
|
|
1090
|
-
expect(result.metadata?.nested.array).toHaveLength(4);
|
|
1091
|
-
});
|
|
1092
|
-
});
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
// Logical Operator Tests
|
|
1096
|
-
describe('Logical Operators', () => {
|
|
1097
|
-
it('should handle AND filter conditions', async () => {
|
|
1098
|
-
const results = await vectorDB.query({
|
|
1099
|
-
indexName,
|
|
1100
|
-
queryVector: [1, 0, 0],
|
|
1101
|
-
filter: { $and: [{ category: { $eq: 'electronics' } }, { price: { $gt: 75 } }] },
|
|
1102
|
-
});
|
|
1103
|
-
expect(results).toHaveLength(1);
|
|
1104
|
-
expect(results[0]?.metadata?.category).toBe('electronics');
|
|
1105
|
-
expect(results[0]?.metadata?.price).toBeGreaterThan(75);
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
it('should handle OR filter conditions', async () => {
|
|
1109
|
-
const results = await vectorDB.query({
|
|
1110
|
-
indexName,
|
|
1111
|
-
queryVector: [1, 0, 0],
|
|
1112
|
-
filter: { $or: [{ category: { $eq: 'electronics' } }, { category: { $eq: 'books' } }] },
|
|
1113
|
-
});
|
|
1114
|
-
expect(results.length).toBeGreaterThan(1);
|
|
1115
|
-
results.forEach(result => {
|
|
1116
|
-
expect(['electronics', 'books']).toContain(result?.metadata?.category);
|
|
1117
|
-
});
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
it('should handle $not operator', async () => {
|
|
1121
|
-
const results = await vectorDB.query({
|
|
1122
|
-
indexName,
|
|
1123
|
-
queryVector: [1, 0, 0],
|
|
1124
|
-
filter: { $not: { category: 'electronics' } },
|
|
1125
|
-
});
|
|
1126
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1127
|
-
results.forEach(result => {
|
|
1128
|
-
expect(result.metadata?.category).not.toBe('electronics');
|
|
1129
|
-
});
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
it('should handle $nor operator', async () => {
|
|
1133
|
-
const results = await vectorDB.query({
|
|
1134
|
-
indexName,
|
|
1135
|
-
queryVector: [1, 0, 0],
|
|
1136
|
-
filter: { $nor: [{ category: 'electronics' }, { category: 'books' }] },
|
|
1137
|
-
});
|
|
1138
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1139
|
-
results.forEach(result => {
|
|
1140
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
1141
|
-
});
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
it('should handle nested $not with $or', async () => {
|
|
1145
|
-
const results = await vectorDB.query({
|
|
1146
|
-
indexName,
|
|
1147
|
-
queryVector: [1, 0, 0],
|
|
1148
|
-
filter: { $not: { $or: [{ category: 'electronics' }, { category: 'books' }] } },
|
|
1149
|
-
});
|
|
1150
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1151
|
-
results.forEach(result => {
|
|
1152
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
1153
|
-
});
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
it('should handle $not with comparison operators', async () => {
|
|
1157
|
-
const results = await vectorDB.query({
|
|
1158
|
-
indexName,
|
|
1159
|
-
queryVector: [1, 0, 0],
|
|
1160
|
-
filter: { price: { $not: { $gt: 100 } } },
|
|
1161
|
-
});
|
|
1162
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1163
|
-
results.forEach(result => {
|
|
1164
|
-
expect(Number(result.metadata?.price)).toBeLessThanOrEqual(100);
|
|
1165
|
-
});
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
it('should handle $not with $in operator', async () => {
|
|
1169
|
-
const results = await vectorDB.query({
|
|
1170
|
-
indexName,
|
|
1171
|
-
queryVector: [1, 0, 0],
|
|
1172
|
-
filter: { category: { $not: { $in: ['electronics', 'books'] } } },
|
|
1173
|
-
});
|
|
1174
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1175
|
-
results.forEach(result => {
|
|
1176
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
1177
|
-
});
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
it('should handle $not with multiple nested conditions', async () => {
|
|
1181
|
-
const results = await vectorDB.query({
|
|
1182
|
-
indexName,
|
|
1183
|
-
queryVector: [1, 0, 0],
|
|
1184
|
-
filter: { $not: { $and: [{ category: 'electronics' }, { price: { $gt: 50 } }] } },
|
|
1185
|
-
});
|
|
1186
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1187
|
-
results.forEach(result => {
|
|
1188
|
-
expect(result.metadata?.category !== 'electronics' || result.metadata?.price <= 50).toBe(true);
|
|
1189
|
-
});
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
it('should handle $not with $exists operator', async () => {
|
|
1193
|
-
const results = await vectorDB.query({
|
|
1194
|
-
indexName,
|
|
1195
|
-
queryVector: [1, 0, 0],
|
|
1196
|
-
filter: { tags: { $not: { $exists: true } } },
|
|
1197
|
-
});
|
|
1198
|
-
expect(results.length).toBe(0); // All test data has tags
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
it('should handle $not with array operators', async () => {
|
|
1202
|
-
const results = await vectorDB.query({
|
|
1203
|
-
indexName,
|
|
1204
|
-
queryVector: [1, 0, 0],
|
|
1205
|
-
filter: { tags: { $not: { $all: ['new', 'premium'] } } },
|
|
1206
|
-
});
|
|
1207
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1208
|
-
results.forEach(result => {
|
|
1209
|
-
expect(!result.metadata?.tags.includes('new') || !result.metadata?.tags.includes('premium')).toBe(true);
|
|
1210
|
-
});
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
it('should handle $not with complex nested conditions', async () => {
|
|
1214
|
-
const results = await vectorDB.query({
|
|
1215
|
-
indexName,
|
|
1216
|
-
queryVector: [1, 0, 0],
|
|
1217
|
-
filter: {
|
|
1218
|
-
$not: {
|
|
1219
|
-
$or: [
|
|
1220
|
-
{
|
|
1221
|
-
$and: [{ category: 'electronics' }, { price: { $gt: 90 } }],
|
|
1222
|
-
},
|
|
1223
|
-
{
|
|
1224
|
-
$and: [{ category: 'books' }, { price: { $lt: 30 } }],
|
|
1225
|
-
},
|
|
1226
|
-
],
|
|
1227
|
-
},
|
|
1228
|
-
},
|
|
1229
|
-
});
|
|
1230
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1231
|
-
results.forEach(result => {
|
|
1232
|
-
const notExpensiveElectronics = !(result.metadata?.category === 'electronics' && result.metadata?.price > 90);
|
|
1233
|
-
const notCheapBooks = !(result.metadata?.category === 'books' && result.metadata?.price < 30);
|
|
1234
|
-
expect(notExpensiveElectronics && notCheapBooks).toBe(true);
|
|
1235
|
-
});
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
it('should handle $not with empty arrays', async () => {
|
|
1239
|
-
const results = await vectorDB.query({
|
|
1240
|
-
indexName,
|
|
1241
|
-
queryVector: [1, 0, 0],
|
|
1242
|
-
filter: { tags: { $not: { $in: [] } } },
|
|
1243
|
-
});
|
|
1244
|
-
expect(results.length).toBeGreaterThan(0); // Should match all records
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
it('should handle $not with null values', async () => {
|
|
1248
|
-
// First insert a record with null value
|
|
1249
|
-
await vectorDB.upsert({
|
|
1250
|
-
indexName,
|
|
1251
|
-
vectors: [[1, 0.1, 0]],
|
|
1252
|
-
metadata: [{ category: null, price: 0 }],
|
|
1253
|
-
});
|
|
1254
|
-
|
|
1255
|
-
const results = await vectorDB.query({
|
|
1256
|
-
indexName,
|
|
1257
|
-
queryVector: [1, 0, 0],
|
|
1258
|
-
filter: { category: { $not: { $eq: null } } },
|
|
1259
|
-
});
|
|
1260
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1261
|
-
results.forEach(result => {
|
|
1262
|
-
expect(result.metadata?.category).not.toBeNull();
|
|
1263
|
-
});
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
it('should handle $not with boolean values', async () => {
|
|
1267
|
-
const results = await vectorDB.query({
|
|
1268
|
-
indexName,
|
|
1269
|
-
queryVector: [1, 0, 0],
|
|
1270
|
-
filter: { active: { $not: { $eq: true } } },
|
|
1271
|
-
});
|
|
1272
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1273
|
-
results.forEach(result => {
|
|
1274
|
-
expect(result.metadata?.active).not.toBe(true);
|
|
1275
|
-
});
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
it('should handle $not with multiple conditions', async () => {
|
|
1279
|
-
const results = await vectorDB.query({
|
|
1280
|
-
indexName,
|
|
1281
|
-
queryVector: [1, 0, 0],
|
|
1282
|
-
filter: { $not: { category: 'electronics', price: { $gt: 50 } } },
|
|
1283
|
-
});
|
|
1284
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
it('should handle $not with $not operator', async () => {
|
|
1288
|
-
const results = await vectorDB.query({
|
|
1289
|
-
indexName,
|
|
1290
|
-
queryVector: [1, 0, 0],
|
|
1291
|
-
filter: { $not: { $not: { category: 'electronics' } } },
|
|
1292
|
-
});
|
|
1293
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
it('should handle $not in nested fields', async () => {
|
|
1297
|
-
await vectorDB.upsert({
|
|
1298
|
-
indexName,
|
|
1299
|
-
vectors: [[1, 0.1, 0]],
|
|
1300
|
-
metadata: [{ user: { profile: { price: 10 } } }],
|
|
1301
|
-
});
|
|
1302
|
-
const results = await vectorDB.query({
|
|
1303
|
-
indexName,
|
|
1304
|
-
queryVector: [1, 0, 0],
|
|
1305
|
-
filter: { 'user.profile.price': { $not: { $gt: 25 } } },
|
|
1306
|
-
});
|
|
1307
|
-
expect(results.length).toBe(1);
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1310
|
-
it('should handle $not with multiple operators', async () => {
|
|
1311
|
-
const results = await vectorDB.query({
|
|
1312
|
-
indexName,
|
|
1313
|
-
queryVector: [1, 0, 0],
|
|
1314
|
-
filter: { price: { $not: { $gte: 30, $lte: 70 } } },
|
|
1315
|
-
});
|
|
1316
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1317
|
-
results.forEach(result => {
|
|
1318
|
-
const price = Number(result.metadata?.price);
|
|
1319
|
-
expect(price < 30 || price > 70).toBe(true);
|
|
1320
|
-
});
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
it('should handle $not with comparison operators', async () => {
|
|
1324
|
-
const results = await vectorDB.query({
|
|
1325
|
-
indexName,
|
|
1326
|
-
queryVector: [1, 0, 0],
|
|
1327
|
-
filter: { price: { $not: { $gt: 100 } } },
|
|
1328
|
-
});
|
|
1329
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1330
|
-
results.forEach(result => {
|
|
1331
|
-
expect(Number(result.metadata?.price)).toBeLessThanOrEqual(100);
|
|
1332
|
-
});
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
it('should handle $not with $and', async () => {
|
|
1336
|
-
const results = await vectorDB.query({
|
|
1337
|
-
indexName,
|
|
1338
|
-
queryVector: [1, 0, 0],
|
|
1339
|
-
filter: { $not: { $and: [{ category: 'electronics' }, { price: { $gt: 50 } }] } },
|
|
1340
|
-
});
|
|
1341
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1342
|
-
results.forEach(result => {
|
|
1343
|
-
expect(result.metadata?.category !== 'electronics' || result.metadata?.price <= 50).toBe(true);
|
|
1344
|
-
});
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
it('should handle $nor with $or', async () => {
|
|
1348
|
-
const results = await vectorDB.query({
|
|
1349
|
-
indexName,
|
|
1350
|
-
queryVector: [1, 0, 0],
|
|
1351
|
-
filter: { $nor: [{ $or: [{ category: 'electronics' }, { category: 'books' }] }, { price: { $gt: 75 } }] },
|
|
1352
|
-
});
|
|
1353
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1354
|
-
results.forEach(result => {
|
|
1355
|
-
expect(['electronics', 'books']).not.toContain(result.metadata?.category);
|
|
1356
|
-
expect(result.metadata?.price).toBeLessThanOrEqual(75);
|
|
1357
|
-
});
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
it('should handle $nor with nested $and conditions', async () => {
|
|
1361
|
-
const results = await vectorDB.query({
|
|
1362
|
-
indexName,
|
|
1363
|
-
queryVector: [1, 0, 0],
|
|
1364
|
-
filter: {
|
|
1365
|
-
$nor: [
|
|
1366
|
-
{ $and: [{ category: 'electronics' }, { active: true }] },
|
|
1367
|
-
{ $and: [{ category: 'books' }, { price: { $lt: 30 } }] },
|
|
1368
|
-
],
|
|
1369
|
-
},
|
|
1370
|
-
});
|
|
1371
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1372
|
-
results.forEach(result => {
|
|
1373
|
-
const notElectronicsActive = !(
|
|
1374
|
-
result.metadata?.category === 'electronics' && result.metadata?.active === true
|
|
1375
|
-
);
|
|
1376
|
-
const notBooksLowPrice = !(result.metadata?.category === 'books' && result.metadata?.price < 30);
|
|
1377
|
-
expect(notElectronicsActive && notBooksLowPrice).toBe(true);
|
|
1378
|
-
});
|
|
1379
|
-
});
|
|
1380
|
-
|
|
1381
|
-
it('should handle nested $and with $or and $not', async () => {
|
|
1382
|
-
const results = await vectorDB.query({
|
|
1383
|
-
indexName,
|
|
1384
|
-
queryVector: [1, 0, 0],
|
|
1385
|
-
filter: {
|
|
1386
|
-
$and: [{ $or: [{ category: 'electronics' }, { category: 'books' }] }, { $not: { price: { $lt: 50 } } }],
|
|
1387
|
-
},
|
|
1388
|
-
});
|
|
1389
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1390
|
-
results.forEach(result => {
|
|
1391
|
-
expect(['electronics', 'books']).toContain(result.metadata?.category);
|
|
1392
|
-
expect(result.metadata?.price).toBeGreaterThanOrEqual(50);
|
|
1393
|
-
});
|
|
1394
|
-
});
|
|
1395
|
-
|
|
1396
|
-
it('should handle $or with multiple $not conditions', async () => {
|
|
1397
|
-
const results = await vectorDB.query({
|
|
1398
|
-
indexName,
|
|
1399
|
-
queryVector: [1, 0, 0],
|
|
1400
|
-
filter: { $or: [{ $not: { category: 'electronics' } }, { $not: { price: { $gt: 50 } } }] },
|
|
1401
|
-
});
|
|
1402
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1403
|
-
results.forEach(result => {
|
|
1404
|
-
expect(result.metadata?.category !== 'electronics' || result.metadata?.price <= 50).toBe(true);
|
|
1405
|
-
});
|
|
1406
|
-
});
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
// Edge Cases and Special Values
|
|
1410
|
-
describe('Edge Cases and Special Values', () => {
|
|
1411
|
-
it('should handle empty result sets with valid filters', async () => {
|
|
1412
|
-
const results = await vectorDB.query({
|
|
1413
|
-
indexName,
|
|
1414
|
-
queryVector: [1, 0, 0],
|
|
1415
|
-
filter: { price: { $gt: 1000 } },
|
|
1416
|
-
});
|
|
1417
|
-
expect(results).toHaveLength(0);
|
|
1418
|
-
});
|
|
1419
|
-
|
|
1420
|
-
it('should throw error for invalid operator', async () => {
|
|
1421
|
-
await expect(
|
|
1422
|
-
vectorDB.query({
|
|
1423
|
-
indexName,
|
|
1424
|
-
queryVector: [1, 0, 0],
|
|
1425
|
-
filter: { price: { $invalid: 100 } } as any,
|
|
1426
|
-
}),
|
|
1427
|
-
).rejects.toThrow('Unsupported operator: $invalid');
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
it('should handle empty filter object', async () => {
|
|
1431
|
-
const results = await vectorDB.query({
|
|
1432
|
-
indexName,
|
|
1433
|
-
queryVector: [1, 0, 0],
|
|
1434
|
-
filter: {},
|
|
1435
|
-
});
|
|
1436
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
it('should handle numeric string comparisons', async () => {
|
|
1440
|
-
await vectorDB.upsert({
|
|
1441
|
-
indexName,
|
|
1442
|
-
vectors: [[1, 0.1, 0]],
|
|
1443
|
-
metadata: [{ numericString: '123' }],
|
|
1444
|
-
});
|
|
1445
|
-
const results = await vectorDB.query({
|
|
1446
|
-
indexName,
|
|
1447
|
-
queryVector: [1, 0, 0],
|
|
1448
|
-
filter: { numericString: { $gt: '100' } },
|
|
1449
|
-
});
|
|
1450
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1451
|
-
expect(results[0]?.metadata?.numericString).toBe('123');
|
|
1452
|
-
});
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
// Score Threshold Tests
|
|
1456
|
-
describe('Score Threshold', () => {
|
|
1457
|
-
it('should respect minimum score threshold', async () => {
|
|
1458
|
-
const results = await vectorDB.query({
|
|
1459
|
-
indexName,
|
|
1460
|
-
queryVector: [1, 0, 0],
|
|
1461
|
-
filter: { category: 'electronics' },
|
|
1462
|
-
includeVector: false,
|
|
1463
|
-
minScore: 0.9,
|
|
1464
|
-
});
|
|
1465
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1466
|
-
results.forEach(result => {
|
|
1467
|
-
expect(result.score).toBeGreaterThan(0.9);
|
|
1468
|
-
});
|
|
1469
|
-
});
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
describe('Error Handling', () => {
|
|
1473
|
-
const testIndexName = 'test_index_error';
|
|
1474
|
-
beforeAll(async () => {
|
|
1475
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
afterAll(async () => {
|
|
1479
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
1480
|
-
});
|
|
1481
|
-
|
|
1482
|
-
it('should handle non-existent index queries', async () => {
|
|
1483
|
-
await expect(vectorDB.query({ indexName: 'non_existent_index_yu', queryVector: [1, 2, 3] })).rejects.toThrow();
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
it('should handle invalid dimension vectors', async () => {
|
|
1487
|
-
const invalidVector = [1, 2, 3, 4]; // 4D vector for 3D index
|
|
1488
|
-
await expect(vectorDB.upsert({ indexName: testIndexName, vectors: [invalidVector] })).rejects.toThrow();
|
|
1489
|
-
});
|
|
1490
|
-
|
|
1491
|
-
it('should handle duplicate index creation gracefully', async () => {
|
|
1492
|
-
const duplicateIndexName = `duplicate_test`;
|
|
1493
|
-
const dimension = 768;
|
|
1494
|
-
|
|
1495
|
-
// Create index first time
|
|
1496
|
-
await vectorDB.createIndex({
|
|
1497
|
-
indexName: duplicateIndexName,
|
|
1498
|
-
dimension,
|
|
1499
|
-
metric: 'cosine',
|
|
1500
|
-
});
|
|
1501
|
-
|
|
1502
|
-
// Try to create with same dimensions - should not throw
|
|
1503
|
-
await expect(
|
|
1504
|
-
vectorDB.createIndex({
|
|
1505
|
-
indexName: duplicateIndexName,
|
|
1506
|
-
dimension,
|
|
1507
|
-
metric: 'cosine',
|
|
1508
|
-
}),
|
|
1509
|
-
).resolves.not.toThrow();
|
|
1510
|
-
|
|
1511
|
-
// Cleanup
|
|
1512
|
-
await vectorDB.deleteIndex({ indexName: duplicateIndexName });
|
|
1513
|
-
});
|
|
1514
|
-
});
|
|
1515
|
-
|
|
1516
|
-
describe('Edge Cases and Special Values', () => {
|
|
1517
|
-
// Additional Edge Cases
|
|
1518
|
-
it('should handle empty result sets with valid filters', async () => {
|
|
1519
|
-
const results = await vectorDB.query({
|
|
1520
|
-
indexName,
|
|
1521
|
-
queryVector: [1, 0, 0],
|
|
1522
|
-
filter: { price: { $gt: 1000 } },
|
|
1523
|
-
});
|
|
1524
|
-
expect(results).toHaveLength(0);
|
|
1525
|
-
});
|
|
1526
|
-
|
|
1527
|
-
it('should handle empty filter object', async () => {
|
|
1528
|
-
const results = await vectorDB.query({
|
|
1529
|
-
indexName,
|
|
1530
|
-
queryVector: [1, 0, 0],
|
|
1531
|
-
filter: {},
|
|
1532
|
-
});
|
|
1533
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1534
|
-
});
|
|
1535
|
-
|
|
1536
|
-
it('should handle non-existent field', async () => {
|
|
1537
|
-
const results = await vectorDB.query({
|
|
1538
|
-
indexName,
|
|
1539
|
-
queryVector: [1, 0, 0],
|
|
1540
|
-
filter: { nonexistent: { $elemMatch: { $eq: 'value' } } },
|
|
1541
|
-
});
|
|
1542
|
-
expect(results).toHaveLength(0);
|
|
1543
|
-
});
|
|
1544
|
-
|
|
1545
|
-
it('should handle non-existent values', async () => {
|
|
1546
|
-
const results = await vectorDB.query({
|
|
1547
|
-
indexName,
|
|
1548
|
-
queryVector: [1, 0, 0],
|
|
1549
|
-
filter: { tags: { $elemMatch: { $eq: 'nonexistent-tag' } } },
|
|
1550
|
-
});
|
|
1551
|
-
expect(results).toHaveLength(0);
|
|
1552
|
-
});
|
|
1553
|
-
// Empty Conditions Tests
|
|
1554
|
-
it('should handle empty conditions in logical operators', async () => {
|
|
1555
|
-
const results = await vectorDB.query({
|
|
1556
|
-
indexName,
|
|
1557
|
-
queryVector: [1, 0, 0],
|
|
1558
|
-
filter: { $and: [], category: 'electronics' },
|
|
1559
|
-
});
|
|
1560
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1561
|
-
results.forEach(result => {
|
|
1562
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1563
|
-
});
|
|
1564
|
-
});
|
|
1565
|
-
|
|
1566
|
-
it('should handle empty $and conditions', async () => {
|
|
1567
|
-
const results = await vectorDB.query({
|
|
1568
|
-
indexName,
|
|
1569
|
-
queryVector: [1, 0, 0],
|
|
1570
|
-
filter: { $and: [], category: 'electronics' },
|
|
1571
|
-
});
|
|
1572
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1573
|
-
results.forEach(result => {
|
|
1574
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1575
|
-
});
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
it('should handle empty $or conditions', async () => {
|
|
1579
|
-
const results = await vectorDB.query({
|
|
1580
|
-
indexName,
|
|
1581
|
-
queryVector: [1, 0, 0],
|
|
1582
|
-
filter: { $or: [], category: 'electronics' },
|
|
1583
|
-
});
|
|
1584
|
-
expect(results).toHaveLength(0);
|
|
1585
|
-
});
|
|
1586
|
-
|
|
1587
|
-
it('should handle empty $nor conditions', async () => {
|
|
1588
|
-
const results = await vectorDB.query({
|
|
1589
|
-
indexName,
|
|
1590
|
-
queryVector: [1, 0, 0],
|
|
1591
|
-
filter: { $nor: [], category: 'electronics' },
|
|
1592
|
-
});
|
|
1593
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1594
|
-
results.forEach(result => {
|
|
1595
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1596
|
-
});
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
it('should handle empty $not conditions', async () => {
|
|
1600
|
-
await expect(
|
|
1601
|
-
vectorDB.query({
|
|
1602
|
-
indexName,
|
|
1603
|
-
queryVector: [1, 0, 0],
|
|
1604
|
-
filter: { $not: {}, category: 'electronics' },
|
|
1605
|
-
}),
|
|
1606
|
-
).rejects.toThrow('$not operator cannot be empty');
|
|
1607
|
-
});
|
|
1608
|
-
|
|
1609
|
-
it('should handle multiple empty logical operators', async () => {
|
|
1610
|
-
const results = await vectorDB.query({
|
|
1611
|
-
indexName,
|
|
1612
|
-
queryVector: [1, 0, 0],
|
|
1613
|
-
filter: { $and: [], $or: [], $nor: [], category: 'electronics' },
|
|
1614
|
-
});
|
|
1615
|
-
expect(results).toHaveLength(0);
|
|
1616
|
-
});
|
|
1617
|
-
|
|
1618
|
-
// Nested Field Tests
|
|
1619
|
-
it('should handle deeply nested metadata paths', async () => {
|
|
1620
|
-
await vectorDB.upsert({
|
|
1621
|
-
indexName,
|
|
1622
|
-
vectors: [[1, 0.1, 0]],
|
|
1623
|
-
metadata: [
|
|
1624
|
-
{
|
|
1625
|
-
level1: {
|
|
1626
|
-
level2: {
|
|
1627
|
-
level3: 'deep value',
|
|
1628
|
-
},
|
|
1629
|
-
},
|
|
1630
|
-
},
|
|
1631
|
-
],
|
|
1632
|
-
});
|
|
1633
|
-
|
|
1634
|
-
const results = await vectorDB.query({
|
|
1635
|
-
indexName,
|
|
1636
|
-
queryVector: [1, 0, 0],
|
|
1637
|
-
filter: { 'level1.level2.level3': 'deep value' },
|
|
1638
|
-
});
|
|
1639
|
-
expect(results).toHaveLength(1);
|
|
1640
|
-
expect(results[0]?.metadata?.level1?.level2?.level3).toBe('deep value');
|
|
1641
|
-
});
|
|
1642
|
-
|
|
1643
|
-
it('should handle non-existent nested paths', async () => {
|
|
1644
|
-
const results = await vectorDB.query({
|
|
1645
|
-
indexName,
|
|
1646
|
-
queryVector: [1, 0, 0],
|
|
1647
|
-
filter: { 'nonexistent.path': 'value' },
|
|
1648
|
-
});
|
|
1649
|
-
expect(results).toHaveLength(0);
|
|
1650
|
-
});
|
|
1651
|
-
|
|
1652
|
-
// Score Threshold Tests
|
|
1653
|
-
it('should respect minimum score threshold', async () => {
|
|
1654
|
-
const results = await vectorDB.query({
|
|
1655
|
-
indexName,
|
|
1656
|
-
queryVector: [1, 0, 0],
|
|
1657
|
-
filter: { category: 'electronics' },
|
|
1658
|
-
includeVector: false,
|
|
1659
|
-
minScore: 0.9,
|
|
1660
|
-
});
|
|
1661
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1662
|
-
results.forEach(result => {
|
|
1663
|
-
expect(result.score).toBeGreaterThan(0.9);
|
|
1664
|
-
});
|
|
1665
|
-
});
|
|
1666
|
-
|
|
1667
|
-
// Complex Nested Operators Test
|
|
1668
|
-
it('should handle deeply nested logical operators', async () => {
|
|
1669
|
-
const results = await vectorDB.query({
|
|
1670
|
-
indexName,
|
|
1671
|
-
queryVector: [1, 0, 0],
|
|
1672
|
-
filter: {
|
|
1673
|
-
$and: [
|
|
1674
|
-
{
|
|
1675
|
-
$or: [{ category: 'electronics' }, { $and: [{ category: 'books' }, { price: { $lt: 30 } }] }],
|
|
1676
|
-
},
|
|
1677
|
-
{
|
|
1678
|
-
$not: {
|
|
1679
|
-
$or: [{ active: false }, { price: { $gt: 100 } }],
|
|
1680
|
-
},
|
|
1681
|
-
},
|
|
1682
|
-
],
|
|
1683
|
-
},
|
|
1684
|
-
});
|
|
1685
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1686
|
-
results.forEach(result => {
|
|
1687
|
-
// First condition: electronics OR (books AND price < 30)
|
|
1688
|
-
const firstCondition =
|
|
1689
|
-
result.metadata?.category === 'electronics' ||
|
|
1690
|
-
(result.metadata?.category === 'books' && result.metadata?.price < 30);
|
|
1691
|
-
|
|
1692
|
-
// Second condition: NOT (active = false OR price > 100)
|
|
1693
|
-
const secondCondition = result.metadata?.active !== false && result.metadata?.price <= 100;
|
|
1694
|
-
|
|
1695
|
-
expect(firstCondition && secondCondition).toBe(true);
|
|
1696
|
-
});
|
|
1697
|
-
});
|
|
1698
|
-
|
|
1699
|
-
it('should throw error for invalid operator', async () => {
|
|
1700
|
-
await expect(
|
|
1701
|
-
vectorDB.query({
|
|
1702
|
-
indexName,
|
|
1703
|
-
queryVector: [1, 0, 0],
|
|
1704
|
-
filter: { price: { $invalid: 100 } } as any,
|
|
1705
|
-
}),
|
|
1706
|
-
).rejects.toThrow('Unsupported operator: $invalid');
|
|
1707
|
-
});
|
|
1708
|
-
|
|
1709
|
-
it('should handle multiple logical operators at root level', async () => {
|
|
1710
|
-
const results = await vectorDB.query({
|
|
1711
|
-
indexName,
|
|
1712
|
-
queryVector: [1, 0, 0],
|
|
1713
|
-
filter: {
|
|
1714
|
-
$and: [{ category: 'electronics' }],
|
|
1715
|
-
$or: [{ price: { $lt: 100 } }, { price: { $gt: 20 } }],
|
|
1716
|
-
$nor: [],
|
|
1717
|
-
},
|
|
1718
|
-
});
|
|
1719
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1720
|
-
results.forEach(result => {
|
|
1721
|
-
expect(result.metadata?.category).toBe('electronics');
|
|
1722
|
-
expect(result.metadata?.price < 100 || result.metadata?.price > 20).toBe(true);
|
|
1723
|
-
});
|
|
1724
|
-
});
|
|
1725
|
-
|
|
1726
|
-
it('should handle non-array field with $elemMatch', async () => {
|
|
1727
|
-
// First insert a record with non-array field
|
|
1728
|
-
await vectorDB.upsert({
|
|
1729
|
-
indexName,
|
|
1730
|
-
vectors: [[1, 0.1, 0]],
|
|
1731
|
-
metadata: [{ tags: 'not-an-array' }],
|
|
1732
|
-
});
|
|
1733
|
-
|
|
1734
|
-
const results = await vectorDB.query({
|
|
1735
|
-
indexName,
|
|
1736
|
-
queryVector: [1, 0, 0],
|
|
1737
|
-
filter: { tags: { $elemMatch: { $eq: 'value' } } },
|
|
1738
|
-
});
|
|
1739
|
-
expect(results).toHaveLength(0); // Should return no results for non-array field
|
|
1740
|
-
});
|
|
1741
|
-
it('should handle undefined filter', async () => {
|
|
1742
|
-
const results1 = await vectorDB.query({
|
|
1743
|
-
indexName,
|
|
1744
|
-
queryVector: [1, 0, 0],
|
|
1745
|
-
filter: undefined,
|
|
1746
|
-
});
|
|
1747
|
-
const results2 = await vectorDB.query({
|
|
1748
|
-
indexName,
|
|
1749
|
-
queryVector: [1, 0, 0],
|
|
1750
|
-
});
|
|
1751
|
-
expect(results1).toEqual(results2);
|
|
1752
|
-
expect(results1.length).toBeGreaterThan(0);
|
|
1753
|
-
});
|
|
1754
|
-
|
|
1755
|
-
it('should handle empty object filter', async () => {
|
|
1756
|
-
const results = await vectorDB.query({
|
|
1757
|
-
indexName,
|
|
1758
|
-
queryVector: [1, 0, 0],
|
|
1759
|
-
filter: {},
|
|
1760
|
-
});
|
|
1761
|
-
const results2 = await vectorDB.query({
|
|
1762
|
-
indexName,
|
|
1763
|
-
queryVector: [1, 0, 0],
|
|
1764
|
-
});
|
|
1765
|
-
expect(results).toEqual(results2);
|
|
1766
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1767
|
-
});
|
|
1768
|
-
|
|
1769
|
-
it('should handle null filter', async () => {
|
|
1770
|
-
const results = await vectorDB.query({
|
|
1771
|
-
indexName,
|
|
1772
|
-
queryVector: [1, 0, 0],
|
|
1773
|
-
filter: null,
|
|
1774
|
-
});
|
|
1775
|
-
const results2 = await vectorDB.query({
|
|
1776
|
-
indexName,
|
|
1777
|
-
queryVector: [1, 0, 0],
|
|
1778
|
-
});
|
|
1779
|
-
expect(results).toEqual(results2);
|
|
1780
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1781
|
-
});
|
|
1782
|
-
});
|
|
1783
|
-
|
|
1784
|
-
describe('PgVector Table Name Quoting', () => {
|
|
1785
|
-
const camelCaseIndex = 'TestCamelCaseIndex';
|
|
1786
|
-
const snakeCaseIndex = 'test_snake_case_index';
|
|
1787
|
-
|
|
1788
|
-
beforeEach(async () => {
|
|
1789
|
-
// Clean up any existing indexes
|
|
1790
|
-
try {
|
|
1791
|
-
await vectorDB.deleteIndex({ indexName: camelCaseIndex });
|
|
1792
|
-
} catch {
|
|
1793
|
-
// Ignore if doesn't exist
|
|
1794
|
-
}
|
|
1795
|
-
try {
|
|
1796
|
-
await vectorDB.deleteIndex({ indexName: snakeCaseIndex });
|
|
1797
|
-
} catch {
|
|
1798
|
-
// Ignore if doesn't exist
|
|
1799
|
-
}
|
|
1800
|
-
});
|
|
1801
|
-
|
|
1802
|
-
afterEach(async () => {
|
|
1803
|
-
// Clean up indexes after each test
|
|
1804
|
-
try {
|
|
1805
|
-
await vectorDB.deleteIndex({ indexName: camelCaseIndex });
|
|
1806
|
-
} catch {
|
|
1807
|
-
// Ignore if doesn't exist
|
|
1808
|
-
}
|
|
1809
|
-
try {
|
|
1810
|
-
await vectorDB.deleteIndex({ indexName: snakeCaseIndex });
|
|
1811
|
-
} catch {
|
|
1812
|
-
// Ignore if doesn't exist
|
|
1813
|
-
}
|
|
1814
|
-
});
|
|
1815
|
-
|
|
1816
|
-
it('should create and query a camelCase index without quoting errors', async () => {
|
|
1817
|
-
await expect(
|
|
1818
|
-
vectorDB.createIndex({
|
|
1819
|
-
indexName: camelCaseIndex,
|
|
1820
|
-
dimension: 3,
|
|
1821
|
-
metric: 'cosine',
|
|
1822
|
-
indexConfig: { type: 'hnsw' },
|
|
1823
|
-
}),
|
|
1824
|
-
).resolves.not.toThrow();
|
|
1825
|
-
|
|
1826
|
-
const results = await vectorDB.query({
|
|
1827
|
-
indexName: camelCaseIndex,
|
|
1828
|
-
queryVector: [1, 0, 0],
|
|
1829
|
-
topK: 1,
|
|
1830
|
-
});
|
|
1831
|
-
expect(Array.isArray(results)).toBe(true);
|
|
1832
|
-
});
|
|
1833
|
-
|
|
1834
|
-
it('should create and query a snake_case index without quoting errors', async () => {
|
|
1835
|
-
await expect(
|
|
1836
|
-
vectorDB.createIndex({
|
|
1837
|
-
indexName: snakeCaseIndex,
|
|
1838
|
-
dimension: 3,
|
|
1839
|
-
metric: 'cosine',
|
|
1840
|
-
indexConfig: { type: 'hnsw' },
|
|
1841
|
-
}),
|
|
1842
|
-
).resolves.not.toThrow();
|
|
1843
|
-
|
|
1844
|
-
const results = await vectorDB.query({
|
|
1845
|
-
indexName: snakeCaseIndex,
|
|
1846
|
-
queryVector: [1, 0, 0],
|
|
1847
|
-
topK: 1,
|
|
1848
|
-
});
|
|
1849
|
-
expect(Array.isArray(results)).toBe(true);
|
|
1850
|
-
});
|
|
1851
|
-
});
|
|
1852
|
-
|
|
1853
|
-
// Regex Operator Tests
|
|
1854
|
-
describe('Regex Operators', () => {
|
|
1855
|
-
it('should handle $regex with case sensitivity', async () => {
|
|
1856
|
-
const results = await vectorDB.query({
|
|
1857
|
-
indexName,
|
|
1858
|
-
queryVector: [1, 0, 0],
|
|
1859
|
-
filter: { category: { $regex: 'ELECTRONICS' } },
|
|
1860
|
-
});
|
|
1861
|
-
expect(results).toHaveLength(0);
|
|
1862
|
-
});
|
|
1863
|
-
|
|
1864
|
-
it('should handle $regex with case insensitivity', async () => {
|
|
1865
|
-
const results = await vectorDB.query({
|
|
1866
|
-
indexName,
|
|
1867
|
-
queryVector: [1, 0, 0],
|
|
1868
|
-
filter: { category: { $regex: 'ELECTRONICS', $options: 'i' } },
|
|
1869
|
-
});
|
|
1870
|
-
expect(results).toHaveLength(2);
|
|
1871
|
-
});
|
|
1872
|
-
|
|
1873
|
-
it('should handle $regex with start anchor', async () => {
|
|
1874
|
-
const results = await vectorDB.query({
|
|
1875
|
-
indexName,
|
|
1876
|
-
queryVector: [1, 0, 0],
|
|
1877
|
-
filter: { category: { $regex: '^elect' } },
|
|
1878
|
-
});
|
|
1879
|
-
expect(results).toHaveLength(2);
|
|
1880
|
-
});
|
|
1881
|
-
|
|
1882
|
-
it('should handle $regex with end anchor', async () => {
|
|
1883
|
-
const results = await vectorDB.query({
|
|
1884
|
-
indexName,
|
|
1885
|
-
queryVector: [1, 0, 0],
|
|
1886
|
-
filter: { category: { $regex: 'nics$' } },
|
|
1887
|
-
});
|
|
1888
|
-
expect(results).toHaveLength(2);
|
|
1889
|
-
});
|
|
1890
|
-
|
|
1891
|
-
it('should handle multiline flag', async () => {
|
|
1892
|
-
await vectorDB.upsert({
|
|
1893
|
-
indexName,
|
|
1894
|
-
vectors: [[1, 0.1, 0]],
|
|
1895
|
-
metadata: [{ description: 'First line\nSecond line\nThird line' }],
|
|
1896
|
-
});
|
|
1897
|
-
|
|
1898
|
-
const results = await vectorDB.query({
|
|
1899
|
-
indexName,
|
|
1900
|
-
queryVector: [1, 0, 0],
|
|
1901
|
-
filter: { description: { $regex: '^Second', $options: 'm' } },
|
|
1902
|
-
});
|
|
1903
|
-
expect(results).toHaveLength(1);
|
|
1904
|
-
});
|
|
1905
|
-
|
|
1906
|
-
it('should handle dotall flag', async () => {
|
|
1907
|
-
await vectorDB.upsert({
|
|
1908
|
-
indexName,
|
|
1909
|
-
vectors: [[1, 0.1, 0]],
|
|
1910
|
-
metadata: [{ description: 'First\nSecond\nThird' }],
|
|
1911
|
-
});
|
|
1912
|
-
|
|
1913
|
-
const withoutS = await vectorDB.query({
|
|
1914
|
-
indexName,
|
|
1915
|
-
queryVector: [1, 0, 0],
|
|
1916
|
-
filter: { description: { $regex: 'First[^\\n]*Third' } },
|
|
1917
|
-
});
|
|
1918
|
-
expect(withoutS).toHaveLength(0);
|
|
1919
|
-
|
|
1920
|
-
const withS = await vectorDB.query({
|
|
1921
|
-
indexName,
|
|
1922
|
-
queryVector: [1, 0, 0],
|
|
1923
|
-
filter: { description: { $regex: 'First.*Third', $options: 's' } },
|
|
1924
|
-
});
|
|
1925
|
-
expect(withS).toHaveLength(1);
|
|
1926
|
-
});
|
|
1927
|
-
it('should handle $not with $regex operator', async () => {
|
|
1928
|
-
const results = await vectorDB.query({
|
|
1929
|
-
indexName,
|
|
1930
|
-
queryVector: [1, 0, 0],
|
|
1931
|
-
filter: { category: { $not: { $regex: '^elect' } } },
|
|
1932
|
-
});
|
|
1933
|
-
expect(results.length).toBeGreaterThan(0);
|
|
1934
|
-
results.forEach(result => {
|
|
1935
|
-
expect(result.metadata?.category).not.toMatch(/^elect/);
|
|
1936
|
-
});
|
|
1937
|
-
});
|
|
1938
|
-
});
|
|
1939
|
-
});
|
|
1940
|
-
|
|
1941
|
-
describe('Search Parameters', () => {
|
|
1942
|
-
const indexName = 'test_search_params';
|
|
1943
|
-
const vectors = [
|
|
1944
|
-
[1, 0, 0], // Query vector will be closest to this
|
|
1945
|
-
[0.8, 0.2, 0], // Second closest
|
|
1946
|
-
[0, 1, 0], // Third (much further)
|
|
1947
|
-
];
|
|
1948
|
-
|
|
1949
|
-
describe('HNSW Parameters', () => {
|
|
1950
|
-
beforeAll(async () => {
|
|
1951
|
-
await vectorDB.createIndex({
|
|
1952
|
-
indexName,
|
|
1953
|
-
dimension: 3,
|
|
1954
|
-
metric: 'cosine',
|
|
1955
|
-
indexConfig: {
|
|
1956
|
-
type: 'hnsw',
|
|
1957
|
-
hnsw: { m: 16, efConstruction: 64 },
|
|
1958
|
-
},
|
|
1959
|
-
});
|
|
1960
|
-
await vectorDB.upsert({
|
|
1961
|
-
indexName,
|
|
1962
|
-
vectors,
|
|
1963
|
-
});
|
|
1964
|
-
});
|
|
1965
|
-
|
|
1966
|
-
afterAll(async () => {
|
|
1967
|
-
await vectorDB.deleteIndex({ indexName });
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
it('should use default ef value', async () => {
|
|
1971
|
-
const results = await vectorDB.query({
|
|
1972
|
-
indexName,
|
|
1973
|
-
queryVector: [1, 0, 0],
|
|
1974
|
-
topK: 2,
|
|
1975
|
-
});
|
|
1976
|
-
expect(results).toHaveLength(2);
|
|
1977
|
-
expect(results[0]?.score).toBeCloseTo(1, 5);
|
|
1978
|
-
expect(results[1]?.score).toBeGreaterThan(0.9); // Second vector should be close
|
|
1979
|
-
});
|
|
1980
|
-
|
|
1981
|
-
it('should respect custom ef value', async () => {
|
|
1982
|
-
const results = await vectorDB.query({
|
|
1983
|
-
indexName,
|
|
1984
|
-
queryVector: [1, 0, 0],
|
|
1985
|
-
topK: 2,
|
|
1986
|
-
ef: 100,
|
|
1987
|
-
});
|
|
1988
|
-
expect(results).toHaveLength(2);
|
|
1989
|
-
expect(results[0]?.score).toBeCloseTo(1, 5);
|
|
1990
|
-
expect(results[1]?.score).toBeGreaterThan(0.9);
|
|
1991
|
-
});
|
|
1992
|
-
|
|
1993
|
-
// NEW TEST: Reproduce the SET LOCAL bug
|
|
1994
|
-
it('should verify that ef_search parameter is actually being set (reproduces SET LOCAL bug)', async () => {
|
|
1995
|
-
const client = await vectorDB.pool.connect();
|
|
1996
|
-
try {
|
|
1997
|
-
// Test current behavior: SET LOCAL without transaction should have no effect
|
|
1998
|
-
await client.query('SET LOCAL hnsw.ef_search = 500');
|
|
1999
|
-
|
|
2000
|
-
// Check if the parameter was actually set
|
|
2001
|
-
const result = await client.query('SHOW hnsw.ef_search');
|
|
2002
|
-
const currentValue = result.rows[0]['hnsw.ef_search'];
|
|
2003
|
-
|
|
2004
|
-
// The value should still be the default (not 500)
|
|
2005
|
-
expect(parseInt(currentValue)).not.toBe(500);
|
|
2006
|
-
|
|
2007
|
-
// Now test with proper transaction
|
|
2008
|
-
await client.query('BEGIN');
|
|
2009
|
-
await client.query('SET LOCAL hnsw.ef_search = 500');
|
|
2010
|
-
|
|
2011
|
-
const resultInTransaction = await client.query('SHOW hnsw.ef_search');
|
|
2012
|
-
const valueInTransaction = resultInTransaction.rows[0]['hnsw.ef_search'];
|
|
2013
|
-
|
|
2014
|
-
// This should work because we're in a transaction
|
|
2015
|
-
expect(parseInt(valueInTransaction)).toBe(500);
|
|
2016
|
-
|
|
2017
|
-
await client.query('ROLLBACK');
|
|
2018
|
-
|
|
2019
|
-
// After rollback, should return to default
|
|
2020
|
-
const resultAfterRollback = await client.query('SHOW hnsw.ef_search');
|
|
2021
|
-
const valueAfterRollback = resultAfterRollback.rows[0]['hnsw.ef_search'];
|
|
2022
|
-
expect(parseInt(valueAfterRollback)).not.toBe(500);
|
|
2023
|
-
} finally {
|
|
2024
|
-
client.release();
|
|
2025
|
-
}
|
|
2026
|
-
});
|
|
2027
|
-
|
|
2028
|
-
// Verify the fix works - ef parameter is properly applied in query method
|
|
2029
|
-
it('should properly apply ef parameter using transactions (verifies fix)', async () => {
|
|
2030
|
-
const client = await vectorDB.pool.connect();
|
|
2031
|
-
const queryCommands: string[] = [];
|
|
2032
|
-
|
|
2033
|
-
// Spy on the client query method to capture all SQL commands
|
|
2034
|
-
const originalClientQuery = client.query;
|
|
2035
|
-
const clientQuerySpy = vi.fn().mockImplementation((query, ...args) => {
|
|
2036
|
-
if (typeof query === 'string') {
|
|
2037
|
-
queryCommands.push(query);
|
|
2038
|
-
}
|
|
2039
|
-
return originalClientQuery.call(client, query, ...args);
|
|
2040
|
-
});
|
|
2041
|
-
client.query = clientQuerySpy;
|
|
2042
|
-
|
|
2043
|
-
try {
|
|
2044
|
-
// Manually release the client so query() can get a fresh one
|
|
2045
|
-
client.release();
|
|
2046
|
-
|
|
2047
|
-
await vectorDB.query({
|
|
2048
|
-
indexName,
|
|
2049
|
-
queryVector: [1, 0, 0],
|
|
2050
|
-
topK: 2,
|
|
2051
|
-
ef: 128,
|
|
2052
|
-
});
|
|
2053
|
-
|
|
2054
|
-
const testClient = await vectorDB.pool.connect();
|
|
2055
|
-
try {
|
|
2056
|
-
// Test that SET LOCAL works within a transaction
|
|
2057
|
-
await testClient.query('BEGIN');
|
|
2058
|
-
await testClient.query('SET LOCAL hnsw.ef_search = 256');
|
|
2059
|
-
|
|
2060
|
-
const result = await testClient.query('SHOW hnsw.ef_search');
|
|
2061
|
-
const value = result.rows[0]['hnsw.ef_search'];
|
|
2062
|
-
expect(parseInt(value)).toBe(256);
|
|
2063
|
-
|
|
2064
|
-
await testClient.query('ROLLBACK');
|
|
2065
|
-
|
|
2066
|
-
// After rollback, should revert
|
|
2067
|
-
const resultAfter = await testClient.query('SHOW hnsw.ef_search');
|
|
2068
|
-
const valueAfter = resultAfter.rows[0]['hnsw.ef_search'];
|
|
2069
|
-
expect(parseInt(valueAfter)).not.toBe(256);
|
|
2070
|
-
} finally {
|
|
2071
|
-
testClient.release();
|
|
2072
|
-
}
|
|
2073
|
-
} finally {
|
|
2074
|
-
// Restore original function if client is still connected
|
|
2075
|
-
if (client.query === clientQuerySpy) {
|
|
2076
|
-
client.query = originalClientQuery;
|
|
2077
|
-
}
|
|
2078
|
-
clientQuerySpy.mockRestore();
|
|
2079
|
-
}
|
|
2080
|
-
});
|
|
2081
|
-
});
|
|
2082
|
-
|
|
2083
|
-
describe('IVF Parameters', () => {
|
|
2084
|
-
beforeAll(async () => {
|
|
2085
|
-
await vectorDB.createIndex({
|
|
2086
|
-
indexName,
|
|
2087
|
-
dimension: 3,
|
|
2088
|
-
metric: 'cosine',
|
|
2089
|
-
indexConfig: {
|
|
2090
|
-
type: 'ivfflat',
|
|
2091
|
-
ivf: { lists: 2 }, // Small number for test data
|
|
2092
|
-
},
|
|
2093
|
-
});
|
|
2094
|
-
await vectorDB.upsert({
|
|
2095
|
-
indexName,
|
|
2096
|
-
vectors,
|
|
2097
|
-
});
|
|
2098
|
-
});
|
|
2099
|
-
|
|
2100
|
-
afterAll(async () => {
|
|
2101
|
-
await vectorDB.deleteIndex({ indexName });
|
|
2102
|
-
});
|
|
2103
|
-
|
|
2104
|
-
it('should use default probe value', async () => {
|
|
2105
|
-
const results = await vectorDB.query({
|
|
2106
|
-
indexName,
|
|
2107
|
-
queryVector: [1, 0, 0],
|
|
2108
|
-
topK: 2,
|
|
2109
|
-
});
|
|
2110
|
-
expect(results).toHaveLength(2);
|
|
2111
|
-
expect(results[0]?.score).toBeCloseTo(1, 5);
|
|
2112
|
-
expect(results[1]?.score).toBeGreaterThan(0.9);
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
it('should respect custom probe value', async () => {
|
|
2116
|
-
const results = await vectorDB.query({
|
|
2117
|
-
indexName,
|
|
2118
|
-
queryVector: [1, 0, 0],
|
|
2119
|
-
topK: 2,
|
|
2120
|
-
probes: 2,
|
|
2121
|
-
});
|
|
2122
|
-
expect(results).toHaveLength(2);
|
|
2123
|
-
expect(results[0]?.score).toBeCloseTo(1, 5);
|
|
2124
|
-
expect(results[1]?.score).toBeGreaterThan(0.9);
|
|
2125
|
-
});
|
|
2126
|
-
});
|
|
2127
|
-
});
|
|
2128
|
-
|
|
2129
|
-
describe('Concurrent Operations', () => {
|
|
2130
|
-
it('should handle concurrent index creation attempts', async () => {
|
|
2131
|
-
const indexName = 'concurrent_test_index';
|
|
2132
|
-
const dimension = 384;
|
|
2133
|
-
|
|
2134
|
-
// Create multiple promises trying to create the same index
|
|
2135
|
-
const promises = Array(5)
|
|
2136
|
-
.fill(null)
|
|
2137
|
-
.map(() => vectorDB.createIndex({ indexName, dimension }));
|
|
2138
|
-
|
|
2139
|
-
// All should resolve without error - subsequent attempts should be no-ops
|
|
2140
|
-
await expect(Promise.all(promises)).resolves.not.toThrow();
|
|
2141
|
-
|
|
2142
|
-
// Verify only one index was actually created
|
|
2143
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
2144
|
-
expect(stats.dimension).toBe(dimension);
|
|
2145
|
-
|
|
2146
|
-
await vectorDB.deleteIndex({ indexName });
|
|
2147
|
-
});
|
|
2148
|
-
|
|
2149
|
-
it('should handle concurrent buildIndex attempts', async () => {
|
|
2150
|
-
const indexName = 'concurrent_build_test';
|
|
2151
|
-
await vectorDB.createIndex({ indexName, dimension: 384 });
|
|
2152
|
-
|
|
2153
|
-
const promises = Array(5)
|
|
2154
|
-
.fill(null)
|
|
2155
|
-
.map(() =>
|
|
2156
|
-
vectorDB.buildIndex({
|
|
2157
|
-
indexName,
|
|
2158
|
-
metric: 'cosine',
|
|
2159
|
-
indexConfig: { type: 'ivfflat', ivf: { lists: 100 } },
|
|
2160
|
-
}),
|
|
2161
|
-
);
|
|
2162
|
-
|
|
2163
|
-
await expect(Promise.all(promises)).resolves.not.toThrow();
|
|
2164
|
-
|
|
2165
|
-
const stats = await vectorDB.describeIndex({ indexName });
|
|
2166
|
-
expect(stats.type).toBe('ivfflat');
|
|
2167
|
-
|
|
2168
|
-
await vectorDB.deleteIndex({ indexName });
|
|
2169
|
-
});
|
|
2170
|
-
});
|
|
2171
|
-
|
|
2172
|
-
describe('Schema Support', () => {
|
|
2173
|
-
const customSchema = 'mastraTest';
|
|
2174
|
-
let vectorDB: PgVector;
|
|
2175
|
-
let customSchemaVectorDB: PgVector;
|
|
2176
|
-
|
|
2177
|
-
beforeAll(async () => {
|
|
2178
|
-
// Initialize default vectorDB first
|
|
2179
|
-
vectorDB = new PgVector({ connectionString });
|
|
2180
|
-
|
|
2181
|
-
// Create schema using the default vectorDB connection
|
|
2182
|
-
const client = await vectorDB['pool'].connect();
|
|
2183
|
-
try {
|
|
2184
|
-
await client.query(`CREATE SCHEMA IF NOT EXISTS ${customSchema}`);
|
|
2185
|
-
await client.query('COMMIT');
|
|
2186
|
-
} catch (e) {
|
|
2187
|
-
await client.query('ROLLBACK');
|
|
2188
|
-
throw e;
|
|
2189
|
-
} finally {
|
|
2190
|
-
client.release();
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
// Now create the custom schema vectorDB instance
|
|
2194
|
-
customSchemaVectorDB = new PgVector({
|
|
2195
|
-
connectionString,
|
|
2196
|
-
schemaName: customSchema,
|
|
2197
|
-
});
|
|
2198
|
-
});
|
|
2199
|
-
|
|
2200
|
-
afterAll(async () => {
|
|
2201
|
-
// Clean up test tables and schema
|
|
2202
|
-
try {
|
|
2203
|
-
await customSchemaVectorDB.deleteIndex({ indexName: 'schema_test_vectors' });
|
|
2204
|
-
} catch {
|
|
2205
|
-
// Ignore errors if index doesn't exist
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
// Drop schema using the default vectorDB connection
|
|
2209
|
-
const client = await vectorDB['pool'].connect();
|
|
2210
|
-
try {
|
|
2211
|
-
await client.query(`DROP SCHEMA IF EXISTS ${customSchema} CASCADE`);
|
|
2212
|
-
await client.query('COMMIT');
|
|
2213
|
-
} catch (e) {
|
|
2214
|
-
await client.query('ROLLBACK');
|
|
2215
|
-
throw e;
|
|
2216
|
-
} finally {
|
|
2217
|
-
client.release();
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
// Disconnect in reverse order
|
|
2221
|
-
await customSchemaVectorDB.disconnect();
|
|
2222
|
-
await vectorDB.disconnect();
|
|
2223
|
-
});
|
|
2224
|
-
|
|
2225
|
-
describe('Constructor', () => {
|
|
2226
|
-
it('should accept config object with connectionString', () => {
|
|
2227
|
-
const db = new PgVector({ connectionString });
|
|
2228
|
-
expect(db).toBeInstanceOf(PgVector);
|
|
2229
|
-
});
|
|
2230
|
-
|
|
2231
|
-
it('should accept config object with schema', () => {
|
|
2232
|
-
const db = new PgVector({ connectionString, schemaName: customSchema });
|
|
2233
|
-
expect(db).toBeInstanceOf(PgVector);
|
|
2234
|
-
});
|
|
2235
|
-
});
|
|
2236
|
-
|
|
2237
|
-
describe('Schema Operations', () => {
|
|
2238
|
-
const testIndexName = 'schema_test_vectors';
|
|
2239
|
-
|
|
2240
|
-
beforeEach(async () => {
|
|
2241
|
-
// Clean up any existing indexes
|
|
2242
|
-
try {
|
|
2243
|
-
await customSchemaVectorDB.deleteIndex({ indexName: testIndexName });
|
|
2244
|
-
} catch {
|
|
2245
|
-
// Ignore if doesn't exist
|
|
2246
|
-
}
|
|
2247
|
-
try {
|
|
2248
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
2249
|
-
} catch {
|
|
2250
|
-
// Ignore if doesn't exist
|
|
2251
|
-
}
|
|
2252
|
-
});
|
|
2253
|
-
|
|
2254
|
-
afterEach(async () => {
|
|
2255
|
-
// Clean up indexes after each test
|
|
2256
|
-
try {
|
|
2257
|
-
await customSchemaVectorDB.deleteIndex({ indexName: testIndexName });
|
|
2258
|
-
} catch {
|
|
2259
|
-
// Ignore if doesn't exist
|
|
2260
|
-
}
|
|
2261
|
-
try {
|
|
2262
|
-
await vectorDB.deleteIndex({ indexName: testIndexName });
|
|
2263
|
-
} catch {
|
|
2264
|
-
// Ignore if doesn't exist
|
|
2265
|
-
}
|
|
2266
|
-
});
|
|
2267
|
-
|
|
2268
|
-
it('should create and query index in custom schema', async () => {
|
|
2269
|
-
// Create index in custom schema
|
|
2270
|
-
await customSchemaVectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
2271
|
-
|
|
2272
|
-
// Insert test vectors
|
|
2273
|
-
const vectors = [
|
|
2274
|
-
[1, 2, 3],
|
|
2275
|
-
[4, 5, 6],
|
|
2276
|
-
];
|
|
2277
|
-
const metadata = [{ test: 'custom_schema_1' }, { test: 'custom_schema_2' }];
|
|
2278
|
-
await customSchemaVectorDB.upsert({ indexName: testIndexName, vectors, metadata });
|
|
2279
|
-
|
|
2280
|
-
// Query and verify results
|
|
2281
|
-
const results = await customSchemaVectorDB.query({
|
|
2282
|
-
indexName: testIndexName,
|
|
2283
|
-
queryVector: [1, 2, 3],
|
|
2284
|
-
topK: 2,
|
|
2285
|
-
});
|
|
2286
|
-
expect(results).toHaveLength(2);
|
|
2287
|
-
expect(results[0]?.metadata?.test).toMatch(/custom_schema_/);
|
|
2288
|
-
|
|
2289
|
-
// Verify table exists in correct schema
|
|
2290
|
-
const client = await customSchemaVectorDB['pool'].connect();
|
|
2291
|
-
try {
|
|
2292
|
-
const res = await client.query(
|
|
2293
|
-
`
|
|
2294
|
-
SELECT EXISTS (
|
|
2295
|
-
SELECT FROM information_schema.tables
|
|
2296
|
-
WHERE table_schema = $1
|
|
2297
|
-
AND table_name = $2
|
|
2298
|
-
)`,
|
|
2299
|
-
[customSchema, testIndexName],
|
|
2300
|
-
);
|
|
2301
|
-
expect(res.rows[0].exists).toBe(true);
|
|
2302
|
-
} finally {
|
|
2303
|
-
client.release();
|
|
2304
|
-
}
|
|
2305
|
-
});
|
|
2306
|
-
|
|
2307
|
-
it('should describe index in custom schema', async () => {
|
|
2308
|
-
// Create index in custom schema
|
|
2309
|
-
await customSchemaVectorDB.createIndex({
|
|
2310
|
-
indexName: testIndexName,
|
|
2311
|
-
dimension: 3,
|
|
2312
|
-
metric: 'dotproduct',
|
|
2313
|
-
indexConfig: { type: 'hnsw' },
|
|
2314
|
-
});
|
|
2315
|
-
// Insert a vector
|
|
2316
|
-
await customSchemaVectorDB.upsert({ indexName: testIndexName, vectors: [[1, 2, 3]] });
|
|
2317
|
-
// Describe the index
|
|
2318
|
-
const stats = await customSchemaVectorDB.describeIndex({ indexName: testIndexName });
|
|
2319
|
-
expect(stats).toMatchObject({
|
|
2320
|
-
dimension: 3,
|
|
2321
|
-
metric: 'dotproduct',
|
|
2322
|
-
type: 'hnsw',
|
|
2323
|
-
count: 1,
|
|
2324
|
-
});
|
|
2325
|
-
});
|
|
2326
|
-
|
|
2327
|
-
it('should allow same index name in different schemas', async () => {
|
|
2328
|
-
// Create same index name in both schemas
|
|
2329
|
-
await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
2330
|
-
await customSchemaVectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
2331
|
-
|
|
2332
|
-
// Insert different test data in each schema
|
|
2333
|
-
await vectorDB.upsert({
|
|
2334
|
-
indexName: testIndexName,
|
|
2335
|
-
vectors: [[1, 2, 3]],
|
|
2336
|
-
metadata: [{ test: 'default_schema' }],
|
|
2337
|
-
});
|
|
2338
|
-
|
|
2339
|
-
await customSchemaVectorDB.upsert({
|
|
2340
|
-
indexName: testIndexName,
|
|
2341
|
-
vectors: [[1, 2, 3]],
|
|
2342
|
-
metadata: [{ test: 'custom_schema' }],
|
|
2343
|
-
});
|
|
2344
|
-
|
|
2345
|
-
// Query both schemas and verify different results
|
|
2346
|
-
const defaultResults = await vectorDB.query({
|
|
2347
|
-
indexName: testIndexName,
|
|
2348
|
-
queryVector: [1, 2, 3],
|
|
2349
|
-
topK: 1,
|
|
2350
|
-
});
|
|
2351
|
-
const customResults = await customSchemaVectorDB.query({
|
|
2352
|
-
indexName: testIndexName,
|
|
2353
|
-
queryVector: [1, 2, 3],
|
|
2354
|
-
topK: 1,
|
|
2355
|
-
});
|
|
2356
|
-
|
|
2357
|
-
expect(defaultResults[0]?.metadata?.test).toBe('default_schema');
|
|
2358
|
-
expect(customResults[0]?.metadata?.test).toBe('custom_schema');
|
|
2359
|
-
});
|
|
2360
|
-
|
|
2361
|
-
it('should maintain schema separation for all operations', async () => {
|
|
2362
|
-
// Create index in custom schema
|
|
2363
|
-
await customSchemaVectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
|
|
2364
|
-
|
|
2365
|
-
// Test index operations
|
|
2366
|
-
const stats = await customSchemaVectorDB.describeIndex({ indexName: testIndexName });
|
|
2367
|
-
expect(stats.dimension).toBe(3);
|
|
2368
|
-
|
|
2369
|
-
// Test list operation
|
|
2370
|
-
const indexes = await customSchemaVectorDB.listIndexes();
|
|
2371
|
-
expect(indexes).toContain(testIndexName);
|
|
2372
|
-
|
|
2373
|
-
// Test update operation
|
|
2374
|
-
const vectors = [[7, 8, 9]];
|
|
2375
|
-
const metadata = [{ test: 'updated_in_custom_schema' }];
|
|
2376
|
-
const [id] = await customSchemaVectorDB.upsert({
|
|
2377
|
-
indexName: testIndexName,
|
|
2378
|
-
vectors,
|
|
2379
|
-
metadata,
|
|
2380
|
-
});
|
|
2381
|
-
|
|
2382
|
-
// Test delete operation
|
|
2383
|
-
await customSchemaVectorDB.deleteVector({ indexName: testIndexName, id: id! });
|
|
2384
|
-
|
|
2385
|
-
// Verify deletion
|
|
2386
|
-
const results = await customSchemaVectorDB.query({
|
|
2387
|
-
indexName: testIndexName,
|
|
2388
|
-
queryVector: [7, 8, 9],
|
|
2389
|
-
topK: 1,
|
|
2390
|
-
});
|
|
2391
|
-
expect(results).toHaveLength(0);
|
|
2392
|
-
});
|
|
2393
|
-
});
|
|
2394
|
-
});
|
|
2395
|
-
|
|
2396
|
-
describe('Permission Handling', () => {
|
|
2397
|
-
const schemaRestrictedUser = 'mastra_schema_restricted';
|
|
2398
|
-
const vectorRestrictedUser = 'mastra_vector_restricted';
|
|
2399
|
-
const restrictedPassword = 'test123';
|
|
2400
|
-
const testSchema = 'test_schema';
|
|
2401
|
-
|
|
2402
|
-
const getConnectionString = (username: string) =>
|
|
2403
|
-
connectionString.replace(/(postgresql:\/\/)[^:]+:[^@]+@/, `$1${username}:${restrictedPassword}@`);
|
|
2404
|
-
|
|
2405
|
-
beforeAll(async () => {
|
|
2406
|
-
// First ensure the test schema doesn't exist from previous runs
|
|
2407
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2408
|
-
try {
|
|
2409
|
-
await adminClient.query('BEGIN');
|
|
2410
|
-
|
|
2411
|
-
// Drop the test schema if it exists from previous runs
|
|
2412
|
-
await adminClient.query(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
|
|
2413
|
-
|
|
2414
|
-
// Create schema restricted user with minimal permissions
|
|
2415
|
-
await adminClient.query(`
|
|
2416
|
-
DO $$
|
|
2417
|
-
BEGIN
|
|
2418
|
-
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${schemaRestrictedUser}') THEN
|
|
2419
|
-
CREATE USER ${schemaRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
|
|
2420
|
-
END IF;
|
|
2421
|
-
END
|
|
2422
|
-
$$;
|
|
2423
|
-
`);
|
|
2424
|
-
|
|
2425
|
-
// Grant only connect and usage to schema restricted user
|
|
2426
|
-
await adminClient.query(`
|
|
2427
|
-
REVOKE ALL ON DATABASE ${connectionString.split('/').pop()} FROM ${schemaRestrictedUser};
|
|
2428
|
-
GRANT CONNECT ON DATABASE ${connectionString.split('/').pop()} TO ${schemaRestrictedUser};
|
|
2429
|
-
REVOKE ALL ON SCHEMA public FROM ${schemaRestrictedUser};
|
|
2430
|
-
GRANT USAGE ON SCHEMA public TO ${schemaRestrictedUser};
|
|
2431
|
-
`);
|
|
2432
|
-
|
|
2433
|
-
// Create vector restricted user with table creation permissions
|
|
2434
|
-
await adminClient.query(`
|
|
2435
|
-
DO $$
|
|
2436
|
-
BEGIN
|
|
2437
|
-
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${vectorRestrictedUser}') THEN
|
|
2438
|
-
CREATE USER ${vectorRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
|
|
2439
|
-
END IF;
|
|
2440
|
-
END
|
|
2441
|
-
$$;
|
|
2442
|
-
`);
|
|
2443
|
-
|
|
2444
|
-
// Grant connect, usage, and create to vector restricted user
|
|
2445
|
-
await adminClient.query(`
|
|
2446
|
-
REVOKE ALL ON DATABASE ${connectionString.split('/').pop()} FROM ${vectorRestrictedUser};
|
|
2447
|
-
GRANT CONNECT ON DATABASE ${connectionString.split('/').pop()} TO ${vectorRestrictedUser};
|
|
2448
|
-
REVOKE ALL ON SCHEMA public FROM ${vectorRestrictedUser};
|
|
2449
|
-
GRANT USAGE, CREATE ON SCHEMA public TO ${vectorRestrictedUser};
|
|
2450
|
-
`);
|
|
2451
|
-
|
|
2452
|
-
await adminClient.query('COMMIT');
|
|
2453
|
-
} catch (e) {
|
|
2454
|
-
await adminClient.query('ROLLBACK');
|
|
2455
|
-
throw e;
|
|
2456
|
-
} finally {
|
|
2457
|
-
adminClient.release();
|
|
2458
|
-
}
|
|
2459
|
-
});
|
|
2460
|
-
|
|
2461
|
-
afterAll(async () => {
|
|
2462
|
-
// Clean up test users and any objects they own
|
|
2463
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2464
|
-
try {
|
|
2465
|
-
await adminClient.query('BEGIN');
|
|
2466
|
-
|
|
2467
|
-
// Helper function to drop user and their objects
|
|
2468
|
-
const dropUser = async username => {
|
|
2469
|
-
// First revoke all possible privileges and reassign objects
|
|
2470
|
-
await adminClient.query(
|
|
2471
|
-
`
|
|
2472
|
-
-- Handle object ownership (CASCADE is critical here)
|
|
2473
|
-
REASSIGN OWNED BY ${username} TO postgres;
|
|
2474
|
-
DROP OWNED BY ${username} CASCADE;
|
|
2475
|
-
|
|
2476
|
-
-- Finally drop the user
|
|
2477
|
-
DROP ROLE ${username};
|
|
2478
|
-
`,
|
|
2479
|
-
);
|
|
2480
|
-
};
|
|
2481
|
-
|
|
2482
|
-
// Drop both users
|
|
2483
|
-
await dropUser(vectorRestrictedUser);
|
|
2484
|
-
await dropUser(schemaRestrictedUser);
|
|
2485
|
-
|
|
2486
|
-
await adminClient.query('COMMIT');
|
|
2487
|
-
} catch (e) {
|
|
2488
|
-
await adminClient.query('ROLLBACK');
|
|
2489
|
-
throw e;
|
|
2490
|
-
} finally {
|
|
2491
|
-
adminClient.release();
|
|
2492
|
-
}
|
|
2493
|
-
});
|
|
2494
|
-
|
|
2495
|
-
describe('Schema Creation', () => {
|
|
2496
|
-
beforeEach(async () => {
|
|
2497
|
-
// Ensure schema doesn't exist before each test
|
|
2498
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2499
|
-
try {
|
|
2500
|
-
await adminClient.query('BEGIN');
|
|
2501
|
-
await adminClient.query(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
|
|
2502
|
-
await adminClient.query('COMMIT');
|
|
2503
|
-
} catch (e) {
|
|
2504
|
-
await adminClient.query('ROLLBACK');
|
|
2505
|
-
throw e;
|
|
2506
|
-
} finally {
|
|
2507
|
-
adminClient.release();
|
|
2508
|
-
}
|
|
2509
|
-
});
|
|
2510
|
-
|
|
2511
|
-
it('should fail when user lacks CREATE privilege', async () => {
|
|
2512
|
-
const restrictedDB = new PgVector({
|
|
2513
|
-
connectionString: getConnectionString(schemaRestrictedUser),
|
|
2514
|
-
schemaName: testSchema,
|
|
2515
|
-
});
|
|
2516
|
-
|
|
2517
|
-
// Test schema creation directly by accessing private method
|
|
2518
|
-
await expect(async () => {
|
|
2519
|
-
const client = await restrictedDB['pool'].connect();
|
|
2520
|
-
try {
|
|
2521
|
-
await restrictedDB['setupSchema'](client);
|
|
2522
|
-
} finally {
|
|
2523
|
-
client.release();
|
|
2524
|
-
}
|
|
2525
|
-
}).rejects.toThrow(`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`);
|
|
2526
|
-
|
|
2527
|
-
// Verify schema was not created
|
|
2528
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2529
|
-
try {
|
|
2530
|
-
const res = await adminClient.query(
|
|
2531
|
-
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
|
|
2532
|
-
[testSchema],
|
|
2533
|
-
);
|
|
2534
|
-
expect(res.rows[0].exists).toBe(false);
|
|
2535
|
-
} finally {
|
|
2536
|
-
adminClient.release();
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
await restrictedDB.disconnect();
|
|
2540
|
-
});
|
|
2541
|
-
|
|
2542
|
-
it('should fail with schema creation error when creating index', async () => {
|
|
2543
|
-
const restrictedDB = new PgVector({
|
|
2544
|
-
connectionString: getConnectionString(schemaRestrictedUser),
|
|
2545
|
-
schemaName: testSchema,
|
|
2546
|
-
});
|
|
2547
|
-
|
|
2548
|
-
// This should fail with the schema creation error
|
|
2549
|
-
await expect(async () => {
|
|
2550
|
-
await restrictedDB.createIndex({ indexName: 'test', dimension: 3 });
|
|
2551
|
-
}).rejects.toThrow(`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`);
|
|
2552
|
-
|
|
2553
|
-
// Verify schema was not created
|
|
2554
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2555
|
-
try {
|
|
2556
|
-
const res = await adminClient.query(
|
|
2557
|
-
`SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
|
|
2558
|
-
[testSchema],
|
|
2559
|
-
);
|
|
2560
|
-
expect(res.rows[0].exists).toBe(false);
|
|
2561
|
-
} finally {
|
|
2562
|
-
adminClient.release();
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
await restrictedDB.disconnect();
|
|
2566
|
-
});
|
|
2567
|
-
});
|
|
2568
|
-
|
|
2569
|
-
describe('Vector Extension', () => {
|
|
2570
|
-
beforeEach(async () => {
|
|
2571
|
-
// Create test table and grant necessary permissions
|
|
2572
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2573
|
-
try {
|
|
2574
|
-
await adminClient.query('BEGIN');
|
|
2575
|
-
|
|
2576
|
-
// First install vector extension
|
|
2577
|
-
await adminClient.query('CREATE EXTENSION IF NOT EXISTS vector');
|
|
2578
|
-
|
|
2579
|
-
// Drop existing table if any
|
|
2580
|
-
await adminClient.query('DROP TABLE IF EXISTS test CASCADE');
|
|
2581
|
-
|
|
2582
|
-
// Create test table as admin
|
|
2583
|
-
await adminClient.query('CREATE TABLE IF NOT EXISTS test (id SERIAL PRIMARY KEY, embedding vector(3))');
|
|
2584
|
-
|
|
2585
|
-
// Grant ALL permissions including index creation
|
|
2586
|
-
await adminClient.query(`
|
|
2587
|
-
GRANT ALL ON TABLE test TO ${vectorRestrictedUser};
|
|
2588
|
-
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${vectorRestrictedUser};
|
|
2589
|
-
ALTER TABLE test OWNER TO ${vectorRestrictedUser};
|
|
2590
|
-
`);
|
|
2591
|
-
|
|
2592
|
-
await adminClient.query('COMMIT');
|
|
2593
|
-
} catch (e) {
|
|
2594
|
-
await adminClient.query('ROLLBACK');
|
|
2595
|
-
throw e;
|
|
2596
|
-
} finally {
|
|
2597
|
-
adminClient.release();
|
|
2598
|
-
}
|
|
2599
|
-
});
|
|
2600
|
-
|
|
2601
|
-
afterEach(async () => {
|
|
2602
|
-
// Clean up test table
|
|
2603
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2604
|
-
try {
|
|
2605
|
-
await adminClient.query('BEGIN');
|
|
2606
|
-
await adminClient.query('DROP TABLE IF EXISTS test CASCADE');
|
|
2607
|
-
await adminClient.query('COMMIT');
|
|
2608
|
-
} catch (e) {
|
|
2609
|
-
await adminClient.query('ROLLBACK');
|
|
2610
|
-
throw e;
|
|
2611
|
-
} finally {
|
|
2612
|
-
adminClient.release();
|
|
2613
|
-
}
|
|
2614
|
-
});
|
|
2615
|
-
|
|
2616
|
-
it('should handle lack of superuser privileges gracefully', async () => {
|
|
2617
|
-
// First ensure vector extension is not installed
|
|
2618
|
-
const adminClient = await new pg.Pool({ connectionString }).connect();
|
|
2619
|
-
try {
|
|
2620
|
-
await adminClient.query('DROP EXTENSION IF EXISTS vector CASCADE');
|
|
2621
|
-
} finally {
|
|
2622
|
-
adminClient.release();
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
const restrictedDB = new PgVector({
|
|
2626
|
-
connectionString: getConnectionString(vectorRestrictedUser),
|
|
2627
|
-
});
|
|
2628
|
-
|
|
2629
|
-
try {
|
|
2630
|
-
const warnSpy = vi.spyOn(restrictedDB['logger'], 'warn');
|
|
2631
|
-
|
|
2632
|
-
// Try to create index which will trigger vector extension installation attempt
|
|
2633
|
-
await expect(restrictedDB.createIndex({ indexName: 'test', dimension: 3 })).rejects.toThrow();
|
|
2634
|
-
|
|
2635
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
2636
|
-
expect.stringContaining('Could not install vector extension. This requires superuser privileges'),
|
|
2637
|
-
);
|
|
2638
|
-
|
|
2639
|
-
warnSpy.mockRestore();
|
|
2640
|
-
} finally {
|
|
2641
|
-
// Ensure we wait for any pending operations before disconnecting
|
|
2642
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2643
|
-
await restrictedDB.disconnect();
|
|
2644
|
-
}
|
|
2645
|
-
});
|
|
2646
|
-
|
|
2647
|
-
it('should continue if vector extension is already installed', async () => {
|
|
2648
|
-
const restrictedDB = new PgVector({
|
|
2649
|
-
connectionString: getConnectionString(vectorRestrictedUser),
|
|
2650
|
-
});
|
|
2651
|
-
|
|
2652
|
-
try {
|
|
2653
|
-
const debugSpy = vi.spyOn(restrictedDB['logger'], 'debug');
|
|
2654
|
-
|
|
2655
|
-
await restrictedDB.createIndex({ indexName: 'test', dimension: 3 });
|
|
2656
|
-
|
|
2657
|
-
expect(debugSpy).toHaveBeenCalledWith('Vector extension already installed, skipping installation');
|
|
2658
|
-
|
|
2659
|
-
debugSpy.mockRestore();
|
|
2660
|
-
} finally {
|
|
2661
|
-
// Ensure we wait for any pending operations before disconnecting
|
|
2662
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2663
|
-
await restrictedDB.disconnect();
|
|
2664
|
-
}
|
|
2665
|
-
});
|
|
2666
|
-
});
|
|
2667
|
-
});
|
|
2668
|
-
|
|
2669
|
-
describe('PoolConfig Custom Options', () => {
|
|
2670
|
-
it('should apply custom values to properties with default values', async () => {
|
|
2671
|
-
const db = new PgVector({
|
|
2672
|
-
connectionString,
|
|
2673
|
-
pgPoolOptions: {
|
|
2674
|
-
max: 5,
|
|
2675
|
-
idleTimeoutMillis: 10000,
|
|
2676
|
-
connectionTimeoutMillis: 1000,
|
|
2677
|
-
},
|
|
2678
|
-
});
|
|
2679
|
-
|
|
2680
|
-
expect(db['pool'].options.max).toBe(5);
|
|
2681
|
-
expect(db['pool'].options.idleTimeoutMillis).toBe(10000);
|
|
2682
|
-
expect(db['pool'].options.connectionTimeoutMillis).toBe(1000);
|
|
2683
|
-
});
|
|
2684
|
-
|
|
2685
|
-
it('should pass properties with no default values', async () => {
|
|
2686
|
-
const db = new PgVector({
|
|
2687
|
-
connectionString,
|
|
2688
|
-
pgPoolOptions: {
|
|
2689
|
-
ssl: false,
|
|
2690
|
-
},
|
|
2691
|
-
});
|
|
2692
|
-
|
|
2693
|
-
expect(db['pool'].options.ssl).toBe(false);
|
|
2694
|
-
});
|
|
2695
|
-
it('should keep default values when custom values are added', async () => {
|
|
2696
|
-
const db = new PgVector({
|
|
2697
|
-
connectionString,
|
|
2698
|
-
pgPoolOptions: {
|
|
2699
|
-
ssl: false,
|
|
2700
|
-
},
|
|
2701
|
-
});
|
|
2702
|
-
|
|
2703
|
-
expect(db['pool'].options.max).toBe(20);
|
|
2704
|
-
expect(db['pool'].options.idleTimeoutMillis).toBe(30000);
|
|
2705
|
-
expect(db['pool'].options.connectionTimeoutMillis).toBe(2000);
|
|
2706
|
-
expect(db['pool'].options.ssl).toBe(false);
|
|
2707
|
-
});
|
|
2708
|
-
});
|
|
2709
|
-
});
|
|
2710
|
-
|
|
2711
|
-
// Metadata filtering tests for Memory system
|
|
2712
|
-
describe('PgVector Metadata Filtering', () => {
|
|
2713
|
-
const connectionString = process.env.DB_URL || 'postgresql://postgres:postgres@localhost:5434/mastra';
|
|
2714
|
-
const metadataVectorDB = new PgVector({ connectionString });
|
|
2715
|
-
|
|
2716
|
-
createVectorTestSuite({
|
|
2717
|
-
vector: metadataVectorDB,
|
|
2718
|
-
createIndex: async (indexName: string) => {
|
|
2719
|
-
// Using dimension 4 as required by the metadata filtering test vectors
|
|
2720
|
-
await metadataVectorDB.createIndex({ indexName, dimension: 4 });
|
|
2721
|
-
},
|
|
2722
|
-
deleteIndex: async (indexName: string) => {
|
|
2723
|
-
await metadataVectorDB.deleteIndex({ indexName });
|
|
2724
|
-
},
|
|
2725
|
-
waitForIndexing: async () => {
|
|
2726
|
-
// PG doesn't need to wait for indexing
|
|
2727
|
-
},
|
|
2728
|
-
});
|
|
2729
|
-
});
|