@mastra/mongodb 0.0.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ import type { VectorFilter } from '@mastra/core/vector/filter';
2
+ import { vi, describe, it, expect, beforeAll, afterAll, test } from 'vitest';
3
+ import { MongoDBVector } from './';
4
+
5
+ // Give tests enough time to complete database operations
6
+ vi.setConfig({ testTimeout: 300000, hookTimeout: 300000 });
7
+
8
+ // Concrete MongoDB configuration values – adjust these for your environment
9
+ const uri =
10
+ 'mongodb://mongodb:mongodb@localhost:27018/?authSource=admin&directConnection=true&serverSelectionTimeoutMS=2000';
11
+ const dbName = 'vector_db';
12
+
13
+ async function waitForAtlasSearchReady(
14
+ vectorDB: MongoDBVector,
15
+ indexName: string = 'dummy_vector_index',
16
+ dimension: number = 1,
17
+ metric: 'cosine' | 'euclidean' | 'dotproduct' = 'cosine',
18
+ timeout: number = 300000,
19
+ interval: number = 5000,
20
+ ) {
21
+ const start = Date.now();
22
+ let lastError: any = null;
23
+ let attempt = 0;
24
+ while (Date.now() - start < timeout) {
25
+ attempt++;
26
+ try {
27
+ await vectorDB.createIndex({ indexName, dimension, metric });
28
+ // If it succeeds, we're ready
29
+ console.log(`[waitForAtlasSearchReady] Atlas Search is ready! (attempt ${attempt})`);
30
+ return;
31
+ } catch (e: any) {
32
+ lastError = e;
33
+ console.log(`[waitForAtlasSearchReady] Not ready yet (attempt ${attempt}): ${e.message}`);
34
+ await new Promise(res => setTimeout(res, interval));
35
+ }
36
+ }
37
+ throw new Error(
38
+ 'Atlas Search did not become ready in time. Last error: ' + (lastError ? lastError.message : 'unknown'),
39
+ );
40
+ }
41
+ // Helper function to wait for a condition with timeout (similar to mdb_toolkit)
42
+ async function waitForCondition(
43
+ condition: () => Promise<boolean>,
44
+ timeout: number = 10000,
45
+ interval: number = 1000,
46
+ ): Promise<boolean> {
47
+ const startTime = Date.now();
48
+ while (Date.now() - startTime < timeout) {
49
+ if (await condition()) return true;
50
+ await new Promise(resolve => setTimeout(resolve, interval));
51
+ }
52
+ return false;
53
+ }
54
+
55
+ // Create index and wait until the search index (named `${indexName}_vector_index`) is READY
56
+ async function createIndexAndWait(
57
+ vectorDB: MongoDBVector,
58
+ indexName: string,
59
+ dimension: number,
60
+ metric: 'cosine' | 'euclidean' | 'dotproduct',
61
+ ) {
62
+ await vectorDB.createIndex({ indexName, dimension, metric });
63
+ await vectorDB.waitForIndexReady(indexName);
64
+ const created = await waitForCondition(
65
+ async () => {
66
+ const cols = await vectorDB.listIndexes();
67
+ return cols.includes(indexName);
68
+ },
69
+ 30000,
70
+ 2000,
71
+ );
72
+ if (!created) throw new Error('Timed out waiting for collection to be created');
73
+ }
74
+
75
+ // Delete index (collection) and wait until it is removed
76
+ async function deleteIndexAndWait(vectorDB: MongoDBVector, indexName: string) {
77
+ try {
78
+ await vectorDB.deleteIndex(indexName);
79
+ const deleted = await waitForCondition(
80
+ async () => {
81
+ const cols = await vectorDB.listIndexes();
82
+ return !cols.includes(indexName);
83
+ },
84
+ 30000,
85
+ 2000,
86
+ );
87
+ if (!deleted) throw new Error('Timed out waiting for collection to be deleted');
88
+ } catch (error) {
89
+ console.error(`Error deleting index ${indexName}:`, error);
90
+ }
91
+ }
92
+
93
+ describe('MongoDBVector Integration Tests', () => {
94
+ let vectorDB: MongoDBVector;
95
+ const testIndexName = 'my_vectors';
96
+ const testIndexName2 = 'my_vectors_2';
97
+
98
+ beforeAll(async () => {
99
+ vectorDB = new MongoDBVector({ uri, dbName });
100
+ await vectorDB.connect();
101
+
102
+ // Wait for Atlas Search to be ready
103
+ await waitForAtlasSearchReady(vectorDB);
104
+
105
+ // Cleanup any existing collections
106
+ try {
107
+ const cols = await vectorDB.listIndexes();
108
+ await Promise.all(cols.map(c => vectorDB.deleteIndex(c)));
109
+ const deleted = await waitForCondition(async () => (await vectorDB.listIndexes()).length === 0, 30000, 2000);
110
+ if (!deleted) throw new Error('Timed out waiting for collections to be deleted');
111
+ } catch (error) {
112
+ console.error('Failed to delete test collections:', error);
113
+ throw error;
114
+ }
115
+
116
+ await createIndexAndWait(vectorDB, testIndexName, 4, 'cosine');
117
+ await createIndexAndWait(vectorDB, testIndexName2, 4, 'cosine');
118
+ }, 500000);
119
+
120
+ afterAll(async () => {
121
+ try {
122
+ await vectorDB.deleteIndex(testIndexName);
123
+ } catch (error) {
124
+ console.error('Failed to delete test collection:', error);
125
+ }
126
+ try {
127
+ await vectorDB.deleteIndex(testIndexName2);
128
+ } catch (error) {
129
+ console.error('Failed to delete test collection:', error);
130
+ }
131
+ await vectorDB.disconnect();
132
+ });
133
+
134
+ test('full vector database workflow', async () => {
135
+ // Verify collection exists
136
+ const cols = await vectorDB.listIndexes();
137
+ expect(cols).toContain(testIndexName);
138
+
139
+ // Check stats (should be zero docs initially)
140
+ const initialStats = await vectorDB.describeIndex(testIndexName);
141
+ expect(initialStats).toEqual({ dimension: 4, metric: 'cosine', count: 0 });
142
+
143
+ // Upsert 4 vectors with metadata
144
+ const vectors = [
145
+ [1, 0, 0, 0],
146
+ [0, 1, 0, 0],
147
+ [0, 0, 1, 0],
148
+ [0, 0, 0, 1],
149
+ ];
150
+ const metadata = [{ label: 'vector1' }, { label: 'vector2' }, { label: 'vector3' }, { label: 'vector4' }];
151
+ const ids = await vectorDB.upsert({ indexName: testIndexName, vectors, metadata });
152
+ expect(ids).toHaveLength(4);
153
+
154
+ // Wait for the document count to update (increased delay to 5000ms)
155
+ await new Promise(resolve => setTimeout(resolve, 5000));
156
+ const updatedStats = await vectorDB.describeIndex(testIndexName);
157
+ expect(updatedStats.count).toEqual(4);
158
+
159
+ // Query for similar vectors (delay again to allow for index update)
160
+ await new Promise(resolve => setTimeout(resolve, 5000));
161
+ const queryVector = [1, 0, 0, 0];
162
+ const results = await vectorDB.query({ indexName: testIndexName, queryVector, topK: 2 });
163
+ expect(results).toHaveLength(2);
164
+ expect(results[0]?.metadata).toEqual({ label: 'vector1' });
165
+ expect(results[0]?.score).toBeCloseTo(1, 4);
166
+
167
+ // Query using a filter via filter.ts translator
168
+ const filteredResults = await vectorDB.query({
169
+ indexName: testIndexName,
170
+ queryVector,
171
+ topK: 4, // Increased from 2 to 4 to ensure all vectors are considered before filtering
172
+ filter: { 'metadata.label': 'vector2' },
173
+ });
174
+ expect(filteredResults).toHaveLength(1);
175
+ expect(filteredResults[0]?.metadata).toEqual({ label: 'vector2' });
176
+
177
+ // Final stats should show > 0 documents
178
+ const finalStats = await vectorDB.describeIndex(testIndexName);
179
+ expect(finalStats.count).toBeGreaterThan(0);
180
+ });
181
+
182
+ test('gets vector results back from query with vector included', async () => {
183
+ // Delay to allow index update after any writes
184
+ await new Promise(resolve => setTimeout(resolve, 5000));
185
+ const queryVector = [1, 0, 0, 0];
186
+ const results = await vectorDB.query({
187
+ indexName: testIndexName,
188
+ queryVector,
189
+ topK: 2,
190
+ includeVector: true,
191
+ });
192
+ expect(results).toHaveLength(2);
193
+ expect(results[0]?.metadata).toEqual({ label: 'vector1' });
194
+ expect(results[0]?.score).toBeCloseTo(1, 4);
195
+ expect(results[0]?.vector).toBeDefined();
196
+ });
197
+
198
+ test('handles different vector dimensions', async () => {
199
+ const highDimIndexName = 'high_dim_test_' + Date.now();
200
+ try {
201
+ await createIndexAndWait(vectorDB, highDimIndexName, 1536, 'cosine');
202
+ const vectors = [
203
+ Array(1536)
204
+ .fill(0)
205
+ .map((_, i) => i % 2),
206
+ Array(1536)
207
+ .fill(0)
208
+ .map((_, i) => (i + 1) % 2),
209
+ ];
210
+ const metadata = [{ label: 'even' }, { label: 'odd' }];
211
+ const ids = await vectorDB.upsert({ indexName: highDimIndexName, vectors, metadata });
212
+ expect(ids).toHaveLength(2);
213
+ await new Promise(resolve => setTimeout(resolve, 5000));
214
+ const queryVector = Array(1536)
215
+ .fill(0)
216
+ .map((_, i) => i % 2);
217
+ const results = await vectorDB.query({ indexName: highDimIndexName, queryVector, topK: 2 });
218
+ expect(results).toHaveLength(2);
219
+ expect(results[0]?.metadata).toEqual({ label: 'even' });
220
+ expect(results[0]?.score).toBeCloseTo(1, 4);
221
+ } finally {
222
+ await deleteIndexAndWait(vectorDB, highDimIndexName);
223
+ }
224
+ });
225
+
226
+ test('handles different distance metrics', async () => {
227
+ const metrics: Array<'cosine' | 'euclidean' | 'dotproduct'> = ['cosine', 'euclidean', 'dotproduct'];
228
+ for (const metric of metrics) {
229
+ const metricIndexName = `metrictest_${metric}_${Date.now()}`;
230
+ try {
231
+ await createIndexAndWait(vectorDB, metricIndexName, 4, metric);
232
+ const vectors = [
233
+ [1, 0, 0, 0],
234
+ [0.7071, 0.7071, 0, 0],
235
+ ];
236
+ await vectorDB.upsert({ indexName: metricIndexName, vectors });
237
+ await new Promise(resolve => setTimeout(resolve, 5000));
238
+ const results = await vectorDB.query({ indexName: metricIndexName, queryVector: [1, 0, 0, 0], topK: 2 });
239
+ expect(results).toHaveLength(2);
240
+ expect(results[0]?.score).toBeGreaterThan(results[1]?.score ?? 0);
241
+ } finally {
242
+ await deleteIndexAndWait(vectorDB, metricIndexName);
243
+ }
244
+ }
245
+ }, 500000);
246
+
247
+ describe('Filter Validation in Queries', () => {
248
+ // Helper function to retry queries with delay
249
+ async function retryQuery(params: any, maxRetries = 2) {
250
+ let results = await vectorDB.query(params);
251
+ let retryCount = 0;
252
+
253
+ // If no results, retry a few times with delay
254
+ while (results.length === 0 && retryCount < maxRetries) {
255
+ console.log(`No results found, retrying (${retryCount + 1}/${maxRetries})...`);
256
+ await new Promise(resolve => setTimeout(resolve, 5000));
257
+ results = await vectorDB.query(params);
258
+ retryCount++;
259
+ }
260
+
261
+ return results;
262
+ }
263
+
264
+ beforeAll(async () => {
265
+ // Ensure testIndexName2 has at least one document
266
+ const testVector = [1, 0, 0, 0];
267
+ const testMetadata = { label: 'test_filter_validation' };
268
+
269
+ // First check if there are already documents
270
+ const existingResults = await vectorDB.query({
271
+ indexName: testIndexName2,
272
+ queryVector: testVector,
273
+ topK: 1,
274
+ });
275
+
276
+ // If no documents exist, insert one
277
+ if (existingResults.length === 0) {
278
+ await vectorDB.upsert({
279
+ indexName: testIndexName2,
280
+ vectors: [testVector],
281
+ metadata: [testMetadata],
282
+ });
283
+
284
+ // Wait for the document to be indexed
285
+ await new Promise(resolve => setTimeout(resolve, 5000));
286
+
287
+ // Verify the document is indexed
288
+ const verifyResults = await retryQuery({
289
+ indexName: testIndexName2,
290
+ queryVector: testVector,
291
+ topK: 1,
292
+ });
293
+
294
+ if (verifyResults.length === 0) {
295
+ console.warn('Warning: Could not verify document was indexed in testIndexName2');
296
+ }
297
+ }
298
+ }, 30000);
299
+
300
+ it('handles undefined filter', async () => {
301
+ const results1 = await retryQuery({
302
+ indexName: testIndexName2,
303
+ queryVector: [1, 0, 0, 0],
304
+ filter: undefined,
305
+ });
306
+ const results2 = await retryQuery({
307
+ indexName: testIndexName2,
308
+ queryVector: [1, 0, 0, 0],
309
+ });
310
+ expect(results1).toEqual(results2);
311
+ expect(results1.length).toBeGreaterThan(0);
312
+ });
313
+
314
+ it('handles empty object filter', async () => {
315
+ const results = await retryQuery({
316
+ indexName: testIndexName2,
317
+ queryVector: [1, 0, 0, 0],
318
+ filter: {},
319
+ });
320
+ const results2 = await retryQuery({
321
+ indexName: testIndexName2,
322
+ queryVector: [1, 0, 0, 0],
323
+ });
324
+ expect(results).toEqual(results2);
325
+ expect(results.length).toBeGreaterThan(0);
326
+ });
327
+
328
+ it('handles null filter', async () => {
329
+ const results = await retryQuery({
330
+ indexName: testIndexName2,
331
+ queryVector: [1, 0, 0, 0],
332
+ filter: null as unknown as VectorFilter,
333
+ });
334
+ const results2 = await retryQuery({
335
+ indexName: testIndexName2,
336
+ queryVector: [1, 0, 0, 0],
337
+ });
338
+ expect(results).toEqual(results2);
339
+ expect(results.length).toBeGreaterThan(0);
340
+ });
341
+
342
+ it('normalizes date values in filter using filter.ts', async () => {
343
+ const vector = [1, 0, 0, 0];
344
+ const timestampDate = new Date('2024-01-01T00:00:00Z');
345
+ // Upsert a document with a timestamp in metadata
346
+ await vectorDB.upsert({
347
+ indexName: testIndexName2,
348
+ vectors: [vector],
349
+ metadata: [{ timestamp: timestampDate }],
350
+ });
351
+ await new Promise(r => setTimeout(r, 5000));
352
+ const results = await retryQuery({
353
+ indexName: testIndexName2,
354
+ queryVector: vector,
355
+ filter: { 'metadata.timestamp': { $gt: new Date('2023-01-01T00:00:00Z') } },
356
+ });
357
+ expect(results.length).toBeGreaterThan(0);
358
+ expect(new Date(results[0]?.metadata?.timestamp).toISOString()).toEqual(timestampDate.toISOString());
359
+ });
360
+ });
361
+
362
+ describe('Basic vector operations', () => {
363
+ const indexName = 'test_basic_vector_ops_' + Date.now();
364
+ beforeAll(async () => {
365
+ await createIndexAndWait(vectorDB, indexName, 4, 'cosine');
366
+ });
367
+ afterAll(async () => {
368
+ await deleteIndexAndWait(vectorDB, indexName);
369
+ });
370
+ const testVectors = [
371
+ [1, 0, 0, 0],
372
+ [0, 1, 0, 0],
373
+ [0, 0, 1, 0],
374
+ [0, 0, 0, 1],
375
+ ];
376
+ it('should update the vector by id', async () => {
377
+ const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
378
+ expect(ids).toHaveLength(4);
379
+ const idToBeUpdated = ids[0];
380
+ const newVector = [1, 2, 3, 4];
381
+ const newMetaData = { test: 'updates' };
382
+ await vectorDB.updateIndexById(indexName, idToBeUpdated, { vector: newVector, metadata: newMetaData });
383
+ await new Promise(resolve => setTimeout(resolve, 5000));
384
+ const results = await vectorDB.query({
385
+ indexName,
386
+ queryVector: newVector,
387
+ topK: 2,
388
+ includeVector: true,
389
+ });
390
+ expect(results).toHaveLength(2);
391
+ const updatedResult = results.find(result => result.id === idToBeUpdated);
392
+ expect(updatedResult).toBeDefined();
393
+ expect(updatedResult?.id).toEqual(idToBeUpdated);
394
+ expect(updatedResult?.vector).toEqual(newVector);
395
+ expect(updatedResult?.metadata).toEqual(newMetaData);
396
+ });
397
+ it('should only update the metadata by id', async () => {
398
+ const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
399
+ expect(ids).toHaveLength(4);
400
+ const idToBeUpdated = ids[0];
401
+ const newMetaData = { test: 'metadata only update' };
402
+ await vectorDB.updateIndexById(indexName, idToBeUpdated, { metadata: newMetaData });
403
+ await new Promise(resolve => setTimeout(resolve, 5000));
404
+ const results = await vectorDB.query({
405
+ indexName,
406
+ queryVector: testVectors[0],
407
+ topK: 2,
408
+ includeVector: true,
409
+ });
410
+ expect(results).toHaveLength(2);
411
+ const updatedResult = results.find(result => result.id === idToBeUpdated);
412
+ expect(updatedResult).toBeDefined();
413
+ expect(updatedResult?.id).toEqual(idToBeUpdated);
414
+ expect(updatedResult?.vector).toEqual(testVectors[0]);
415
+ expect(updatedResult?.metadata).toEqual(newMetaData);
416
+ });
417
+ it('should only update vector embeddings by id', async () => {
418
+ const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
419
+ expect(ids).toHaveLength(4);
420
+ const idToBeUpdated = ids[0];
421
+ const newVector = [1, 2, 3, 4];
422
+ await vectorDB.updateIndexById(indexName, idToBeUpdated, { vector: newVector });
423
+ await new Promise(resolve => setTimeout(resolve, 5000));
424
+ const results = await vectorDB.query({
425
+ indexName,
426
+ queryVector: newVector,
427
+ topK: 2,
428
+ includeVector: true,
429
+ });
430
+ expect(results).toHaveLength(2);
431
+ const updatedResult = results.find(result => result.id === idToBeUpdated);
432
+ expect(updatedResult).toBeDefined();
433
+ expect(updatedResult?.id).toEqual(idToBeUpdated);
434
+ expect(updatedResult?.vector).toEqual(newVector);
435
+ });
436
+ it('should throw exception when no updates are given', async () => {
437
+ await expect(vectorDB.updateIndexById(indexName, 'nonexistent-id', {})).rejects.toThrow('No updates provided');
438
+ });
439
+ it('should delete the vector by id', async () => {
440
+ const ids = await vectorDB.upsert({ indexName, vectors: testVectors });
441
+ expect(ids).toHaveLength(4);
442
+ const idToBeDeleted = ids[0];
443
+ await vectorDB.deleteIndexById(indexName, idToBeDeleted);
444
+ const results = await vectorDB.query({ indexName, queryVector: [1, 0, 0, 0], topK: 2 });
445
+ expect(results.map(res => res.id)).not.toContain(idToBeDeleted);
446
+ });
447
+ });
448
+ });