@mastra/mongodb 1.13.0-alpha.0 → 1.14.0-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/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { skillSnapshotFieldValuesEqual } from '@mastra/core/storage/domains/skil
13
13
 
14
14
  // package.json
15
15
  var package_default = {
16
- version: "1.13.0-alpha.0"};
16
+ version: "1.14.0-alpha.0"};
17
17
  var MongoDBFilterTranslator = class extends BaseFilterTranslator {
18
18
  getSupportedOperators() {
19
19
  return {
@@ -103,13 +103,38 @@ var MongoDBFilterTranslator = class extends BaseFilterTranslator {
103
103
  };
104
104
 
105
105
  // src/vector/index.ts
106
- var MongoDBVector = class extends MastraVector {
106
+ var MongoDBVector = class _MongoDBVector extends MastraVector {
107
107
  client;
108
108
  db;
109
109
  collections;
110
110
  embeddingFieldName;
111
111
  metadataFieldName = "metadata";
112
112
  documentFieldName = "document";
113
+ /**
114
+ * Per-index cache of the filter paths declared in the Atlas vectorSearch index
115
+ * (e.g. `document`, `metadata.category`). Populated when an index is created in
116
+ * this process and lazily hydrated from the live index definition otherwise.
117
+ * `_id` is excluded because it is materialised separately by the fallback path.
118
+ */
119
+ declaredFilterPaths = /* @__PURE__ */ new Map();
120
+ /**
121
+ * MongoDB query operators supported inside `$vectorSearch.filter`. Intentionally
122
+ * conservative: filters using any operator outside this set fall back to the
123
+ * `$match` pre-filter, which supports the full query language. Widening this set
124
+ * is safe only for operators Atlas Vector Search actually accepts in `filter`.
125
+ */
126
+ static PUSHDOWN_OPERATORS = /* @__PURE__ */ new Set([
127
+ "$and",
128
+ "$or",
129
+ "$eq",
130
+ "$ne",
131
+ "$gt",
132
+ "$gte",
133
+ "$lt",
134
+ "$lte",
135
+ "$in",
136
+ "$nin"
137
+ ]);
113
138
  mongoMetricMap = {
114
139
  cosine: "cosine",
115
140
  euclidean: "euclidean",
@@ -172,7 +197,12 @@ var MongoDBVector = class extends MastraVector {
172
197
  * queryable. Skipping that step on a real Atlas cluster may cause
173
198
  * "index not found" or "index not ready" errors on subsequent operations.
174
199
  */
175
- async createIndex({ indexName, dimension, metric = "cosine" }) {
200
+ async createIndex({
201
+ indexName,
202
+ dimension,
203
+ metric = "cosine",
204
+ filterFields
205
+ }) {
176
206
  let mongoMetric;
177
207
  try {
178
208
  if (!Number.isInteger(dimension) || dimension <= 0) {
@@ -207,29 +237,33 @@ var MongoDBVector = class extends MastraVector {
207
237
  const indexNameInternal = `${indexName}_vector_index`;
208
238
  const embeddingField = this.embeddingFieldName;
209
239
  const numDimensions = dimension;
210
- await collection.createSearchIndex({
211
- definition: {
212
- fields: [
213
- {
214
- type: "vector",
215
- path: embeddingField,
216
- numDimensions,
217
- similarity: mongoMetric
218
- },
219
- {
220
- type: "filter",
221
- path: "_id"
222
- },
223
- {
224
- type: "filter",
225
- path: "document"
226
- }
227
- ]
240
+ const declaredMetadataPaths = this.buildDeclaredMetadataPaths(filterFields);
241
+ const fields = [
242
+ {
243
+ type: "vector",
244
+ path: embeddingField,
245
+ numDimensions,
246
+ similarity: mongoMetric
247
+ },
248
+ {
249
+ type: "filter",
250
+ path: "_id"
228
251
  },
252
+ {
253
+ type: "filter",
254
+ path: this.documentFieldName
255
+ },
256
+ ...declaredMetadataPaths.map((path) => ({ type: "filter", path }))
257
+ ];
258
+ const vectorIndexCreated = await this.createSearchIndexIgnoringExisting(collection, {
259
+ definition: { fields },
229
260
  name: indexNameInternal,
230
261
  type: "vectorSearch"
231
262
  });
232
- await collection.createSearchIndex({
263
+ if (vectorIndexCreated) {
264
+ this.declaredFilterPaths.set(indexName, /* @__PURE__ */ new Set([this.documentFieldName, ...declaredMetadataPaths]));
265
+ }
266
+ await this.createSearchIndexIgnoringExisting(collection, {
233
267
  definition: {
234
268
  mappings: {
235
269
  dynamic: true
@@ -239,16 +273,30 @@ var MongoDBVector = class extends MastraVector {
239
273
  type: "search"
240
274
  });
241
275
  } catch (error) {
242
- if (error.codeName !== "IndexAlreadyExists") {
243
- throw new MastraError(
244
- {
245
- id: createVectorErrorId("MONGODB", "CREATE_INDEX", "FAILED"),
246
- domain: ErrorDomain.STORAGE,
247
- category: ErrorCategory.THIRD_PARTY
248
- },
249
- error
250
- );
276
+ throw new MastraError(
277
+ {
278
+ id: createVectorErrorId("MONGODB", "CREATE_INDEX", "FAILED"),
279
+ domain: ErrorDomain.STORAGE,
280
+ category: ErrorCategory.THIRD_PARTY
281
+ },
282
+ error
283
+ );
284
+ }
285
+ }
286
+ /**
287
+ * Creates an Atlas Search index, treating IndexAlreadyExists as a no-op so
288
+ * `createIndex` stays idempotent per index. Returns true when the index was
289
+ * actually created by this call.
290
+ */
291
+ async createSearchIndexIgnoringExisting(collection, description) {
292
+ try {
293
+ await collection.createSearchIndex(description);
294
+ return true;
295
+ } catch (error) {
296
+ if (error?.codeName === "IndexAlreadyExists") {
297
+ return false;
251
298
  }
299
+ throw error;
252
300
  }
253
301
  }
254
302
  /**
@@ -390,9 +438,19 @@ var MongoDBVector = class extends MastraVector {
390
438
  limit: Math.min(1e4, topK)
391
439
  };
392
440
  if (hasMetadataFilter) {
393
- const candidateIds = await collection.aggregate([{ $match: metadataFilter }, { $project: { _id: 1 } }]).map((doc) => doc._id).toArray();
394
- if (candidateIds.length === 0) return [];
395
- vectorSearch.filter = documentFilter ? { $and: [{ _id: { $in: candidateIds } }, { [this.documentFieldName]: documentFilter }] } : { _id: { $in: candidateIds } };
441
+ let declaredPaths;
442
+ try {
443
+ declaredPaths = await this.getDeclaredFilterPaths(indexName);
444
+ } catch {
445
+ declaredPaths = /* @__PURE__ */ new Set();
446
+ }
447
+ if (this.canPushDownFilter(metadataFilter, declaredPaths)) {
448
+ vectorSearch.filter = documentFilter ? { $and: [metadataFilter, { [this.documentFieldName]: documentFilter }] } : metadataFilter;
449
+ } else {
450
+ const candidateIds = await collection.aggregate([{ $match: metadataFilter }, { $project: { _id: 1 } }]).map((doc) => doc._id).toArray();
451
+ if (candidateIds.length === 0) return [];
452
+ vectorSearch.filter = documentFilter ? { $and: [{ _id: { $in: candidateIds } }, { [this.documentFieldName]: documentFilter }] } : { _id: { $in: candidateIds } };
453
+ }
396
454
  } else if (documentFilter) {
397
455
  vectorSearch.filter = { [this.documentFieldName]: documentFilter };
398
456
  }
@@ -509,6 +567,7 @@ var MongoDBVector = class extends MastraVector {
509
567
  if (collection) {
510
568
  await collection.drop();
511
569
  this.collections.delete(indexName);
570
+ this.declaredFilterPaths.delete(indexName);
512
571
  } else {
513
572
  throw new Error(`Index (Collection) "${indexName}" does not exist`);
514
573
  }
@@ -775,6 +834,66 @@ var MongoDBVector = class extends MastraVector {
775
834
  }
776
835
  }
777
836
  }
837
+ /**
838
+ * Maps user-facing `filterFields` (bare metadata field names) to the fully
839
+ * qualified filter paths stored in the index, e.g. `category` → `metadata.category`.
840
+ * Blank entries are dropped and duplicates collapsed. A name already carrying the
841
+ * `metadata.` prefix is left untouched so callers can pass either form.
842
+ */
843
+ buildDeclaredMetadataPaths(filterFields) {
844
+ if (!filterFields?.length) return [];
845
+ const paths = /* @__PURE__ */ new Set();
846
+ for (const field of filterFields) {
847
+ const trimmed = field?.trim();
848
+ if (!trimmed) continue;
849
+ paths.add(trimmed.startsWith(`${this.metadataFieldName}.`) ? trimmed : `${this.metadataFieldName}.${trimmed}`);
850
+ }
851
+ return [...paths];
852
+ }
853
+ /**
854
+ * Returns the set of filter paths declared in the vectorSearch index (e.g.
855
+ * `document`, `metadata.category`), excluding `_id`. Reads the live index
856
+ * definition once per index and caches the result. Used to decide whether a
857
+ * metadata filter can be pushed down into `$vectorSearch.filter`.
858
+ */
859
+ async getDeclaredFilterPaths(indexName) {
860
+ const cached = this.declaredFilterPaths.get(indexName);
861
+ if (cached) return cached;
862
+ const collection = await this.getCollection(indexName, true);
863
+ const indexNameInternal = `${indexName}_vector_index`;
864
+ const indexInfo = await collection.listSearchIndexes().toArray();
865
+ const indexData = indexInfo.find((idx) => idx.name === indexNameInternal);
866
+ const paths = /* @__PURE__ */ new Set();
867
+ if (indexData?.status !== "READY") {
868
+ return paths;
869
+ }
870
+ for (const field of indexData.latestDefinition?.fields ?? []) {
871
+ if (field?.type === "filter" && typeof field.path === "string" && field.path !== "_id") {
872
+ paths.add(field.path);
873
+ }
874
+ }
875
+ this.declaredFilterPaths.set(indexName, paths);
876
+ return paths;
877
+ }
878
+ /**
879
+ * True when `filter` can be passed directly to `$vectorSearch.filter`: every
880
+ * field path it references is a declared filter field and every operator is in
881
+ * {@link MongoDBVector.PUSHDOWN_OPERATORS}. Conservative by design — anything it
882
+ * can't prove safe returns false so the caller uses the `$match` pre-filter.
883
+ */
884
+ canPushDownFilter(filter, declaredPaths) {
885
+ if (filter === null || typeof filter !== "object") return true;
886
+ if (Array.isArray(filter)) return filter.every((item) => this.canPushDownFilter(item, declaredPaths));
887
+ for (const [key, value] of Object.entries(filter)) {
888
+ if (key.startsWith("$")) {
889
+ if (!_MongoDBVector.PUSHDOWN_OPERATORS.has(key)) return false;
890
+ } else if (!declaredPaths.has(key)) {
891
+ return false;
892
+ }
893
+ if (!this.canPushDownFilter(value, declaredPaths)) return false;
894
+ }
895
+ return true;
896
+ }
778
897
  transformFilter(filter) {
779
898
  const translator = new MongoDBFilterTranslator();
780
899
  if (!filter) return {};