@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.
- package/.turbo/turbo-build.log +23 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE.md +46 -0
- package/README.md +140 -0
- package/dist/_tsup-dts-rollup.d.cts +88 -0
- package/dist/_tsup-dts-rollup.d.ts +88 -0
- package/dist/index.cjs +363 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +361 -0
- package/docker-compose.yml +8 -0
- package/eslint.config.js +6 -0
- package/package.json +48 -0
- package/src/index.ts +1 -0
- package/src/vector/filter.test.ts +415 -0
- package/src/vector/filter.ts +124 -0
- package/src/vector/index.test.ts +448 -0
- package/src/vector/index.ts +380 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +11 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { MastraVector } from '@mastra/core/vector';
|
|
2
|
+
import { MongoClient } from 'mongodb';
|
|
3
|
+
import { v4 } from 'uuid';
|
|
4
|
+
import { BaseFilterTranslator } from '@mastra/core/vector/filter';
|
|
5
|
+
|
|
6
|
+
// src/vector/index.ts
|
|
7
|
+
var MongoDBFilterTranslator = class extends BaseFilterTranslator {
|
|
8
|
+
getSupportedOperators() {
|
|
9
|
+
return {
|
|
10
|
+
...BaseFilterTranslator.DEFAULT_OPERATORS,
|
|
11
|
+
array: ["$all", "$in", "$nin"],
|
|
12
|
+
logical: ["$and", "$or", "$not", "$nor"],
|
|
13
|
+
regex: ["$regex"],
|
|
14
|
+
custom: ["$size", "$elemMatch"]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
translate(filter) {
|
|
18
|
+
if (this.isEmpty(filter)) return filter;
|
|
19
|
+
this.validateFilter(filter);
|
|
20
|
+
return this.translateNode(filter);
|
|
21
|
+
}
|
|
22
|
+
translateNode(node) {
|
|
23
|
+
if (this.isRegex(node)) {
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
if (this.isPrimitive(node)) return node;
|
|
27
|
+
if (Array.isArray(node)) return node;
|
|
28
|
+
const entries = Object.entries(node);
|
|
29
|
+
const translatedEntries = entries.map(([key, value]) => {
|
|
30
|
+
if (this.isOperator(key)) {
|
|
31
|
+
return [key, this.translateOperatorValue(key, value)];
|
|
32
|
+
}
|
|
33
|
+
return [key, this.translateNode(value)];
|
|
34
|
+
});
|
|
35
|
+
return Object.fromEntries(translatedEntries);
|
|
36
|
+
}
|
|
37
|
+
translateOperatorValue(operator, value) {
|
|
38
|
+
if (this.isLogicalOperator(operator)) {
|
|
39
|
+
if (operator === "$not") {
|
|
40
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
41
|
+
throw new Error("$not operator requires an object");
|
|
42
|
+
}
|
|
43
|
+
if (this.isEmpty(value)) {
|
|
44
|
+
throw new Error("$not operator cannot be empty");
|
|
45
|
+
}
|
|
46
|
+
return this.translateNode(value);
|
|
47
|
+
} else {
|
|
48
|
+
if (!Array.isArray(value)) {
|
|
49
|
+
throw new Error(`Value for logical operator ${operator} must be an array`);
|
|
50
|
+
}
|
|
51
|
+
return value.map((item) => this.translateNode(item));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (this.isBasicOperator(operator) || this.isNumericOperator(operator)) {
|
|
55
|
+
if (value instanceof Date) {
|
|
56
|
+
return value.toISOString();
|
|
57
|
+
}
|
|
58
|
+
return this.normalizeComparisonValue(value);
|
|
59
|
+
}
|
|
60
|
+
if (operator === "$elemMatch") {
|
|
61
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
62
|
+
throw new Error(`Value for $elemMatch operator must be an object`);
|
|
63
|
+
}
|
|
64
|
+
return this.translateNode(value);
|
|
65
|
+
}
|
|
66
|
+
if (this.isArrayOperator(operator)) {
|
|
67
|
+
if (!Array.isArray(value)) {
|
|
68
|
+
throw new Error(`Value for array operator ${operator} must be an array`);
|
|
69
|
+
}
|
|
70
|
+
return this.normalizeArrayValues(value);
|
|
71
|
+
}
|
|
72
|
+
if (this.isElementOperator(operator)) {
|
|
73
|
+
if (operator === "$exists" && typeof value !== "boolean") {
|
|
74
|
+
throw new Error(`Value for $exists operator must be a boolean`);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
if (this.isRegexOperator(operator)) {
|
|
79
|
+
if (!(value instanceof RegExp) && typeof value !== "string") {
|
|
80
|
+
throw new Error(`Value for ${operator} operator must be a RegExp or string`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (operator === "$size") {
|
|
85
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
86
|
+
throw new Error(`Value for $size operator must be a non-negative integer`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
91
|
+
}
|
|
92
|
+
isEmpty(filter) {
|
|
93
|
+
return filter === void 0 || filter === null || typeof filter === "object" && Object.keys(filter).length === 0;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/vector/index.ts
|
|
98
|
+
var MongoDBVector = class extends MastraVector {
|
|
99
|
+
client;
|
|
100
|
+
db;
|
|
101
|
+
collections;
|
|
102
|
+
embeddingFieldName = "embedding";
|
|
103
|
+
metadataFieldName = "metadata";
|
|
104
|
+
documentFieldName = "document";
|
|
105
|
+
collectionForValidation = null;
|
|
106
|
+
mongoMetricMap = {
|
|
107
|
+
cosine: "cosine",
|
|
108
|
+
euclidean: "euclidean",
|
|
109
|
+
dotproduct: "dotProduct"
|
|
110
|
+
};
|
|
111
|
+
constructor({ uri, dbName, options }) {
|
|
112
|
+
super();
|
|
113
|
+
this.client = new MongoClient(uri, options);
|
|
114
|
+
this.db = this.client.db(dbName);
|
|
115
|
+
this.collections = /* @__PURE__ */ new Map();
|
|
116
|
+
}
|
|
117
|
+
// Public methods
|
|
118
|
+
async connect() {
|
|
119
|
+
await this.client.connect();
|
|
120
|
+
}
|
|
121
|
+
async disconnect() {
|
|
122
|
+
await this.client.close();
|
|
123
|
+
}
|
|
124
|
+
async createIndex(params) {
|
|
125
|
+
const { indexName, dimension, metric = "cosine" } = params;
|
|
126
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
127
|
+
throw new Error("Dimension must be a positive integer");
|
|
128
|
+
}
|
|
129
|
+
const mongoMetric = this.mongoMetricMap[metric];
|
|
130
|
+
if (!mongoMetric) {
|
|
131
|
+
throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
|
|
132
|
+
}
|
|
133
|
+
const collectionExists = await this.db.listCollections({ name: indexName }).hasNext();
|
|
134
|
+
if (!collectionExists) {
|
|
135
|
+
await this.db.createCollection(indexName);
|
|
136
|
+
}
|
|
137
|
+
const collection = await this.getCollection(indexName);
|
|
138
|
+
const indexNameInternal = `${indexName}_vector_index`;
|
|
139
|
+
const embeddingField = this.embeddingFieldName;
|
|
140
|
+
const numDimensions = dimension;
|
|
141
|
+
try {
|
|
142
|
+
await collection.createSearchIndex({
|
|
143
|
+
definition: {
|
|
144
|
+
fields: [
|
|
145
|
+
{
|
|
146
|
+
type: "vector",
|
|
147
|
+
path: embeddingField,
|
|
148
|
+
numDimensions,
|
|
149
|
+
similarity: mongoMetric
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
name: indexNameInternal,
|
|
154
|
+
type: "vectorSearch"
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.codeName !== "IndexAlreadyExists") {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await collection.updateOne({ _id: "__index_metadata__" }, { $set: { dimension, metric } }, { upsert: true });
|
|
162
|
+
}
|
|
163
|
+
async waitForIndexReady(indexName, timeoutMs = 6e4, checkIntervalMs = 2e3) {
|
|
164
|
+
const collection = await this.getCollection(indexName, true);
|
|
165
|
+
const indexNameInternal = `${indexName}_vector_index`;
|
|
166
|
+
const startTime = Date.now();
|
|
167
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
168
|
+
const indexInfo = await collection.listSearchIndexes().toArray();
|
|
169
|
+
const indexData = indexInfo.find((idx) => idx.name === indexNameInternal);
|
|
170
|
+
const status = indexData?.status;
|
|
171
|
+
if (status === "READY") {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, checkIntervalMs));
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`Index "${indexNameInternal}" did not become ready within timeout`);
|
|
177
|
+
}
|
|
178
|
+
async upsert(params) {
|
|
179
|
+
const { indexName, vectors, metadata, ids, documents } = params;
|
|
180
|
+
const collection = await this.getCollection(indexName);
|
|
181
|
+
this.collectionForValidation = collection;
|
|
182
|
+
const stats = await this.describeIndex(indexName);
|
|
183
|
+
await this.validateVectorDimensions(vectors, stats.dimension);
|
|
184
|
+
const generatedIds = ids || vectors.map(() => v4());
|
|
185
|
+
const operations = vectors.map((vector, idx) => {
|
|
186
|
+
const id = generatedIds[idx];
|
|
187
|
+
const meta = metadata?.[idx] || {};
|
|
188
|
+
const doc = documents?.[idx];
|
|
189
|
+
const normalizedMeta = Object.keys(meta).reduce(
|
|
190
|
+
(acc, key) => {
|
|
191
|
+
acc[key] = meta[key] instanceof Date ? meta[key].toISOString() : meta[key];
|
|
192
|
+
return acc;
|
|
193
|
+
},
|
|
194
|
+
{}
|
|
195
|
+
);
|
|
196
|
+
const updateDoc = {
|
|
197
|
+
[this.embeddingFieldName]: vector,
|
|
198
|
+
[this.metadataFieldName]: normalizedMeta
|
|
199
|
+
};
|
|
200
|
+
if (doc !== void 0) {
|
|
201
|
+
updateDoc[this.documentFieldName] = doc;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
updateOne: {
|
|
205
|
+
filter: { _id: id },
|
|
206
|
+
// '_id' is a string as per MongoDBDocument interface
|
|
207
|
+
update: { $set: updateDoc },
|
|
208
|
+
upsert: true
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
await collection.bulkWrite(operations);
|
|
213
|
+
return generatedIds;
|
|
214
|
+
}
|
|
215
|
+
async query(params) {
|
|
216
|
+
const { indexName, queryVector, topK = 10, filter, includeVector = false, documentFilter } = params;
|
|
217
|
+
const collection = await this.getCollection(indexName, true);
|
|
218
|
+
const indexNameInternal = `${indexName}_vector_index`;
|
|
219
|
+
const mongoFilter = this.transformFilter(filter);
|
|
220
|
+
const documentMongoFilter = documentFilter ? { [this.documentFieldName]: documentFilter } : {};
|
|
221
|
+
let combinedFilter = {};
|
|
222
|
+
if (Object.keys(mongoFilter).length > 0 && Object.keys(documentMongoFilter).length > 0) {
|
|
223
|
+
combinedFilter = { $and: [mongoFilter, documentMongoFilter] };
|
|
224
|
+
} else if (Object.keys(mongoFilter).length > 0) {
|
|
225
|
+
combinedFilter = mongoFilter;
|
|
226
|
+
} else if (Object.keys(documentMongoFilter).length > 0) {
|
|
227
|
+
combinedFilter = documentMongoFilter;
|
|
228
|
+
}
|
|
229
|
+
const pipeline = [
|
|
230
|
+
{
|
|
231
|
+
$vectorSearch: {
|
|
232
|
+
index: indexNameInternal,
|
|
233
|
+
queryVector,
|
|
234
|
+
path: this.embeddingFieldName,
|
|
235
|
+
numCandidates: 100,
|
|
236
|
+
limit: topK
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
// Apply the filter using $match stage
|
|
240
|
+
...Object.keys(combinedFilter).length > 0 ? [{ $match: combinedFilter }] : [],
|
|
241
|
+
{
|
|
242
|
+
$set: { score: { $meta: "vectorSearchScore" } }
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
$project: {
|
|
246
|
+
_id: 1,
|
|
247
|
+
score: 1,
|
|
248
|
+
metadata: `$${this.metadataFieldName}`,
|
|
249
|
+
document: `$${this.documentFieldName}`,
|
|
250
|
+
...includeVector && { vector: `$${this.embeddingFieldName}` }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
try {
|
|
255
|
+
const results = await collection.aggregate(pipeline).toArray();
|
|
256
|
+
return results.map((result) => ({
|
|
257
|
+
id: result._id,
|
|
258
|
+
score: result.score,
|
|
259
|
+
metadata: result.metadata,
|
|
260
|
+
vector: includeVector ? result.vector : void 0,
|
|
261
|
+
document: result.document
|
|
262
|
+
}));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error("Error during vector search:", error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async listIndexes() {
|
|
269
|
+
const collections = await this.db.listCollections().toArray();
|
|
270
|
+
return collections.map((col) => col.name);
|
|
271
|
+
}
|
|
272
|
+
async describeIndex(indexName) {
|
|
273
|
+
const collection = await this.getCollection(indexName, true);
|
|
274
|
+
const count = await collection.countDocuments({ _id: { $ne: "__index_metadata__" } });
|
|
275
|
+
const metadataDoc = await collection.findOne({ _id: "__index_metadata__" });
|
|
276
|
+
const dimension = metadataDoc?.dimension || 0;
|
|
277
|
+
const metric = metadataDoc?.metric || "cosine";
|
|
278
|
+
return {
|
|
279
|
+
dimension,
|
|
280
|
+
count,
|
|
281
|
+
metric
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async deleteIndex(indexName) {
|
|
285
|
+
const collection = await this.getCollection(indexName, false);
|
|
286
|
+
if (collection) {
|
|
287
|
+
await collection.drop();
|
|
288
|
+
this.collections.delete(indexName);
|
|
289
|
+
} else {
|
|
290
|
+
throw new Error(`Index (Collection) "${indexName}" does not exist`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async updateIndexById(indexName, id, update) {
|
|
294
|
+
if (!update.vector && !update.metadata) {
|
|
295
|
+
throw new Error("No updates provided");
|
|
296
|
+
}
|
|
297
|
+
const collection = await this.getCollection(indexName, true);
|
|
298
|
+
const updateDoc = {};
|
|
299
|
+
if (update.vector) {
|
|
300
|
+
updateDoc[this.embeddingFieldName] = update.vector;
|
|
301
|
+
}
|
|
302
|
+
if (update.metadata) {
|
|
303
|
+
const normalizedMeta = Object.keys(update.metadata).reduce(
|
|
304
|
+
(acc, key) => {
|
|
305
|
+
acc[key] = update.metadata[key] instanceof Date ? update.metadata[key].toISOString() : update.metadata[key];
|
|
306
|
+
return acc;
|
|
307
|
+
},
|
|
308
|
+
{}
|
|
309
|
+
);
|
|
310
|
+
updateDoc[this.metadataFieldName] = normalizedMeta;
|
|
311
|
+
}
|
|
312
|
+
await collection.findOneAndUpdate({ _id: id }, { $set: updateDoc });
|
|
313
|
+
}
|
|
314
|
+
async deleteIndexById(indexName, id) {
|
|
315
|
+
try {
|
|
316
|
+
const collection = await this.getCollection(indexName, true);
|
|
317
|
+
await collection.deleteOne({ _id: id });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
throw new Error(`Failed to delete index by id: ${id} for index name: ${indexName}: ${error.message}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Private methods
|
|
323
|
+
async getCollection(indexName, throwIfNotExists = true) {
|
|
324
|
+
if (this.collections.has(indexName)) {
|
|
325
|
+
return this.collections.get(indexName);
|
|
326
|
+
}
|
|
327
|
+
const collection = this.db.collection(indexName);
|
|
328
|
+
const collectionExists = await this.db.listCollections({ name: indexName }).hasNext();
|
|
329
|
+
if (!collectionExists && throwIfNotExists) {
|
|
330
|
+
throw new Error(`Index (Collection) "${indexName}" does not exist`);
|
|
331
|
+
}
|
|
332
|
+
this.collections.set(indexName, collection);
|
|
333
|
+
return collection;
|
|
334
|
+
}
|
|
335
|
+
async validateVectorDimensions(vectors, dimension) {
|
|
336
|
+
if (vectors.length === 0) {
|
|
337
|
+
throw new Error("No vectors provided for validation");
|
|
338
|
+
}
|
|
339
|
+
if (dimension === 0) {
|
|
340
|
+
dimension = vectors[0] ? vectors[0].length : 0;
|
|
341
|
+
await this.setIndexDimension(dimension);
|
|
342
|
+
}
|
|
343
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
344
|
+
let v = vectors[i]?.length;
|
|
345
|
+
if (v !== dimension) {
|
|
346
|
+
throw new Error(`Vector at index ${i} has invalid dimension ${v}. Expected ${dimension} dimensions.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async setIndexDimension(dimension) {
|
|
351
|
+
const collection = this.collectionForValidation;
|
|
352
|
+
await collection.updateOne({ _id: "__index_metadata__" }, { $set: { dimension } }, { upsert: true });
|
|
353
|
+
}
|
|
354
|
+
transformFilter(filter) {
|
|
355
|
+
const translator = new MongoDBFilterTranslator();
|
|
356
|
+
if (!filter) return {};
|
|
357
|
+
return translator.translate(filter);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export { MongoDBVector };
|
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mastra/mongodb",
|
|
3
|
+
"version": "0.0.2-alpha.0",
|
|
4
|
+
"description": "MongoDB provider for Mastra - includes vector store capabilities",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@types/mongodb": "^4.0.7",
|
|
23
|
+
"cloudflare": "^4.1.0",
|
|
24
|
+
"mongodb": "^6.15.0",
|
|
25
|
+
"uuid": "^11.1.0",
|
|
26
|
+
"@mastra/core": "^0.9.1-alpha.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@microsoft/api-extractor": "^7.52.1",
|
|
30
|
+
"@types/node": "^20.17.27",
|
|
31
|
+
"eslint": "^9.23.0",
|
|
32
|
+
"tsup": "^8.4.0",
|
|
33
|
+
"typescript": "^5.8.2",
|
|
34
|
+
"vitest": "^3.0.9",
|
|
35
|
+
"@internal/lint": "0.0.2"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
|
39
|
+
"build:watch": "pnpm build --watch",
|
|
40
|
+
"pretest": "docker compose up -d && (for i in $(seq 1 30); do docker compose exec -T mongodb mongosh --eval 'db.adminCommand(\"ping\")' --quiet && break || (sleep 1; [ $i -eq 30 ] && exit 1); done)",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"posttest": "docker compose down -v",
|
|
43
|
+
"pretest:watch": "docker compose up -d",
|
|
44
|
+
"test:watch": "vitest watch",
|
|
45
|
+
"posttest:watch": "docker compose down -v",
|
|
46
|
+
"lint": "eslint ."
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vector';
|