Nest.js Tutorial

Definining indexes with MongoDB and Mongoose

Marcin Wanago
JavaScriptMongoDBNestJSTypeScript

The bigger our database is, the more demanding our queries become in terms of computing power. A common way of tackling this problem is by creating indexes. In this article, we explore this concept and create indexes with MongoDB and Mongoose.

When performing a MongoDB query, the database must scan every document in a given collection to find matching documents. MongoDB can limit the number of records to inspect if we have an appropriate index in our database. Since it makes it easier to search for the documents in the database, indexes can speed up finding, updating, and deleting.

Under the hood, indexes are data structures that store a small part of the collection’s data in an easy-to-traverse way. It includes the ordered values of a particular field of the documents. It makes MongoDB indexes similar to indexes in databases such as PostgreSQL.

When we define indexes, MongoDB needs to store additional data to speed up our queries. But, unfortunately, it slows down our write queries. It also takes up more memory. Therefore, we need to create indexes sparingly.

Unique indexes

The unique index makes sure that we don’t store duplicate values. We can create it by passing unique: true to the @Prop decorator.

user.schema.ts
1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2import { Document, ObjectId } from 'mongoose';
3import { Transform } from 'class-transformer';
4 
5export type UserDocument = User & Document;
6 
7@Schema({
8  toJSON: {
9    getters: true,
10    virtuals: true,
11  },
12})
13export class User {
14  @Transform(({ value }) => value.toString())
15  _id: ObjectId;
16 
17  @Prop({ unique: true })
18  email: string;
19 
20  // ...
21}
22 
23const UserSchema = SchemaFactory.createForClass(User);

It is important to know that MongoDB creates a unique index on the _id field when creating a collection. Therefore, we sometimes refer to it as the primary index. We take advantage of the above in the last part of this series, where we implement pagination and sort documents by the _id field.

When we sort documents using a field without an index, MongoDB performs sorting at query time. It takes time and resources to do that and makes our app response slower. However, having the right index can help us avoid sorting results at query time because the results are already sorted in the index. Therefore, we can return them immediately.

We need to keep in mind that making a property unique creates an index and slows down our write queries.

Implementing indexes with Mongoose

With MongoDB, we can also define secondary indexes that don’t make properties unique.

post.schema.ts
1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2import { Document, ObjectId } from 'mongoose';
3import { Transform, Type } from 'class-transformer';
4 
5export type PostDocument = Post & Document;
6 
7@Schema()
8export class Post {
9  @Transform(({ value }) => value.toString())
10  _id: ObjectId;
11 
12  @Prop({ index: true })
13  title: string;
14 
15  // ...
16}
17 
18export const PostSchema = SchemaFactory.createForClass(Post);

By doing the above, we speed up queries, such as when we look for a post with a specific title, for example. We also speed up queries where we sort posts by the title alphabetically.

Text indexes

MongoDB also implements text indexes that support search queries on string content. To define a text index, we need to use the index() method.

post.schema.ts
1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2import { Document, ObjectId } from 'mongoose';
3import { Transform } from 'class-transformer';
4 
5export type PostDocument = Post & Document;
6 
7@Schema()
8export class Post {
9  @Transform(({ value }) => value.toString())
10  _id: ObjectId;
11 
12  @Prop()
13  title: string;
14 
15  // ...
16}
17 
18const PostSchema = SchemaFactory.createForClass(Post);
19 
20PostSchema.index({ title: 'text' });
21 
22export { PostSchema };

When we set up a text index, we can take advantage of the $text operator. It performs a text search on the content of the fields indexed with a text index.

A collection can’t have more than one text index.

Let’s implement a feature of searching through our posts by adding a new query parameter.

post.controller.ts
1import {
2  Controller,
3  Get,
4  Query,
5  UseInterceptors,
6} from '@nestjs/common';
7import PostsService from './posts.service';
8import MongooseClassSerializerInterceptor from '../utils/mongooseClassSerializer.interceptor';
9import { Post as PostModel } from './post.schema';
10import { PaginationParams } from '../utils/paginationParams';
11 
12@Controller('posts')
13@UseInterceptors(MongooseClassSerializerInterceptor(PostModel))
14export default class PostsController {
15  constructor(private readonly postsService: PostsService) {}
16 
17  @Get()
18  async getAllPosts(
19    @Query() { skip, limit, startId }: PaginationParams,
20    @Query('searchQuery') searchQuery: string,
21  ) {
22    return this.postsService.findAll(skip, limit, startId, searchQuery);
23  }
24 
25  // ...
26}

We also need to add the $text query to the service.

post.service.ts
1import { FilterQuery, Model } from 'mongoose';
2import { Injectable } from '@nestjs/common';
3import { InjectModel } from '@nestjs/mongoose';
4import { Post, PostDocument } from './post.schema';
5 
6@Injectable()
7class PostsService {
8  constructor(@InjectModel(Post.name) private postModel: Model<PostDocument>) {}
9 
10  async findAll(
11    documentsToSkip = 0,
12    limitOfDocuments?: number,
13    startId?: string,
14    searchQuery?: string,
15  ) {
16    const filters: FilterQuery<PostDocument> = startId
17      ? {
18          _id: {
19            $gt: startId,
20          },
21        }
22      : {};
23 
24    if (searchQuery) {
25      filters.$text = {
26        $search: searchQuery,
27      };
28    }
29 
30    const findQuery = this.postModel
31      .find(filters)
32      .sort({ _id: 1 })
33      .skip(documentsToSkip)
34      .populate('author')
35      .populate('categories');
36  
37    if (limitOfDocuments) {
38      findQuery.limit(limitOfDocuments);
39    }
40 
41    const results = await findQuery;
42    const count = await this.postModel.count();
43 
44    return { results, count };
45  }
46 
47  // ...
48}
49 
50export default PostsService;

Thanks to the above, MongoDB can search through the titles of our posts.

The $text query has more arguments, such as the $caseSensitive boolean. For more, check out the official documentation.

Compound indexes

The $text query searches through all of the fields indexed with the text index. With MongoDB, we can create compound indexes where the index structure holds references to multiple fields.

1PostSchema.index({ title: 'text', content: 'text' });

Thanks to doing the above, the $text query will search both through the titles and contents of posts.

Besides the text indexes, we can also create regular compound indexes.

user.schema.ts
1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2import { Document, ObjectId } from 'mongoose';
3import { Transform } from 'class-transformer';
4 
5export type UserDocument = User & Document;
6 
7@Schema({
8  toJSON: {
9    getters: true,
10    virtuals: true,
11  },
12})
13export class User {
14  @Transform(({ value }) => value.toString())
15  _id: ObjectId;
16 
17  @Prop()
18  firstName: string;
19 
20  @Prop()
21  lastName: string;
22 
23  // ...
24}
25 
26const UserSchema = SchemaFactory.createForClass(User);
27 
28UserSchema.index({ firstName: 1, lastName: 1 });
29 
30export { UserSchema };

Doing the above creates a compound index on the firstName and lastName fields. It can speed queries such as the ones where we look for a user with a specific first name and last name.

By using 1, we create an ascending index. When we use -1, we create a descending index. The direction doesn’t matter for single key indexes because MongoDB can traverse the index in either direction. It can be significant for compound indexes, though. The official documentation and this Stackoverflow page provide a good explanation.

The @Prop({ index: true }) decorator creates an ascending index.

Summary

In this article, we’ve touched on the subject of indexes in MongoDB. We’ve explained different types of indexes, such as unique indexes, single-field indexes, and compound indexes. We’ve also learned about text indexes and implemented a search functionality using them. We also got to know that creating advantages can speed up some queries while slowing down others.