Skip to content

Commit

Permalink
Merge pull request #14251 from Automattic/vkarpov15/gh-14232
Browse files Browse the repository at this point in the history
Add Atlas search index helpers to Models and Schemas
  • Loading branch information
vkarpov15 committed Jan 15, 2024
2 parents dad0da9 + 33b3bba commit 3393c96
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 8 deletions.
20 changes: 20 additions & 0 deletions docs/guide.md
Expand Up @@ -558,6 +558,7 @@ Valid options:
* [collectionOptions](#collectionOptions)
* [methods](#methods)
* [query](#query-helpers)
* [autoSearchIndex](#autoSearchIndex)

<h2 id="autoIndex"><a href="#autoIndex">option: autoIndex</a></h2>

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

<h2 id="autoSearchIndex">
<a href="#autoSearchIndex">
option: autoSearchIndex
</a>
</h2>

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);
``

<h2 id="es6-classes"><a href="#es6-classes">With ES6 Classes</a></h2>

Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass)
Expand Down
3 changes: 2 additions & 1 deletion lib/connection.js
Expand Up @@ -1003,7 +1003,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;
Expand Down
5 changes: 5 additions & 0 deletions lib/drivers/node-mongodb-native/connection.js
Expand Up @@ -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 || {};
Expand Down
97 changes: 93 additions & 4 deletions lib/model.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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 => {
Expand All @@ -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;
Expand Down Expand Up @@ -1506,6 +1529,72 @@ 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);
};

/**
* 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
* @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()`.
*
Expand Down
3 changes: 2 additions & 1 deletion lib/mongoose.js
Expand Up @@ -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) {
Expand Down
24 changes: 24 additions & 0 deletions lib/schema.js
Expand Up @@ -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) || {};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -908,6 +910,28 @@ 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 } } });
*
* @param {Object} description index options, including `name` and `definition`
* @param {String} description.name
* @param {Object} description.definition
* @return {Schema} the Schema instance
* @api public
*/

Schema.prototype.searchIndex = function searchIndex(description) {
this._searchIndexes.push(description);

return this;
};

/**
* Reserved document keys.
*
Expand Down
1 change: 1 addition & 0 deletions lib/validOptions.js
Expand Up @@ -11,6 +11,7 @@ const VALID_OPTIONS = Object.freeze([
'applyPluginsToDiscriminators',
'autoCreate',
'autoIndex',
'autoSearchIndex',
'bufferCommands',
'bufferTimeoutMS',
'cloneSchemas',
Expand Down
2 changes: 1 addition & 1 deletion types/connection.d.ts
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion types/inferschematype.d.ts
Expand Up @@ -271,7 +271,7 @@ type ResolvePathType<PathValueType, Options extends SchemaTypeOptions<PathValueT
IfEquals<PathValueType, Types.Decimal128> extends true ? Types.Decimal128 :
IfEquals<PathValueType, Schema.Types.BigInt> extends true ? bigint :
IfEquals<PathValueType, BigInt> 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<PathValueType, Schema.Types.UUID> extends true ? Buffer :
PathValueType extends MapConstructor | 'Map' ? Map<string, ResolvePathType<Options['of']>> :
Expand Down
18 changes: 18 additions & 0 deletions types/models.d.ts
Expand Up @@ -245,6 +245,12 @@ declare module 'mongoose' {
*/
createCollection<T extends mongodb.Document>(options?: mongodb.CreateCollectionOptions & Pick<SchemaOptions, 'expires'>): Promise<mongodb.Collection<T>>;

/**
* 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<string>;

/** Connection the model uses. */
db: Connection;

Expand Down Expand Up @@ -298,6 +304,12 @@ 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<void>;

/**
* Event emitter that reports any errors that occurred. Useful for global error
* handling.
Expand Down Expand Up @@ -473,6 +485,12 @@ declare module 'mongoose' {
doc: any, options: PopulateOptions | Array<PopulateOptions> | string
): Promise<MergeType<THydratedDocumentType, Paths>>;

/**
* 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<void>;

/** Casts and validates the given object against this model's schema, passing the given `context` to custom validators. */
validate(): Promise<void>;
validate(obj: any): Promise<void>;
Expand Down

0 comments on commit 3393c96

Please sign in to comment.