From 6bd415b35a4a7efe6cbd36b17349d8421a22522b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 11 Jan 2024 10:53:25 -0500 Subject: [PATCH 1/6] feat(model): add createSearchIndex() function Re: #14232 --- lib/model.js | 23 +++++++++++++++++++++++ types/inferschematype.d.ts | 2 +- types/models.d.ts | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 8d9666df0ef..922360862b9 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1506,6 +1506,29 @@ Model.syncIndexes = async function syncIndexes(options) { return dropped; }; +/** + * Create an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/). + * This function only works when connected to MongoDB Atlas. + * + * #### Example: + * + * const schema = new Schema({ name: { type: String, unique: true } }); + * const Customer = mongoose.model('Customer', schema); + * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }) + * + * @param {Object} description index options, including `name` and `definition` + * @param {String} description.name + * @param {Object} description.definition + * @return {Promise} + * @api public + */ + +Model.createSearchIndex = async function createSearchIndex(description) { + _checkContext(this, 'createSearchIndex'); + + return await this.$__collection.createSearchIndex(description); +}; + /** * Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`. * diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 02f3241dd30..8f878b312f4 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -271,7 +271,7 @@ type ResolvePathType extends true ? Types.Decimal128 : IfEquals extends true ? bigint : IfEquals extends true ? bigint : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : IfEquals extends true ? Buffer : PathValueType extends MapConstructor | 'Map' ? Map> : diff --git a/types/models.d.ts b/types/models.d.ts index 976c962687c..22362d5dcde 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -245,6 +245,8 @@ declare module 'mongoose' { */ createCollection(options?: mongodb.CreateCollectionOptions & Pick): Promise>; + createSearchIndex(description: mongodb.SearchIndexDescription): Promise; + /** Connection the model uses. */ db: Connection; From a76a99a9af916db6448cc18a8fad27eb280a175b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 11 Jan 2024 11:24:56 -0500 Subject: [PATCH 2/6] feat(model): add updateSearchIndex and dropSearchIndex helpers re: #14232 --- lib/model.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ types/models.d.ts | 4 ++++ 2 files changed, 48 insertions(+) diff --git a/lib/model.js b/lib/model.js index 922360862b9..fb71f9ebc23 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1529,6 +1529,50 @@ Model.createSearchIndex = async function createSearchIndex(description) { return await this.$__collection.createSearchIndex(description); }; +/** + * Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/). + * This function only works when connected to MongoDB Atlas. + * + * #### Example: + * + * const schema = new Schema({ name: { type: String, unique: true } }); + * const Customer = mongoose.model('Customer', schema); + * await Customer.updateSearchIndex('test', { mappings: { dynamic: true } }); + * + * @param {String} name + * @param {Object} definition + * @return {Promise} + * @api public + */ + +Model.updateSearchIndex = async function updateSearchIndex(name, definition) { + _checkContext(this, 'updateSearchIndex'); + + return await this.$__collection.updateSearchIndex(name, definition); +}; + +/** + * Delete an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) by name. + * This function only works when connected to MongoDB Atlas. + * + * #### Example: + * + * const schema = new Schema({ name: { type: String, unique: true } }); + * const Customer = mongoose.model('Customer', schema); + * await Customer.dropSearchIndex('test'); + * + * @param {String} name + * @param {Object} definition + * @return {Promise} + * @api public + */ + +Model.dropSearchIndex = async function dropSearchIndex(name) { + _checkContext(this, 'dropSearchIndex'); + + return await this.$__collection.dropSearchIndex(name); +}; + /** * Does a dry-run of `Model.syncIndexes()`, returning the indexes that `syncIndexes()` would drop and create if you were to run `syncIndexes()`. * diff --git a/types/models.d.ts b/types/models.d.ts index 22362d5dcde..946121823fc 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -300,6 +300,8 @@ declare module 'mongoose' { 'deleteOne' >; + dropSearchIndex(name: string): Promise; + /** * Event emitter that reports any errors that occurred. Useful for global error * handling. @@ -475,6 +477,8 @@ declare module 'mongoose' { doc: any, options: PopulateOptions | Array | string ): Promise>; + updateSearchIndex(name: string, definition: AnyObject): Promise; + /** Casts and validates the given object against this model's schema, passing the given `context` to custom validators. */ validate(): Promise; validate(obj: any): Promise; From 4611ba2e59311f2335dff1209100382206d97919 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 11 Jan 2024 17:25:10 -0500 Subject: [PATCH 3/6] feat: add autoSearchIndex option and Schema.prototype.searchIndex() re: #14232 --- docs/guide.md | 20 +++++++++++ lib/connection.js | 3 +- lib/drivers/node-mongodb-native/connection.js | 5 +++ lib/model.js | 33 ++++++++++++++++--- lib/mongoose.js | 3 +- lib/schema.js | 21 ++++++++++++ lib/validOptions.js | 1 + types/connection.d.ts | 2 +- 8 files changed, 80 insertions(+), 8 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 06e3ab3f5bd..e42643c18d1 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -558,6 +558,7 @@ Valid options: * [collectionOptions](#collectionOptions) * [methods](#methods) * [query](#query-helpers) +* [autoSearchIndex](#autoSearchIndex)

