@mastra/mongodb 1.18.10 → 1.19.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/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/reference-rag-vector-databases.md +23 -0
- package/dist/docs/references/reference-vectors-mongodb.md +90 -5
- package/dist/index.cjs +233 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +233 -64
- package/dist/index.js.map +1 -1
- package/dist/vector/index.d.ts +89 -5
- package/dist/vector/index.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { MessageList } from "@mastra/core/agent";
|
|
|
9
9
|
import { saveScorePayloadSchema } from "@mastra/core/evals";
|
|
10
10
|
import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
|
|
11
11
|
//#region package.json
|
|
12
|
-
var version = "1.
|
|
12
|
+
var version = "1.19.0-alpha.0";
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/vector/filter.ts
|
|
15
15
|
/**
|
|
@@ -81,6 +81,10 @@ var MongoDBFilterTranslator = class extends BaseFilterTranslator {
|
|
|
81
81
|
};
|
|
82
82
|
//#endregion
|
|
83
83
|
//#region src/vector/index.ts
|
|
84
|
+
/** Name an index's embedding mode for a conflict message. */
|
|
85
|
+
function describeEmbedding(config) {
|
|
86
|
+
return config ? `autoEmbed (path "${config.path}", model "${config.model}")` : "client-side vectors";
|
|
87
|
+
}
|
|
84
88
|
var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
85
89
|
client;
|
|
86
90
|
db;
|
|
@@ -185,7 +189,8 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
185
189
|
searchIndexName: persisted.searchIndexName ?? `${indexName}_vector_index`,
|
|
186
190
|
isByo,
|
|
187
191
|
allowWrites: isByo ? persisted.allowWrites ?? false : true,
|
|
188
|
-
textSearchIndexName: persisted.textSearchIndexName
|
|
192
|
+
textSearchIndexName: persisted.textSearchIndexName,
|
|
193
|
+
autoEmbed: persisted.autoEmbed
|
|
189
194
|
};
|
|
190
195
|
this.indexTargets.set(indexName, target);
|
|
191
196
|
return {
|
|
@@ -288,9 +293,17 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
288
293
|
* Persist (upsert) a logical-index target so it survives a process restart. Idempotent
|
|
289
294
|
* createIndex refreshes it; a partial update (e.g. only the text-search-index name) merges
|
|
290
295
|
* into the existing entry.
|
|
296
|
+
*
|
|
297
|
+
* `claim` narrows the upsert to entries the caller considers compatible. Because the filter
|
|
298
|
+
* still pins `_id`, an incompatible entry matches nothing, the upsert attempts an insert, and
|
|
299
|
+
* the duplicate `_id` surfaces as a duplicate-key error (code 11000) instead of overwriting
|
|
300
|
+
* the entry someone else registered. Callers that only refresh part of an entry omit it.
|
|
291
301
|
*/
|
|
292
|
-
async writeRegistryEntry(indexName, entry) {
|
|
293
|
-
await this.db.collection(MongoDBVector.REGISTRY_COLLECTION).updateOne({
|
|
302
|
+
async writeRegistryEntry(indexName, entry, claim) {
|
|
303
|
+
await this.db.collection(MongoDBVector.REGISTRY_COLLECTION).updateOne({
|
|
304
|
+
_id: indexName,
|
|
305
|
+
...claim
|
|
306
|
+
}, { $set: {
|
|
294
307
|
indexName,
|
|
295
308
|
...entry
|
|
296
309
|
} }, { upsert: true });
|
|
@@ -324,6 +337,31 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
324
337
|
text: `hybridQuery uses $rankFusion, which requires MongoDB >= 8.0 (found ${this.rankFusionSupport.version}). On 8.0.x it may need a MongoDB support case to enable; upgrade to >= 8.1, or run query() and textQuery() separately and fuse client-side.`
|
|
325
338
|
});
|
|
326
339
|
}
|
|
340
|
+
/** Validates a text upsert: `documents` is present, and `metadata`/`ids` line up with it. */
|
|
341
|
+
validateTextUpsertInput(indexName, documents, vectors, metadata, ids) {
|
|
342
|
+
const fail = (errorType, message, details = {}) => {
|
|
343
|
+
throw new MastraError({
|
|
344
|
+
id: createVectorErrorId("MONGODB", "UPSERT", errorType),
|
|
345
|
+
domain: ErrorDomain.STORAGE,
|
|
346
|
+
category: ErrorCategory.USER,
|
|
347
|
+
text: message,
|
|
348
|
+
details: {
|
|
349
|
+
indexName,
|
|
350
|
+
...details
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
if (vectors?.length) fail("INVALID_ARGS", `Index "${indexName}" was created with autoEmbed, so MongoDB generates the embeddings: pass documents only. The supplied vectors would be stored in a field no index reads.`);
|
|
355
|
+
if (!documents || documents.length === 0) fail("EMPTY_DOCUMENTS", "documents array cannot be empty when upserting into an autoEmbed index");
|
|
356
|
+
if (metadata && metadata.length > 0 && metadata.length !== documents.length) fail("METADATA_LENGTH_MISMATCH", "Metadata array length must match documents array length", {
|
|
357
|
+
documentsLength: documents.length,
|
|
358
|
+
metadataLength: metadata.length
|
|
359
|
+
});
|
|
360
|
+
if (ids && ids.length !== documents.length) fail("IDS_LENGTH_MISMATCH", "IDs array length must match documents array length", {
|
|
361
|
+
documentsLength: documents.length,
|
|
362
|
+
idsLength: ids.length
|
|
363
|
+
});
|
|
364
|
+
}
|
|
327
365
|
/**
|
|
328
366
|
* Builds the projection pipeline stage(s) for query results based on metadata mode and
|
|
329
367
|
* whether to include vectors. Shared across query, textQuery, and hybridQuery to keep
|
|
@@ -350,7 +388,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
350
388
|
* @param scoreMeta - metadata field holding the relevance score for the search stage in use
|
|
351
389
|
* (`vectorSearchScore` for $vectorSearch, `searchScore` for $search, `score` for $rankFusion).
|
|
352
390
|
*/
|
|
353
|
-
buildProjection(metadataMode, includeVector, scoreMeta) {
|
|
391
|
+
buildProjection(metadataMode, includeVector, scoreMeta, documentPath = this.documentFieldName) {
|
|
354
392
|
const score = { $meta: scoreMeta };
|
|
355
393
|
if (metadataMode === "document") {
|
|
356
394
|
const project = {
|
|
@@ -366,7 +404,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
366
404
|
_id: 1,
|
|
367
405
|
score,
|
|
368
406
|
metadata: `$${this.metadataFieldName}`,
|
|
369
|
-
document: `$${
|
|
407
|
+
document: `$${documentPath}`,
|
|
370
408
|
...includeVector && { vector: `$${this.embeddingFieldName}` }
|
|
371
409
|
} }];
|
|
372
410
|
}
|
|
@@ -403,10 +441,14 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
403
441
|
* queryable. Skipping that step on a real Atlas cluster may cause
|
|
404
442
|
* "index not found" or "index not ready" errors on subsequent operations.
|
|
405
443
|
*/
|
|
406
|
-
async createIndex(
|
|
444
|
+
async createIndex(params) {
|
|
445
|
+
const { indexName, dimension, metric = "cosine", filterFields, collectionName, searchIndexName, allowWrites, autoEmbed } = params;
|
|
407
446
|
let mongoMetric;
|
|
408
447
|
try {
|
|
409
|
-
if (
|
|
448
|
+
if (autoEmbed) {
|
|
449
|
+
if (dimension !== void 0) throw new Error("dimension cannot be combined with autoEmbed. The embedding model determines the vector size; use autoEmbed.numDimensions to override it.");
|
|
450
|
+
if (params.metric !== void 0) throw new Error("metric cannot be combined with autoEmbed. metric configures a client-side vector field; use autoEmbed.similarity instead.");
|
|
451
|
+
} else if (dimension === void 0 || !Number.isInteger(dimension) || dimension <= 0) throw new Error("Dimension must be a positive integer");
|
|
410
452
|
mongoMetric = this.mongoMetricMap[metric];
|
|
411
453
|
if (!mongoMetric) throw new Error(`Invalid metric: "${metric}". Must be one of: cosine, euclidean, dotproduct`);
|
|
412
454
|
} catch (error) {
|
|
@@ -416,7 +458,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
416
458
|
category: ErrorCategory.USER,
|
|
417
459
|
details: {
|
|
418
460
|
indexName,
|
|
419
|
-
dimension,
|
|
461
|
+
...dimension !== void 0 ? { dimension } : {},
|
|
420
462
|
metric
|
|
421
463
|
}
|
|
422
464
|
}, error);
|
|
@@ -456,40 +498,87 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
456
498
|
text: `searchIndexName "${targetSearchIndex}" collides with the full-text search index name for collection "${targetCollection}". Choose a different vector searchIndexName.`
|
|
457
499
|
});
|
|
458
500
|
const textSearchIndexName = effectiveIsByo ? existingEntry?.textSearchIndexName : existingEntry?.textSearchIndexName ?? defaultTextSearchIndex;
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
501
|
+
const resolvedAutoEmbed = autoEmbed ? {
|
|
502
|
+
...autoEmbed,
|
|
503
|
+
path: autoEmbed.path ?? this.documentFieldName
|
|
504
|
+
} : void 0;
|
|
505
|
+
const existingAutoEmbed = existingEntry?.autoEmbed;
|
|
506
|
+
const embeddingChanged = Boolean(existingAutoEmbed) !== Boolean(resolvedAutoEmbed) || existingAutoEmbed && resolvedAutoEmbed && (existingAutoEmbed.path !== resolvedAutoEmbed.path || existingAutoEmbed.model !== resolvedAutoEmbed.model);
|
|
507
|
+
if (existingEntry && embeddingChanged) throw new MastraError({
|
|
508
|
+
id: createVectorErrorId("MONGODB", "CREATE_INDEX", "CONFLICT"),
|
|
509
|
+
domain: ErrorDomain.STORAGE,
|
|
510
|
+
category: ErrorCategory.USER,
|
|
511
|
+
details: { indexName },
|
|
512
|
+
text: `Index "${indexName}" is already registered with ${describeEmbedding(existingAutoEmbed)}, but this createIndex call resolves to ${describeEmbedding(resolvedAutoEmbed)}. Call deleteIndex({ indexName: "${indexName}" }) first to rebuild it.`
|
|
467
513
|
});
|
|
514
|
+
const registryClaim = {
|
|
515
|
+
collectionName: targetCollection,
|
|
516
|
+
...resolvedAutoEmbed ? {
|
|
517
|
+
"autoEmbed.path": resolvedAutoEmbed.path,
|
|
518
|
+
"autoEmbed.model": resolvedAutoEmbed.model
|
|
519
|
+
} : { autoEmbed: null }
|
|
520
|
+
};
|
|
521
|
+
try {
|
|
522
|
+
await this.writeRegistryEntry(indexName, {
|
|
523
|
+
collectionName: targetCollection,
|
|
524
|
+
searchIndexName: targetSearchIndex,
|
|
525
|
+
isByo: effectiveIsByo,
|
|
526
|
+
allowWrites: effectiveWritable,
|
|
527
|
+
textSearchIndexName,
|
|
528
|
+
dimension,
|
|
529
|
+
...resolvedAutoEmbed ? {} : { metric },
|
|
530
|
+
autoEmbed: resolvedAutoEmbed
|
|
531
|
+
}, registryClaim);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
if (error?.code !== 11e3) throw error;
|
|
534
|
+
const winner = await this.readRegistryEntry(indexName);
|
|
535
|
+
throw new MastraError({
|
|
536
|
+
id: createVectorErrorId("MONGODB", "CREATE_INDEX", "CONFLICT"),
|
|
537
|
+
domain: ErrorDomain.STORAGE,
|
|
538
|
+
category: ErrorCategory.USER,
|
|
539
|
+
details: { indexName },
|
|
540
|
+
text: winner ? `Index "${indexName}" was registered concurrently against collection "${winner.collectionName}" with ${describeEmbedding(winner.autoEmbed)}, but this createIndex call resolves to collection "${targetCollection}" with ${describeEmbedding(resolvedAutoEmbed)}. Call deleteIndex({ indexName: "${indexName}" }) first to rebuild it.` : `Index "${indexName}" was registered concurrently by another process and removed again before this call could read it. Retry createIndex.`
|
|
541
|
+
});
|
|
542
|
+
}
|
|
468
543
|
this.indexTargets.set(indexName, {
|
|
469
544
|
collectionName: targetCollection,
|
|
470
545
|
searchIndexName: targetSearchIndex,
|
|
471
546
|
isByo: effectiveIsByo,
|
|
472
547
|
allowWrites: effectiveWritable,
|
|
473
|
-
textSearchIndexName
|
|
548
|
+
textSearchIndexName,
|
|
549
|
+
autoEmbed: resolvedAutoEmbed
|
|
474
550
|
});
|
|
475
551
|
const embeddingField = this.embeddingFieldName;
|
|
476
552
|
const numDimensions = dimension;
|
|
477
553
|
const declaredMetadataPaths = this.buildDeclaredMetadataPaths(filterFields);
|
|
554
|
+
const autoEmbedPath = resolvedAutoEmbed?.path;
|
|
555
|
+
let searchField;
|
|
556
|
+
if (autoEmbed) {
|
|
557
|
+
const { model, path: _path, ...indexOptions } = autoEmbed;
|
|
558
|
+
searchField = {
|
|
559
|
+
type: "autoEmbed",
|
|
560
|
+
modality: "text",
|
|
561
|
+
path: autoEmbedPath,
|
|
562
|
+
model,
|
|
563
|
+
...indexOptions
|
|
564
|
+
};
|
|
565
|
+
} else searchField = {
|
|
566
|
+
type: "vector",
|
|
567
|
+
path: embeddingField,
|
|
568
|
+
numDimensions,
|
|
569
|
+
similarity: mongoMetric
|
|
570
|
+
};
|
|
571
|
+
const documentIsEmbedded = autoEmbedPath === this.documentFieldName;
|
|
478
572
|
const fields = [
|
|
479
|
-
|
|
480
|
-
type: "vector",
|
|
481
|
-
path: embeddingField,
|
|
482
|
-
numDimensions,
|
|
483
|
-
similarity: mongoMetric
|
|
484
|
-
},
|
|
573
|
+
searchField,
|
|
485
574
|
{
|
|
486
575
|
type: "filter",
|
|
487
576
|
path: "_id"
|
|
488
577
|
},
|
|
489
|
-
{
|
|
578
|
+
...documentIsEmbedded ? [] : [{
|
|
490
579
|
type: "filter",
|
|
491
580
|
path: this.documentFieldName
|
|
492
|
-
},
|
|
581
|
+
}],
|
|
493
582
|
...declaredMetadataPaths.map((path) => ({
|
|
494
583
|
type: "filter",
|
|
495
584
|
path
|
|
@@ -499,7 +588,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
499
588
|
definition: { fields },
|
|
500
589
|
name: indexNameInternal,
|
|
501
590
|
type: "vectorSearch"
|
|
502
|
-
})) this.declaredFilterPaths.set(indexName,
|
|
591
|
+
})) this.declaredFilterPaths.set(indexName, new Set(documentIsEmbedded ? declaredMetadataPaths : [this.documentFieldName, ...declaredMetadataPaths]));
|
|
503
592
|
if (!effectiveIsByo) await this.createSearchIndexIgnoringExisting(collection, {
|
|
504
593
|
definition: { mappings: { dynamic: true } },
|
|
505
594
|
name: textSearchIndexName ?? defaultTextSearchIndex,
|
|
@@ -606,7 +695,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
606
695
|
*/
|
|
607
696
|
async textQuery(params) {
|
|
608
697
|
const { indexName, query, paths, topK = 10, filter, metadataMode = "field", searchIndexName } = params;
|
|
609
|
-
const { collectionName } = await this.resolveIndexTarget(indexName);
|
|
698
|
+
const { collectionName, autoEmbed } = await this.resolveIndexTarget(indexName);
|
|
610
699
|
const textIndex = searchIndexName ?? await this.resolveTextSearchIndexName(indexName);
|
|
611
700
|
try {
|
|
612
701
|
const collection = await this.getCollection(collectionName, true);
|
|
@@ -621,7 +710,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
621
710
|
} },
|
|
622
711
|
...Object.keys(metadataFilter).length ? [{ $match: metadataFilter }] : [],
|
|
623
712
|
{ $limit: Math.min(1e4, topK) },
|
|
624
|
-
...this.buildProjection(metadataMode, false, "searchScore")
|
|
713
|
+
...this.buildProjection(metadataMode, false, "searchScore", autoEmbed?.path)
|
|
625
714
|
];
|
|
626
715
|
return (await collection.aggregate(pipeline).toArray()).map((r) => ({
|
|
627
716
|
id: this.idToString(r._id),
|
|
@@ -644,9 +733,37 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
644
733
|
* need a MongoDB support case to enable, and runs where enabled, e.g. Atlas 8.0.x).
|
|
645
734
|
*/
|
|
646
735
|
async hybridQuery(params) {
|
|
647
|
-
const { indexName, queryVector, query, paths, topK = 10, filter, weights, numCandidates, metadataMode = "field", textSearchIndexName } = params;
|
|
736
|
+
const { indexName, queryVector, queryText, model, query, paths, topK = 10, filter, weights, numCandidates, metadataMode = "field", textSearchIndexName } = params;
|
|
737
|
+
if (model && !queryText) throw new MastraError({
|
|
738
|
+
id: createVectorErrorId("MONGODB", "HYBRID_QUERY", "INVALID_ARGS"),
|
|
739
|
+
text: "model only applies to queryText, which MongoDB embeds server-side for the vector branch.",
|
|
740
|
+
domain: ErrorDomain.STORAGE,
|
|
741
|
+
category: ErrorCategory.USER,
|
|
742
|
+
details: { indexName }
|
|
743
|
+
});
|
|
744
|
+
if (queryVector && queryText) throw new MastraError({
|
|
745
|
+
id: createVectorErrorId("MONGODB", "HYBRID_QUERY", "MUTUALLY_EXCLUSIVE"),
|
|
746
|
+
text: "queryVector and queryText are mutually exclusive: the vector branch takes one or the other.",
|
|
747
|
+
domain: ErrorDomain.STORAGE,
|
|
748
|
+
category: ErrorCategory.USER,
|
|
749
|
+
details: { indexName }
|
|
750
|
+
});
|
|
751
|
+
if (!queryVector && !queryText) throw new MastraError({
|
|
752
|
+
id: createVectorErrorId("MONGODB", "HYBRID_QUERY", "MISSING_VECTOR"),
|
|
753
|
+
text: "The vector branch needs queryVector (or queryText on an autoEmbed index).",
|
|
754
|
+
domain: ErrorDomain.STORAGE,
|
|
755
|
+
category: ErrorCategory.USER,
|
|
756
|
+
details: { indexName }
|
|
757
|
+
});
|
|
648
758
|
await this.assertRankFusionSupported();
|
|
649
|
-
const { collectionName, searchIndexName } = await this.resolveIndexTarget(indexName);
|
|
759
|
+
const { collectionName, searchIndexName, autoEmbed } = await this.resolveIndexTarget(indexName);
|
|
760
|
+
if (queryText && !autoEmbed) throw new MastraError({
|
|
761
|
+
id: createVectorErrorId("MONGODB", "HYBRID_QUERY", "NOT_AUTO_EMBED"),
|
|
762
|
+
text: `Index "${indexName}" was not created with autoEmbed, so MongoDB cannot embed a query string for it. Pass queryVector instead.`,
|
|
763
|
+
domain: ErrorDomain.STORAGE,
|
|
764
|
+
category: ErrorCategory.USER,
|
|
765
|
+
details: { indexName }
|
|
766
|
+
});
|
|
650
767
|
const textIndex = textSearchIndexName ?? await this.resolveTextSearchIndexName(indexName);
|
|
651
768
|
try {
|
|
652
769
|
const collection = await this.getCollection(collectionName, true);
|
|
@@ -656,8 +773,9 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
656
773
|
const candidates = Math.min(1e4, Math.max(perBranch, numCandidates ?? topK * 20));
|
|
657
774
|
const vectorSearch = {
|
|
658
775
|
index: searchIndexName,
|
|
659
|
-
path: this.embeddingFieldName,
|
|
660
|
-
queryVector,
|
|
776
|
+
path: autoEmbed ? autoEmbed.path : this.embeddingFieldName,
|
|
777
|
+
...queryText ? { query: { text: queryText } } : { queryVector },
|
|
778
|
+
...model ? { model } : {},
|
|
661
779
|
numCandidates: candidates,
|
|
662
780
|
limit: perBranch
|
|
663
781
|
};
|
|
@@ -699,7 +817,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
699
817
|
...combination ? { combination } : {}
|
|
700
818
|
} },
|
|
701
819
|
{ $limit: Math.min(1e4, topK) },
|
|
702
|
-
...this.buildProjection(metadataMode, false, "score")
|
|
820
|
+
...this.buildProjection(metadataMode, false, "score", autoEmbed?.path)
|
|
703
821
|
];
|
|
704
822
|
return (await collection.aggregate(pipeline).toArray()).map((r) => ({
|
|
705
823
|
id: this.idToString(r._id),
|
|
@@ -773,17 +891,24 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
773
891
|
* @returns The IDs of the upserted vectors in input order.
|
|
774
892
|
*/
|
|
775
893
|
async upsert({ indexName, vectors, metadata, ids, documents }) {
|
|
776
|
-
|
|
777
|
-
|
|
894
|
+
const { collectionName, autoEmbed } = await this.resolveIndexTarget(indexName);
|
|
895
|
+
if (autoEmbed) this.validateTextUpsertInput(indexName, documents, vectors, metadata, ids);
|
|
896
|
+
else {
|
|
897
|
+
validateUpsertInput("MONGODB", vectors, metadata, ids);
|
|
898
|
+
validateVectorValues("MONGODB", vectors);
|
|
899
|
+
}
|
|
778
900
|
await this.assertWritable(indexName, "UPSERT");
|
|
779
901
|
if (metadata) this.assertNoCircularReferences(metadata, "UPSERT");
|
|
780
902
|
try {
|
|
781
|
-
const { collectionName } = await this.resolveIndexTarget(indexName);
|
|
782
903
|
const collection = await this.getCollection(collectionName);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
904
|
+
if (!autoEmbed) {
|
|
905
|
+
const stats = await this.describeIndex({ indexName });
|
|
906
|
+
await this.validateVectorDimensions(vectors, stats.dimension);
|
|
907
|
+
}
|
|
908
|
+
const rowCount = (autoEmbed ? documents : vectors)?.length ?? 0;
|
|
909
|
+
const generatedIds = ids || Array.from({ length: rowCount }, () => v4());
|
|
910
|
+
const operations = Array.from({ length: rowCount }, (_unused, idx) => {
|
|
911
|
+
const vector = vectors?.[idx];
|
|
787
912
|
const id = generatedIds[idx];
|
|
788
913
|
const meta = metadata?.[idx] || {};
|
|
789
914
|
const doc = documents?.[idx];
|
|
@@ -792,10 +917,10 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
792
917
|
return acc;
|
|
793
918
|
}, {});
|
|
794
919
|
const updateDoc = {
|
|
795
|
-
[this.embeddingFieldName]: vector,
|
|
920
|
+
...autoEmbed ? {} : { [this.embeddingFieldName]: vector },
|
|
796
921
|
[this.metadataFieldName]: normalizedMeta
|
|
797
922
|
};
|
|
798
|
-
if (doc !== void 0) updateDoc[this.documentFieldName] = doc;
|
|
923
|
+
if (doc !== void 0) updateDoc[autoEmbed ? autoEmbed.path : this.documentFieldName] = doc;
|
|
799
924
|
const idMatch = this.buildIdMatch(id);
|
|
800
925
|
return { updateOne: {
|
|
801
926
|
filter: { _id: idMatch },
|
|
@@ -833,42 +958,78 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
833
958
|
* recall at the cost of latency. Defaults to 20 * topK, capped at 10000.
|
|
834
959
|
* @returns Array of results ordered by descending similarity score.
|
|
835
960
|
*/
|
|
836
|
-
async query({ indexName, queryVector, topK = 10, filter, includeVector = false, documentFilter, numCandidates, metadataMode = "field" }) {
|
|
837
|
-
if (!
|
|
961
|
+
async query({ indexName, queryVector, queryText, model, topK = 10, filter, includeVector = false, documentFilter, numCandidates, metadataMode = "field" }) {
|
|
962
|
+
if (model && !queryText) throw new MastraError({
|
|
963
|
+
id: createVectorErrorId("MONGODB", "QUERY", "INVALID_ARGS"),
|
|
964
|
+
text: "model only applies to queryText, which MongoDB embeds server-side. A precomputed queryVector has already been embedded.",
|
|
965
|
+
domain: ErrorDomain.STORAGE,
|
|
966
|
+
category: ErrorCategory.USER,
|
|
967
|
+
details: { indexName }
|
|
968
|
+
});
|
|
969
|
+
if (queryVector && queryText) throw new MastraError({
|
|
970
|
+
id: createVectorErrorId("MONGODB", "QUERY", "MUTUALLY_EXCLUSIVE"),
|
|
971
|
+
text: "queryVector and queryText are mutually exclusive: pass a vector you embedded yourself, or a string for MongoDB to embed server-side.",
|
|
972
|
+
domain: ErrorDomain.STORAGE,
|
|
973
|
+
category: ErrorCategory.USER,
|
|
974
|
+
details: { indexName }
|
|
975
|
+
});
|
|
976
|
+
if (!queryVector && !queryText) throw new MastraError({
|
|
838
977
|
id: createVectorErrorId("MONGODB", "QUERY", "MISSING_VECTOR"),
|
|
839
|
-
text: "queryVector is required for MongoDB queries. Metadata-only queries are not supported by this vector store.",
|
|
978
|
+
text: "queryVector (or queryText on an autoEmbed index) is required for MongoDB queries. Metadata-only queries are not supported by this vector store.",
|
|
979
|
+
domain: ErrorDomain.STORAGE,
|
|
980
|
+
category: ErrorCategory.USER,
|
|
981
|
+
details: { indexName }
|
|
982
|
+
});
|
|
983
|
+
const { collectionName, searchIndexName, autoEmbed, isByo } = await this.resolveIndexTarget(indexName);
|
|
984
|
+
if (queryText && !autoEmbed) throw new MastraError({
|
|
985
|
+
id: createVectorErrorId("MONGODB", "QUERY", "NOT_AUTO_EMBED"),
|
|
986
|
+
text: `Index "${indexName}" was not created with autoEmbed, so MongoDB cannot embed a query string for it. Pass queryVector instead, or use textQuery() for full-text search.`,
|
|
987
|
+
domain: ErrorDomain.STORAGE,
|
|
988
|
+
category: ErrorCategory.USER,
|
|
989
|
+
details: { indexName }
|
|
990
|
+
});
|
|
991
|
+
if (includeVector && autoEmbed) throw new MastraError({
|
|
992
|
+
id: createVectorErrorId("MONGODB", "QUERY", "INVALID_ARGS"),
|
|
993
|
+
text: `includeVector is not supported on autoEmbed index "${indexName}": MongoDB keeps the generated embeddings in its own internal database, so they are not on the returned documents.`,
|
|
840
994
|
domain: ErrorDomain.STORAGE,
|
|
841
995
|
category: ErrorCategory.USER,
|
|
842
996
|
details: { indexName }
|
|
843
997
|
});
|
|
844
998
|
try {
|
|
845
|
-
const { collectionName, searchIndexName } = await this.resolveIndexTarget(indexName);
|
|
846
999
|
const collection = await this.getCollection(collectionName, true);
|
|
847
1000
|
const indexNameInternal = searchIndexName;
|
|
848
1001
|
const metadataFilter = this.transformMetadataFilter(this.transformFilter(filter), metadataMode);
|
|
849
1002
|
const hasMetadataFilter = Object.keys(metadataFilter).length > 0;
|
|
850
1003
|
const vectorSearch = {
|
|
851
1004
|
index: indexNameInternal,
|
|
852
|
-
queryVector,
|
|
853
|
-
|
|
1005
|
+
...queryText ? { query: { text: queryText } } : { queryVector },
|
|
1006
|
+
...model ? { model } : {},
|
|
1007
|
+
path: autoEmbed ? autoEmbed.path : this.embeddingFieldName,
|
|
854
1008
|
numCandidates: Math.min(1e4, Math.max(topK, numCandidates ?? topK * 20)),
|
|
855
1009
|
limit: Math.min(1e4, topK)
|
|
856
1010
|
};
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1011
|
+
const documentTextField = autoEmbed ? autoEmbed.path : this.documentFieldName;
|
|
1012
|
+
const documentFilterExpr = documentFilter ? { [documentTextField]: documentFilter } : void 0;
|
|
1013
|
+
const verifyDocumentPath = Boolean(documentFilterExpr) && !autoEmbed && isByo;
|
|
1014
|
+
if (hasMetadataFilter || documentFilterExpr) {
|
|
1015
|
+
let declaredPaths = /* @__PURE__ */ new Set();
|
|
1016
|
+
if (hasMetadataFilter || verifyDocumentPath) try {
|
|
860
1017
|
declaredPaths = await this.getDeclaredFilterPaths(indexName);
|
|
861
1018
|
} catch {
|
|
862
1019
|
declaredPaths = /* @__PURE__ */ new Set();
|
|
863
1020
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1021
|
+
const pushMetadata = !hasMetadataFilter || this.canPushDownFilter(metadataFilter, declaredPaths);
|
|
1022
|
+
const pushDocument = !documentFilterExpr || !autoEmbed && (!verifyDocumentPath || declaredPaths.has(documentTextField));
|
|
1023
|
+
const pushed = [...hasMetadataFilter && pushMetadata ? [metadataFilter] : [], ...documentFilterExpr && pushDocument ? [documentFilterExpr] : []];
|
|
1024
|
+
if (!pushMetadata || !pushDocument) {
|
|
1025
|
+
const preFilter = [...hasMetadataFilter && !pushMetadata ? [metadataFilter] : [], ...documentFilterExpr && !pushDocument ? [documentFilterExpr] : []];
|
|
1026
|
+
const candidateIds = await collection.aggregate([{ $match: preFilter.length > 1 ? { $and: preFilter } : preFilter[0] }, { $project: { _id: 1 } }]).map((doc) => doc._id).toArray();
|
|
867
1027
|
if (candidateIds.length === 0) return [];
|
|
868
|
-
|
|
1028
|
+
pushed.unshift({ _id: { $in: candidateIds } });
|
|
869
1029
|
}
|
|
870
|
-
|
|
871
|
-
|
|
1030
|
+
vectorSearch.filter = pushed.length > 1 ? { $and: pushed } : pushed[0];
|
|
1031
|
+
}
|
|
1032
|
+
const pipeline = [{ $vectorSearch: vectorSearch }, ...this.buildProjection(metadataMode, includeVector, "vectorSearchScore", autoEmbed?.path)];
|
|
872
1033
|
return (await collection.aggregate(pipeline).toArray()).map((result) => ({
|
|
873
1034
|
id: this.idToString(result._id),
|
|
874
1035
|
score: result.score,
|
|
@@ -936,10 +1097,11 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
936
1097
|
*/
|
|
937
1098
|
async describeIndex({ indexName }) {
|
|
938
1099
|
try {
|
|
939
|
-
const { collectionName, searchIndexName } = await this.resolveIndexTarget(indexName);
|
|
1100
|
+
const { collectionName, searchIndexName, autoEmbed } = await this.resolveIndexTarget(indexName);
|
|
940
1101
|
const collection = await this.getCollection(collectionName, true);
|
|
1102
|
+
const embeddedFieldName = autoEmbed ? autoEmbed.path : this.embeddingFieldName;
|
|
941
1103
|
const count = await collection.countDocuments({
|
|
942
|
-
[
|
|
1104
|
+
[embeddedFieldName]: { $exists: true },
|
|
943
1105
|
_id: { $ne: "__index_metadata__" }
|
|
944
1106
|
});
|
|
945
1107
|
const indexNameInternal = searchIndexName;
|
|
@@ -951,7 +1113,7 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
951
1113
|
details: { indexName },
|
|
952
1114
|
text: `Atlas Search index "${indexNameInternal}" does not exist on collection "${indexName}". The collection may predate Mastra or the index may have been dropped externally.`
|
|
953
1115
|
});
|
|
954
|
-
const vectorField = indexData.latestDefinition?.fields?.find((f) => f.type === "vector");
|
|
1116
|
+
const vectorField = indexData.latestDefinition?.fields?.find((f) => f.type === "vector" || f.type === "autoEmbed");
|
|
955
1117
|
if (!vectorField) throw new MastraError({
|
|
956
1118
|
id: createVectorErrorId("MONGODB", "DESCRIBE_INDEX", "INVALID"),
|
|
957
1119
|
domain: ErrorDomain.STORAGE,
|
|
@@ -960,9 +1122,9 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
960
1122
|
text: `Atlas Search index "${indexNameInternal}" exists but has no vector field. The index may have been created outside of Mastra or without vector configuration.`
|
|
961
1123
|
});
|
|
962
1124
|
return {
|
|
963
|
-
dimension: vectorField.numDimensions,
|
|
1125
|
+
dimension: vectorField.type === "autoEmbed" ? vectorField.numDimensions ?? 1024 : vectorField.numDimensions,
|
|
964
1126
|
count,
|
|
965
|
-
metric: {
|
|
1127
|
+
metric: vectorField.type === "autoEmbed" && vectorField.similarity === void 0 ? void 0 : {
|
|
966
1128
|
cosine: "cosine",
|
|
967
1129
|
euclidean: "euclidean",
|
|
968
1130
|
dotProduct: "dotproduct"
|
|
@@ -1045,9 +1207,16 @@ var MongoDBVector = class MongoDBVector extends MastraVector {
|
|
|
1045
1207
|
if (update.metadata) this.assertNoCircularReferences(update.metadata, "UPDATE_VECTOR");
|
|
1046
1208
|
try {
|
|
1047
1209
|
if (!update.vector && !update.metadata) throw new Error("No updates provided");
|
|
1048
|
-
const { collectionName } = await this.resolveIndexTarget(indexName);
|
|
1210
|
+
const { collectionName, autoEmbed } = await this.resolveIndexTarget(indexName);
|
|
1049
1211
|
const collection = await this.getCollection(collectionName, true);
|
|
1050
1212
|
const updateDoc = {};
|
|
1213
|
+
if (update.vector && autoEmbed) throw new MastraError({
|
|
1214
|
+
id: createVectorErrorId("MONGODB", "UPDATE_VECTOR", "INVALID_ARGS"),
|
|
1215
|
+
domain: ErrorDomain.STORAGE,
|
|
1216
|
+
category: ErrorCategory.USER,
|
|
1217
|
+
details: { indexName },
|
|
1218
|
+
text: `Index "${indexName}" was created with autoEmbed, so MongoDB owns the embeddings and a vector cannot be written directly. Upsert the document with new text instead.`
|
|
1219
|
+
});
|
|
1051
1220
|
if (update.vector) {
|
|
1052
1221
|
const stats = await this.describeIndex({ indexName });
|
|
1053
1222
|
await this.validateVectorDimensions([update.vector], stats.dimension);
|