Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Atlas search index helpers to Models and Schemas #14251

Merged
merged 6 commits into from Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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;
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>;
vkarpov15 marked this conversation as resolved.
Show resolved Hide resolved

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