option: autoIndex

@@ -1453,6 +1454,25 @@ const Test = mongoose.model('Test', schema); await Test.createCollection(); ``` +

+ + option: autoSearchIndex + +

+ +Similar to [`autoIndex`](#autoIndex), except for automatically creates any [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) defined in your schema. +Unlike `autoIndex`, this option defaults to false. + +```javascript +const schema = new Schema({ name: String }, { autoSearchIndex: true }); +schema.searchIndex({ + name: 'my-index', + definition: { mappings: { dynamic: true } } +}); +// Will automatically attempt to create the `my-index` search index. +const Test = mongoose.model('Test', schema); +`` +

With ES6 Classes

Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass) diff --git a/lib/connection.js b/lib/connection.js index 675c84e813c..171ac06a912 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -983,7 +983,8 @@ Connection.prototype.onClose = function(force) { Connection.prototype.collection = function(name, options) { const defaultOptions = { autoIndex: this.config.autoIndex != null ? this.config.autoIndex : this.base.options.autoIndex, - autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate + autoCreate: this.config.autoCreate != null ? this.config.autoCreate : this.base.options.autoCreate, + autoSearchIndex: this.config.autoSearchIndex != null ? this.config.autoSearchIndex : this.base.options.autoSearchIndex }; options = Object.assign({}, defaultOptions, options ? clone(options) : {}); options.$wasForceClosed = this.$wasForceClosed; diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index cfdfd075b89..c0f3261595f 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -246,6 +246,11 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio delete options.sanitizeFilter; } + if ('autoSearchIndex' in options) { + this.config.autoSearchIndex = options.autoSearchIndex; + delete options.autoSearchIndex; + } + // Backwards compat if (options.user || options.pass) { options.auth = options.auth || {}; diff --git a/lib/model.js b/lib/model.js index fb71f9ebc23..36ccce7e8f9 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1273,10 +1273,14 @@ for (const i in EventEmitter.prototype) { } /** - * This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/), - * unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) is turned off. + * This function is responsible for initializing the underlying connection in MongoDB based on schema options. + * This function performs the following operations: * - * Mongoose calls this function automatically when a model is created using + * - `createCollection()` unless [`autoCreate`](https://mongoosejs.com/docs/guide.html#autoCreate) option is turned off + * - `ensureIndexes()` unless [`autoIndex`](https://mongoosejs.com/docs/guide.html#autoIndex) option is turned off + * - `createSearchIndex()` on all schema search indexes if `autoSearchIndex` is enabled. + * + * Mongoose calls this function automatically when a model is a created using * [`mongoose.model()`](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.model()) or * [`connection.model()`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.model()), so you * don't need to call `init()` to trigger index builds. @@ -1324,6 +1328,23 @@ Model.init = function init() { } return await this.ensureIndexes({ _automatic: true }); }; + const _createSearchIndexes = async() => { + const autoSearchIndex = utils.getOption( + 'autoSearchIndex', + this.schema.options, + conn.config, + conn.base.options + ); + if (!autoSearchIndex) { + return; + } + + const results = []; + for (const searchIndex of this.schema._searchIndexes) { + results.push(await this.createSearchIndex(searchIndex)); + } + return results; + }; const _createCollection = async() => { if ((conn.readyState === STATES.connecting || conn.readyState === STATES.disconnected) && conn._shouldBufferCommands()) { await new Promise(resolve => { @@ -1342,7 +1363,9 @@ Model.init = function init() { return await this.createCollection(); }; - this.$init = _createCollection().then(() => _ensureIndexes()); + this.$init = _createCollection(). + then(() => _ensureIndexes()). + then(() => _createSearchIndexes()); const _catch = this.$init.catch; const _this = this; @@ -1514,7 +1537,7 @@ Model.syncIndexes = async function syncIndexes(options) { * * const schema = new Schema({ name: { type: String, unique: true } }); * const Customer = mongoose.model('Customer', schema); - * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }) + * await Customer.createSearchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); * * @param {Object} description index options, including `name` and `definition` * @param {String} description.name diff --git a/lib/mongoose.js b/lib/mongoose.js index f0878728d38..5bee65d3c85 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -65,7 +65,8 @@ function Mongoose(options) { this.options = Object.assign({ pluralization: true, autoIndex: true, - autoCreate: true + autoCreate: true, + autoSearchIndex: false }, options); const createInitialConnection = utils.getOption('createInitialConnection', this.options); if (createInitialConnection == null || createInitialConnection) { diff --git a/lib/schema.js b/lib/schema.js index 4c194023a7b..bc4a78839b0 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -116,6 +116,7 @@ function Schema(obj, options) { this.inherits = {}; this.callQueue = []; this._indexes = []; + this._searchIndexes = []; this.methods = (options && options.methods) || {}; this.methodOptions = {}; this.statics = (options && options.statics) || {}; @@ -411,6 +412,7 @@ Schema.prototype._clone = function _clone(Constructor) { s.query = clone(this.query); s.plugins = Array.prototype.slice.call(this.plugins); s._indexes = clone(this._indexes); + s._searchIndexes = clone(this._searchIndexes); s.s.hooks = this.s.hooks.clone(); s.tree = clone(this.tree); @@ -908,6 +910,25 @@ Schema.prototype.clearIndexes = function clearIndexes() { return this; }; +/** + * Add an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) that Mongoose will create using `Model.createSearchIndex()`. + * This function only works when connected to MongoDB Atlas. + * + * #### Example: + * + * const ToySchema = new Schema({ name: String, color: String, price: Number }); + * ToySchema.searchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); + * + * @return {Schema} the Schema instance + * @api public + */ + +Schema.prototype.searchIndex = function searchIndex(description) { + this._searchIndexes.push(description); + + return this; +}; + /** * Reserved document keys. * diff --git a/lib/validOptions.js b/lib/validOptions.js index af4e116deec..c9968237595 100644 --- a/lib/validOptions.js +++ b/lib/validOptions.js @@ -11,6 +11,7 @@ const VALID_OPTIONS = Object.freeze([ 'applyPluginsToDiscriminators', 'autoCreate', 'autoIndex', + 'autoSearchIndex', 'bufferCommands', 'bufferTimeoutMS', 'cloneSchemas', diff --git a/types/connection.d.ts b/types/connection.d.ts index c34260fde34..9f17b280ec4 100644 --- a/types/connection.d.ts +++ b/types/connection.d.ts @@ -48,7 +48,7 @@ declare module 'mongoose' { pass?: string; /** Set to false to disable automatic index creation for all models associated with this connection. */ autoIndex?: boolean; - /** Set to `true` to make Mongoose automatically call `createCollection()` on every model created on this connection. */ + /** Set to `false` to disable Mongoose automatically calling `createCollection()` on every model created on this connection. */ autoCreate?: boolean; } From d1e3c81d9b074130413f56f8a49251285a8f1695 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 13 Jan 2024 14:59:48 -0500 Subject: [PATCH 4/6] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 36ccce7e8f9..bccd6cdf267 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1585,7 +1585,6 @@ Model.updateSearchIndex = async function updateSearchIndex(name, definition) { * await Customer.dropSearchIndex('test'); * * @param {String} name - * @param {Object} definition * @return {Promise} * @api public */ From ab284966bcd0603a22b6bb2d820a0b545a86b378 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 13 Jan 2024 15:03:28 -0500 Subject: [PATCH 5/6] Update lib/schema.js Co-authored-by: hasezoey --- lib/schema.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/schema.js b/lib/schema.js index bc4a78839b0..6f25a00ccbe 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -919,6 +919,9 @@ Schema.prototype.clearIndexes = function clearIndexes() { * const ToySchema = new Schema({ name: String, color: String, price: Number }); * ToySchema.searchIndex({ name: 'test', definition: { mappings: { dynamic: true } } }); * + * @param {Object} description index options, including `name` and `definition` + * @param {String} description.name + * @param {Object} description.definition * @return {Schema} the Schema instance * @api public */ From 33b3bba66cefee178a47732827ff790ac036a963 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 13 Jan 2024 15:06:39 -0500 Subject: [PATCH 6/6] types: add tsdoc comments re: code review --- types/models.d.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/types/models.d.ts b/types/models.d.ts index 946121823fc..caa4ea62696 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -245,6 +245,10 @@ declare module 'mongoose' { */ createCollection(options?: mongodb.CreateCollectionOptions & Pick): Promise>; + /** + * Create an [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/). + * This function only works when connected to MongoDB Atlas. + */ createSearchIndex(description: mongodb.SearchIndexDescription): Promise; /** Connection the model uses. */ @@ -300,6 +304,10 @@ declare module 'mongoose' { 'deleteOne' >; + /** + * Delete an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) by name. + * This function only works when connected to MongoDB Atlas. + */ dropSearchIndex(name: string): Promise; /** @@ -477,6 +485,10 @@ declare module 'mongoose' { doc: any, options: PopulateOptions | Array | string ): Promise>; + /** + * Update an existing [Atlas search index](https://www.mongodb.com/docs/atlas/atlas-search/create-index/). + * This function only works when connected to MongoDB Atlas. + */ updateSearchIndex(name: string, definition: AnyObject): Promise; /** Casts and validates the given object against this model's schema, passing the given `context` to custom validators. */