One place for hosting & domains

      MongoDB

      Building a modern app using Nest.js, MongoDB, and Vue.js

      Introduction

      Nest.js introduces a modern way of building Node.js apps by giving them a proper and modular structure out of the box. It was fully built with TypeScript but still preserves compatibility with plain JavaScript.

      In this post, I will introduce and explain the fundamental steps to follow in order to combine this awesome framework with a modern frontend JavaScript framework such as Vue.js. You will build a web application to manage customers’ information. The application will be used to create a new customer, add several details about the customer, and update each customer’s records in the database.

      The approach to this post will be to build a separate REST API backend with Nest.js and a frontend to consume this API using Vue.js. So basically, instead of building Nest.js application that uses a Node.js template engine for the client-side, you will leverage the awesomeness of Vue.js as a progressive JavaScript library to quickly render contents and handled every client-side related logic.

      In addition, you will use MongoDB database to persist and retrieve data for the application. MongoDB is a schema-less NoSQL document database that can receive and store data in JSON document format. It is often used with Mongoose; an Object Data Modeling (ODM) library, that helps to manage relationships between data and provides schema validations. You learn more about this later in this tutorial.

      • A reasonable knowledge of building applications with JavaScript is required and basic knowledge of TypeScript will be an added advantage.
      • Ensure that you have Node and npm installed on your local system. Check this link for Node and here for instructions on how to install npm.
      • Read this article here on scotch.io to grasp the fundamental knowledge of the building blocks of Nest.js.
      • Install MongoDB on your local system. Follow the instructions here to download and installed it for your choice of the operating system. This tutorial uses MacOS machine for development. To successfully install MongoDB, you can either install it by using homebrew on Mac or by downloading it from the MongoDB website.
      • A text editor installed, such as Visual Studio Code, Atom, or Sublime Text

      Nest.js has a reputation for bringing design patterns and mature structures to the Node.js world. This makes it quite easy to use as the right tool for building awesome web applications. For the fact that Nest.js uses Express library under the hood.

      Nest.js is fully featured and structured to support MVC (Model-View-Controller) design pattern.

      This means you can install one of the popular template engines used in Node.js and configure it to handle the flow of the application and interaction with backend API from the front end.

      While this might be sufficient for a small app, it is always better to consider a much better and contemporary approach to handling the frontend-related part of an application by leveraging on a tool like Vue.js.

      Vue can be used to set up the frontend logic of your application as you will see later in this post.

      Vue.js is a progressive JavaScript framework for building reusable components for user interfaces. It is simple and yet very powerful and ideal for any project. This makes it seamless to use for a Nest.js application for example.

      As you proceed in this tutorial, you will learn how to use and successfully combine these two tools, that is, Nest.js and Vue.js to build a highly interactive web app.

      As mentioned earlier in this post, you will build a customer list management application. To keep things really simple here, we will not be implementing authentication and authorization for any user. The main objective is for you to get conversant and comfortable using both Nest.js and Vue.js. At the end of the day, you would have learned the means to craft and structure this application as shown below:

      We will use Nest.js to develop the backend API and then a Vue.js application to build components for creating, editing, deleting, and showing the total list of customers from a MongoDB database.

      Now that the basic introductory contents have been properly covered, you will proceed to install Nest.js and its required dependencies. Getting Nest.js installed can be done in two different ways:

      • Scaffold the base project with Nest CLI tool
      • Installing the starter project on GitHub by using Git

      You will use the Nest CLI here in this tutorial to easily craft a new Nest.js project. This comes with a lot of benefits like scaffolding a new project seamlessly, generating different files by using the nest command amongst other things.

      First, you need to install the CLI globally on your system. To do that, run the following command from the terminal:

      1. npm install -g @nestjs/cli

      The installation of this tool will give you access to the nest command to manage the project and create Nest.js application-related files as you will see later in this post.

      Now that you have the CLI installed, you can now proceed to create the project for this tutorial by running the following command from your local development folder:

      1. nest new customer-list-app-backend

      The preceding command will generate a customer-list-app-backend application. Next, change directory into this new project and install the only server dependency for the backend. As mentioned earlier, you will use MongoDB database to persist data for the application. To integrate it with a Nest.js project, you need to install mongoose and the mongoose package built by the Nest.js team. Use the following command for this purpose:

      1. cd customer-list-app-backend
      1. npm install --save @nestjs/mongoose mongoose

      With the installation process properly covered, you can now start the application with:

      1. npm run start

      This will start the application on the default port of 3000. Navigate to http://localhost:3000 from your favorite browser and you should have a page similar to this:

      It is assumed that by now, you have installed MongoDB on your machine as instructed at the beginning of this post. To start the database, open a new terminal so that the application keeps running and run sudo mongod. The preceding command will start the MongoDB service and simultaneously run the database in the background.

      Next, to quickly set up a connection to the database from your application, you will have to import the installed MongooseModule within the root ApplicationModule. To do this, use your preferred code editor to open the project and locate ./src/app.module.ts. Update the contents with the following:

      ./src/app.module.ts

      import { Module } from '@nestjs/common';
      import { AppController } from './app.controller';
      import { AppService } from './app.service';
      import { MongooseModule } from '@nestjs/mongoose';
      @Module({
        imports: [
          MongooseModule.forRoot('mongodb://localhost/customer-app', { useNewUrlParser: true })
        ],
        controllers: [AppController],
        providers: [AppService],
      })
      export class AppModule {}
      

      Here, the Mongoose module for MongoDB uses the forRoot() method to supply the connection to the database.

      Setting up and configuring a database schema, interfaces, and DTO

      To properly structure the kind of data that will be stored in the database for this application, you will create a database schema, a TypeScript, and a data transfer object (DTO).

      Database schema

      Here, you will create a mongoose database schema that will determine the data that should be stored in the database. To begin, navigate to the ./src/ folder and first, create a new folder named customer and within the newly created folder, create another one and call it schemas. Now create a new file within the schemas and name customer.schema.ts. Open this newly created file and paste the following code into it:

      ./src/customer/schemas/customer.schema.ts

      import * as mongoose from 'mongoose';
      
      export const CustomerSchema = new mongoose.Schema({
          first_name: String,
          last_name: String,
          email: String,
          phone: String,
          address: String,
          description: String,
          created_at: { type: Date, default: Date.now }
      })
      

      This will ensure that data with string values will be stored in the database.

      Interfaces

      Next, you will create a TypeScript interface that will be used for type-checking and to determine the type of values that will be received by the application. To set it up, create a new folder named interfaces within the ./src/customer folder. After that, create a new file within the newly created folder and name it customer.interface.ts. Paste the following code in it:

      ./src/customer/interfaces/customer.interface.ts

      import { Document } from 'mongoose';
      
      export interface Customer extends Document {
          readonly first_name: string;
          readonly last_name: string;
          readonly email: string;
          readonly phone: string;
          readonly address: string;
          readonly description: string;
          readonly created_at: Date;
      }
      

      Data transfer object (DTO)

      A data transfer object will define how data will be sent over the network. To do this, create a folder dto inside ./src/customer folder and create a new file create-customer.dto.ts and paste the code below in it:

      ./src/customer/dto/create-customer.dto.ts

      export class CreateCustomerDTO {
          readonly first_name: string;
          readonly last_name: string;
          readonly email: string;
          readonly phone: string;
          readonly address: string;
          readonly description: string;
          readonly created_at: Date;
      }
      

      You are now done with the basic configurations of connecting and interacting with the database

      Generate modules

      A module in Nest.js is identified by the @Module() decorator and it takes in objects such as controllers and providers. Here you will leverage the nest command to easily generate a new module for the customer app. This will ensure that the application is properly structured and more organized. Stop the application, if it is currently running with CTRL+C and run the following command

      1. nest generate module customer

      This will create a new file named customer.module.ts within the src/customer folder and update the root module (i.e., app.module.ts) of the application with the details of the newly created module.

      ./src/customer/customer.module.ts

      import { Module } from '@nestjs/common';
      @Module({})
      export class CustomerModule {}
      

      You will come back to add more content to this module later in this post.

      Generate service

      Service, also known as provider in Nest.js, basically carries out the task of abstracting logic away from controllers. With it in place, a controller will only carry out the functionality of handling HTTP requests from the frontend and delegate the complex tasks to services. Service or provider in Nest.js is identified by adding @Injectable() decorator on top of them.

      Generate a new service using the nest command by running the following command from the terminal within the project directory:

      1. nest generate service customer

      After successfully running the command above, two new files will be created. They are:

      • customer.service.ts: this is the main service file with @Injectable() decorator
      • customer.service.spec.ts: a file for unit testing. You can ignore this file for now as testing will not be covered in this tutorial.

      The customer.service.ts file holds all the logic as regards database interaction for creating and updating every detail of a new customer. In a nutshell, the service will receive a request from the controller, communicate this to the database and return an appropriate response afterward.

      Open the newly created customer.service.ts file and replace the existing code with the following :

      ./src/customer/customer.service.ts

      import { Injectable } from '@nestjs/common';
      import { Model } from 'mongoose';
      import { InjectModel } from '@nestjs/mongoose';
      import { Customer } from './interfaces/customer.interface';
      import { CreateCustomerDTO } from './dto/create-customer.dto';
      
      @Injectable()
      export class CustomerService {
          constructor(@InjectModel('Customer') private readonly customerModel: Model<Customer>) { }
          
          async getAllCustomer(): Promise<Customer[]> {
              const customers = await this.customerModel.find().exec();
              return customers;
          }
          
          async getCustomer(customerID): Promise<Customer> {
              const customer = await this.customerModel.findById(customerID).exec();
              return customer;
          }
          
          async addCustomer(createCustomerDTO: CreateCustomerDTO): Promise<Customer> {
              const newCustomer = await this.customerModel(createCustomerDTO);
              return newCustomer.save();
          }
          
          async updateCustomer(customerID, createCustomerDTO: CreateCustomerDTO): Promise<Customer> {
              const updatedCustomer = await this.customerModel
                  .findByIdAndUpdate(customerID, createCustomerDTO, { new: true });
              return updatedCustomer;
          }
          
          async deleteCustomer(customerID): Promise<any> {
              const deletedCustomer = await this.customerModel.findByIdAndRemove(customerID);
              return deletedCustomer;
          }
      }
      

      Here, you imported the required module from @nestjs/common, mongoose, and @nestjs/mongoose. In addition, you also imported the interface created earlier named Customer and a data transfer object CreateCustomerDTO.

      In order to be able to seamlessly carry out several database-related activities such as creating a customer, retrieving the list of customers, or just a single customer, you used the @InjectModel method to inject the Customer model into the CustomerService class.

      Next, you created the following methods:

      • getAllCustomer(): to retrieve and return the list of customers from the database
      • getCustomer(): it takes customerID as a parameter and based on that, it will search and return the details of a user identified by that ID.
      • addCustomer(): used to add a new customer to the database
      • updateCustomer(): this method also takes the ID of a customer as an argument and will be used to edit and update the details of such customer in the database.
      • deleteCustomer(): this will be used to delete the details of a particular customer completely from the database.

      Generate controller

      Handling each route within the application is one of the major responsibilities of controllers in Nest.js. Similar to most JavaScript server-side frameworks for the web, several endpoints will be created and any requests sent to such endpoint from the client-side will be mapped to a specific method within the controller and an appropriate response will be returned.

      Again, you will use the nest command to generate the controller for this application. To achieve that, run the following command:

      1. nest generate controller customer

      This command will also generate two new files within the src/customer, they are, customer.controller.spec.ts and customer.controller.ts files respectively. The customer.controller.ts file is the actual controller file and the second one should be ignored for now. Controllers in Nest.js are TypeScript files decorated with @Controller metadata.

      Now open the controller and replace the content with the following code that contains methods to create a new customer, retrieve the details of a particular customer and fetch the list of all customers from the database:

      ./src/customer/customer.controller.ts

      import { Controller, Get, Res, HttpStatus, Post, Body, Put, Query, NotFoundException, Delete, Param } from '@nestjs/common';
      import { CustomerService } from './customer.service';
      import { CreateCustomerDTO } from './dto/create-customer.dto';
      
      @Controller('customer')
      export class CustomerController {
          constructor(private customerService: CustomerService) { }
      
          
          @Post('/create')
          async addCustomer(@Res() res, @Body() createCustomerDTO: CreateCustomerDTO) {
              const customer = await this.customerService.addCustomer(createCustomerDTO);
              return res.status(HttpStatus.OK).json({
                  message: "Customer has been created successfully",
                  customer
              })
          }
      
          
          @Get('customers')
          async getAllCustomer(@Res() res) {
              const customers = await this.customerService.getAllCustomer();
              return res.status(HttpStatus.OK).json(customers);
          }
      
          
          @Get('customer/:customerID')
          async getCustomer(@Res() res, @Param('customerID') customerID) {
              const customer = await this.customerService.getCustomer(customerID);
              if (!customer) throw new NotFoundException('Customer does not exist!');
              return res.status(HttpStatus.OK).json(customer);
          }
      }
      

      In order to interact with the database, the CustomerService was injected into the controller via the class constructor(). The addCustomer() and getAllCustomer() methods will be used to add a new customer’s details and retrieve the list of customers while the getCustomer() receives the customerID as a query parameter and throw an exception if the customer does not exist in the database.

      Next, you need to be able to update and delete the details of a customer where and when necessary. For this, you will add two more methods to the CustomerController class. Open the file again and add this:

      ./src/customer/customer.controller.ts

      ...
      @Controller('customer')
      export class CustomerController {
          constructor(private customerService: CustomerService) { }
          ...
      
          
          @Put('/update')
          async updateCustomer(@Res() res, @Query('customerID') customerID, @Body() createCustomerDTO: CreateCustomerDTO) {
              const customer = await this.customerService.updateCustomer(customerID, createCustomerDTO);
              if (!customer) throw new NotFoundException('Customer does not exist!');
              return res.status(HttpStatus.OK).json({
                  message: 'Customer has been successfully updated',
                  customer
              });
          }
      
          
          @Delete('/delete')
          async deleteCustomer(@Res() res, @Query('customerID') customerID) {
              const customer = await this.customerService.deleteCustomer(customerID);
              if (!customer) throw new NotFoundException('Customer does not exist');
              return res.status(HttpStatus.OK).json({
                  message: 'Customer has been deleted',
                  customer
              })
          }
      }
      

      To keep things properly organized go back to the customer.module.ts and set up the Customer model. Update the content with the following:

      ./src/customer/customer.module.ts

      import { Module } from '@nestjs/common';
      import { CustomerController } from './customer.controller';
      import { CustomerService } from './customer.service';
      import { MongooseModule } from '@nestjs/mongoose';
      import { CustomerSchema } from './schemas/customer.schema';
      @Module({
        imports: [
          MongooseModule.forFeature([{ name: 'Customer', schema: CustomerSchema }])
        ],
        controllers: [CustomerController],
        providers: [CustomerService]
      })
      export class CustomerModule { }
      

      By default, it is forbidden for two separate applications on different ports to interact or share resources with each other unless it is otherwise allowed by one of them, which is often the server-side. In order to allow requests from the client-side that will be built with Vue.js, you will need to enable CORS (Cross-Origin Resource Sharing).

      To do that in Nest.js, you only need to add app.enableCors() to the main.ts file as shown below:

      ./src/main.ts

      import { NestFactory } from '@nestjs/core';
      import { AppModule } from './app.module';
      async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        app.enableCors(); 
        await app.listen(3000);
      }
      bootstrap();
      

      With this, you have just completed the backend part of the application and can now proceed to build the frontend with Vue.js.

      The team at Vue.js already created an awesome tool named Vue CLI. It is a standard tool that allows you to quickly generate and install a new Vue.js project with ease. You will use that here to create the frontend part of the customer app, but first, you need to install Vue CLI globally on your machine.

      Open a new terminal and run:

      1. npm install -g @vue/cli

      Once the installation process is complete, you can now use the vue create command to craft a new Vue.js project. Run the following command to do that for this project:

      1. vue create customer-list-app-frontend

      Immediately after you hit RETURN, you will be prompted to pick a preset. You can choose manually select features:

      Next, check the features you will need for this project by using the up and down arrow key on your keyboard to navigate through the list of features. Press the spacebar to select a feature from the list. Select Babel, Router, and Linter / Formatter as shown here:

      Hitting RETURN here will show you another list of options.

      For other instructions, type y to use history mode for a router, this will ensure that history mode is enabled within the router file that will automatically be generated for this project.

      Next, select ESLint with error prevention only in order to pick a linter / formatter config. After that, select Lint on save for additional lint features and save your configuration in a dedicated config file for future projects. Type a name for your preset, I named mine vuescotch:

      This will create a Vue.js application in a directory named customer-list-app-frontend and install all its required dependencies.

      You can now change the directory into the newly created project and start the application with:

      1. cd customer-list-app-frontend

      Run the application:

      1. npm run serve

      You can now view the application on http://localhost:8080:

      Axios, a promised-based HTTP client for the browser will be used here to perform HTTP requests from different components within the application. Stop the frontend application from running by hitting CTRL+C from the terminal and run the following command afterward:

      1. npm install axios --save

      Once the installation process is completed, open the customer-list-app-frontend within a code editor and create a new file named helper.js within the src folder. Open the newly created file and paste the following content in it:

      ./src/helper.js

      export const server = {
          baseURL: 'http://localhost:3000'
      }
      

      What you have done here is to define the baseURL for the backend project built with Nest.js. This is just to ensure that you don’t have to start declaring this URL within several Vue.js components that you will create in the next section.

      Vue.js favors building and structuring applications by creating reusable components to give it a proper structure. Vue.js components contain three different sections, which are

      • <template></template>
      • <script></script>
      • <style></style>.

      You will start by creating a component within the application for a user to create a customer. This component will contain a form with few input fields required to accepts details of a customer and once the form is submitted, the details from the input fields will be posted to the server. To achieve this, create a new folder named customer within the ./src/components folder. This newly created folder will house all the components for this application. Next, create another file within the customer folder and name it Create.vue. Open this new file and add the following:

      ./src/components/customer/Create.vue

      <template>
        <div>
          <div class="col-md-12 form-wrapper">
            <h2> Create Customer</h2>
            <form id="create-post-form" @submit.prevent="createCustomer">
              <div class="form-group col-md-12">
                <label for="first_name">First Name</label>
                <input type="text" id="first_name" v-model="first_name" name="title" class="form-control" placeholder="Enter firstname">
              </div>
              <div class="form-group col-md-12">
                <label for="last_name">Last Name</label>
                <input type="text" id="last_name" v-model="last_name" name="title" class="form-control" placeholder="Enter Last name">
              </div>
              <div class="form-group col-md-12">
                <label for="email">Email</label>
                <input type="text" id="email" v-model="email" name="title" class="form-control" placeholder="Enter email">
              </div>
              <div class="form-group col-md-12">
                <label for="phone_number">Phone</label>
                <input type="text" id="phone_number" v-model="phone" name="title" class="form-control" placeholder="Enter Phone number">
              </div>
              <div class="form-group col-md-12">
                <label for="address">Address</label>
                <input type="text" id="address" v-model="address" name="title" class="form-control" placeholder="Enter Address">
              </div>
              <div class="form-group col-md-12">
                  <label for="description">Description</label>
                  <input type="text" id="description" v-model="description" name="description" class="form-control" placeholder="Enter Description">
              </div>
              <div class="form-group col-md-4 pull-right">
                  <button class="btn btn-success" type="submit">Create Customer</button>
              </div>
            </form>
          </div>
        </div>
      </template>
      

      This is the <template></template> section that contains the details of the input fields. Next, paste the following code just after the end of the </template> tag:

      ./src/components/customer/Create.vue

      ...
      <script>
      import axios from "axios";
      import { server } from "../../helper";
      import router from "../../router";
      export default {
        data() {
          return {
            first_name: "",
            last_name: "",
            email: "",
            phone: "",
            address: "",
            description: ""
          };
        },
        methods: {
          createCustomer() {
            let customerData = {
              first_name: this.first_name,
              last_name: this.last_name,
              email: this.email,
              phone: this.phone,
              address: this.address,
              description: this.description
            };
            this.__submitToServer(customerData);
          },
          __submitToServer(data) {
            axios.post(`${server.baseURL}/customer/create`, data).then(data => {
              router.push({ name: "home" });
            });
          }
        }
      };
      </script>
      

      Here, you created a method createCustomer() to receive the details of a customer via the input fields and used Axios to post the data to the server.

      Similar to the CreateCustomer component, you need to create another component to edit the customer’s details. Navigate to ./src/components/customer and create a new file named Edit.vue. Paste the following code in it:

      ./src/components/customer/Edit.vue

      <template>
        <div>
          <h4 class="text-center mt-20">
            <small>
              <button class="btn btn-success" v-on:click="navigate()">View All Customers</button>
            </small>
          </h4>
          <div class="col-md-12 form-wrapper">
            <h2>Edit Customer</h2>
            <form id="create-post-form" @submit.prevent="editCustomer">
              <div class="form-group col-md-12">
                <label for="first_name">First Name</label>
                <input type="text" id="first_name" v-model="customer.first_name" name="title" class="form-control" placeholder="Enter firstname">
              </div>
              <div class="form-group col-md-12">
                <label for="last_name">Last Name</label>
                <input type="text" id="last_name" v-model="customer.last_name" name="title" class="form-control" placeholder="Enter Last name">
              </div>
              <div class="form-group col-md-12">
                <label for="email">Email</label>
                <input type="text" id="email" v-model="customer.email" name="title" class="form-control" placeholder="Enter email">
              </div>
              <div class="form-group col-md-12">
                <label for="phone_number">Phone</label>
                <input type="text" id="phone_number" v-model="customer.phone" name="title" class="form-control" placeholder="Enter Phone number">
              </div>
              <div class="form-group col-md-12">
                <label for="address">Address</label>
                <input type="text" id="address" v-model="customer.address" name="title" class="form-control" placeholder="Enter Address">
              </div>
              <div class="form-group col-md-12">
                <label for="description">Description</label>
                <input type="text" id="description" v-model="customer.description" name="description" class="form-control" placeholder="Enter Description">
              </div>
              <div class="form-group col-md-4 pull-right">
                <button class="btn btn-success" type="submit">Edit Customer</button>
              </div>
            </form>
          </div>
        </div>
      </template>
      <script>
      import { server } from "../../helper";
      import axios from "axios";
      import router from "../../router";
      export default {
        data() {
          return {
            id: 0,
            customer: {}
          };
        },
        created() {
          this.id = this.$route.params.id;
          this.getCustomer();
        },
        methods: {
          editCustomer() {
            let customerData = {
              first_name: this.customer.first_name,
              last_name: this.customer.last_name,
              email: this.customer.email,
              phone: this.customer.phone,
              address: this.customer.address,
              description: this.customer.description
            };
            axios
              .put(
                `${server.baseURL}/customer/update?customerID=${this.id}`,
                customerData
              )
              .then(data => {
                router.push({ name: "home" });
              });
          },
          getCustomer() {
            axios
              .get(`${server.baseURL}/customer/customer/${this.id}`)
              .then(data => (this.customer = data.data));
          },
          navigate() {
            router.go(-1);
          }
        }
      };
      </script>
      

      The route parameter was used here to fetch the details of a customer from the database and populated the inputs fields with it. As a user of the application, you can now edit the details and submit them back to the server.

      The editCustomer() method within the <script></script> was used to send a PUT HTTP request to the server.

      Finally, to fetch and show the complete list of customers from the server, you will create a new component. Navigate to the views folder within the src folder, you should see a Home.vue file, if otherwise, create it and paste this code in it:

      ./src/views/Home.vue

      <template>
        <div class="container-fluid">
          <div class="text-center">
            <h1>Nest Customer List App Tutorial</h1>
            <p>Built with Nest.js, Vue.js, and MongoDB</p>
            <div v-if="customers.length === 0">
              <h2>No customer found at the moment</h2>
            </div>
          </div>
      
          <div class="">
            <table class="table table-bordered">
              <thead class="thead-dark">
                <tr>
                  <th scope="col">Firstname</th>
                  <th scope="col">Lastname</th>
                  <th scope="col">Email</th>
                  <th scope="col">Phone</th>
                  <th scope="col">Address</th>
                  <th scope="col">Description</th>
                  <th scope="col">Actions</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="customer in customers" :key="customer._id">
                  <td>{{ customer.first_name }}</td>
                  <td>{{ customer.last_name }}</td>
                  <td>{{ customer.email }}</td>
                  <td>{{ customer.phone }}</td>
                  <td>{{ customer.address }}</td>
                  <td>{{ customer.description }}</td>
                  <td>
                    <div class="d-flex justify-content-between align-items-center">
                      <div class="btn-group" style="margin-bottom: 20px;">
                        <router-link :to="{name: 'Edit', params: {id: customer._id}}" class="btn btn-sm btn-outline-secondary">Edit Customer</router-link>
                        <button class="btn btn-sm btn-outline-secondary" v-on:click="deleteCustomer(customer._id)">Delete Customer</button>
                      </div>
                    </div>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </template>
      <script>
      import { server } from "../helper";
      import axios from "axios";
      export default {
        data() {
          return {
            customers: []
          };
        },
        created() {
          this.fetchCustomers();
        },
        methods: {
          fetchCustomers() {
            axios
              .get(`${server.baseURL}/customer/customers`)
              .then(data => (this.customers = data.data));
          },
          deleteCustomer(id) {
            axios
              .delete(`${server.baseURL}/customer/delete?customerID=${id}`)
              .then(data => {
                console.log(data);
                window.location.reload();
              });
          }
        }
      };
      </script>
      

      Within <template> section, you created an HTML table to display all customers’ details and used the <router-link> to create a link for editing and to view a single customer by passing the customer._id as a query parameter. And finally, within the <script> section of this file, you created a method named fetchCustomers() to fetch all customers from the database and updated the page with the data returned from the server.

      Open the AppComponent of the application and update it with the links to both Home and Create components by using the content below:

      ./src/App.vue

      <template>
        <div id="app">
          <div id="nav">
            <router-link to="/">Home</router-link> |
            <router-link to="/create">Create</router-link>
          </div>
          <router-view/>
        </div>
      </template>
      
      <style>
      ...
      .form-wrapper {
        width: 500px;
        margin: 0 auto;
      }
      </style>
      

      Also included is a <style></style> section to include styling for the forms.

      Navigate to the index.html file within the public folder and include the CDN file for Bootstrap as shown below. This is just to give the page some default style:

      <!DOCTYPE html>
      <html lang="en">
      <head>
        ...
        
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
        <title>customer-list-app-frontend</title>
      </head>
      <body>
         ...
      </body>
      </html>
      

      Finally, configure the router file within ./src/router.js to include the link to all the required reusable components created so far by updating its content as shown here:

      ./src/router.js

      import Vue from 'vue'
      import Router from 'vue-router'
      import HomeComponent from '@/views/Home';
      import EditComponent from '@/components/customer/Edit';
      import CreateComponent from '@/components/customer/Create';
      Vue.use(Router)
      export default new Router({
        mode: 'history',
        routes: [
          { path: '/', redirect: { name: 'home' } },
          { path: '/home', name: 'home', component: HomeComponent },
          { path: '/create', name: 'Create', component: CreateComponent },
          { path: '/edit/:id', name: 'Edit', component: EditComponent },
        ]
      });
      

      You can now proceed to test the application by running npm run serve to start it and navigate to http://localhost:8080 to view it:

      Ensure that the backend server is running at this moment, if otherwise, navigate to the backend application from a different terminal and run:

      1. npm run start

      Lastly, also ensure that the MongoDB instance is running as well. Use sudo mongod from another terminal on your local system, if it is not running at the moment.

      In this tutorial, you have created a simple customer list management application by using Nest.js and Vue.js. Here, you used Nest.js to build a RESTful backend API and then leveraged on Vue.js to craft a client that consumes the API.

      This has given you an overview of how to structure Nest.js application and integrate it with a MongoDB database.

      I hope you found this tutorial helpful. Don’t hesitate to explore the source code of both application by checking it out here on GitHub.

      How To Use Schema Validation in MongoDB


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      One important aspect of relational databases — which store databases in tables made up of rows and columns — is that they operate on fixed, rigid schemas with fields of known data types. Document-oriented databases like MongoDB are more flexible in this regard, as they allow you to reshape your documents’ structure as needed.

      However, there are likely to be situations in which you might need your data documents to follow a particular structure or fulfill certain requirements. Many document databases allow you to define rules that dictate how parts of your documents’ data should be structured while still offering some freedom to change this structure if needed.

      MongoDB has a feature called schema validation that allows you to apply constraints on your documents’ structure. Schema validation is built around JSON Schema, an open standard for JSON document structure description and validation. In this tutorial, you’ll write and apply validation rules to control the structure of documents in an example MongoDB collection.

      Prerequisites

      To follow this tutorial, you will need:

      Note: The linked tutorials on how to configure your server, install MongoDB, and secure the MongoDB installation refer to Ubuntu 20.04. This tutorial concentrates on MongoDB itself, not the underlying operating system. It will generally work with any MongoDB installation regardless of the operating system as long as authentication has been enabled.

      Step 1 — Inserting Documents Without Applying Schema Validation

      In order to highlight MongoDB’s schema validation features and why they can be useful, this step outlines how to open the MongoDB shell to connect to your locally-installed MongoDB instance and create a sample collection within it. Then, by inserting a number of example documents into this collection, this step will show how MongoDB doesn’t enforce any schema validation by default. In later steps, you’ll begin creating and enforcing such rules yourself.

      To create the sample collection used in this guide, connect to the MongoDB shell as your administrative user. This tutorial follows the conventions of the prerequisite MongoDB security tutorial and assumes the name of this administrative user is AdminSammy and its authentication database is admin. Be sure to change these details in the following command to reflect your own setup, if different:

      • mongo -u AdminSammy -p --authenticationDatabase admin

      Enter the password set during installation to gain access to the shell. After providing the password, you’ll see the > prompt sign.

      To illustrate the schema validation features, this guide’s examples use an sample database containing documents that represent the highest mountains in the world. The sample document for Mount Everest will take this form:

      The Everest document

      {
          "name": "Everest",
          "height": 8848,
          "location": ["Nepal", "China"],
          "ascents": {
              "first": {
                  "year": 1953,
              },
              "first_winter": {
                  "year": 1980,
              },
              "total": 5656,
          }
      }
      

      This document contains the following information:

      • name: the peak’s name.
      • height: the peak’s elevation, in meters.
      • location: the countries in which the mountain is located. This field stores values as an array to allow for mountains located in more than one country.
      • ascents: this field’s value is another document. When one document is stored within another document like this, it’s known as an embedded or nested document. Each ascents document describes successful ascents of the given mountain. Specifically, each ascents document contains a total field that lists the total number of successful ascents of each given peak. Additionally, each of these nested documents contain two fields whose values are also nested documents:
        • first: this field’s value is a nested document that contains one field, year, which describes the year of the first overall successful ascent.
        • first_winter: this field’s value is a nested document that also contains a year field, the value of which represents the year of the first successful winter ascent of the given mountain.

      Run the following insertOne() method to simultaneously create a collection named peaks in your MongoDB installation and insert the previous example document representing Mount Everest into it:

      • db.peaks.insertOne(
      • {
      • "name": "Everest",
      • "height": 8848,
      • "location": ["Nepal", "China"],
      • "ascents": {
      • "first": {
      • "year": 1953
      • },
      • "first_winter": {
      • "year": 1980
      • },
      • "total": 5656
      • }
      • }
      • )

      The output will contain a success message and an object identifier assigned to the newly inserted object:

      Output

      { "acknowledged" : true, "insertedId" : ObjectId("618ffa70bfa69c93a8980443") }

      Although you inserted this document by running the provided insertOne() method, you had complete freedom in designing this document’s structure. In some cases, you might want to have some degree of flexibility in how documents within the database are structured. However, you might also want to make sure some aspects of the documents’ structure remain consistent to allow for easier data analysis or processing.

      To illustrate why this can be important, consider a few other example documents that might be entered into this database.

      The following document is almost identical to the previous one representing Mount Everest, but it doesn’t contain a name field:

      The Mountain with no name at all

      {
          "height": 8611,
          "location": ["Pakistan", "China"],
          "ascents": {
              "first": {
                  "year": 1954
              },
              "first_winter": {
                  "year": 1921
              },
              "total": 306
          }
      }
      

      For a database containing a list of the highest mountains in the world, adding a document representing a mountain but not including its name would likely be a serious error.

      In this next example document, the mountain’s name is present but its height is represented as a string instead of a number. Additionally, the location is not an array but a single value, and there is no information on the total number of ascent attempts:

      Mountain with a string value for its height

      {
          "name": "Manaslu",
          "height": "8163m",
          "location": "Nepal"
      }
      

      Interpreting a document with as many omissions as this example could prove difficult. For instance, you would not be able to successfully sort the collection by peak heights if the height attribute values are stored as different data types between documents.

      Now run the following insertMany() method to test whether these documents can be inserted into the database without causing any errors:

      • db.peaks.insertMany([
      • {
      • "height": 8611,
      • "location": ["Pakistan", "China"],
      • "ascents": {
      • "first": {
      • "year": 1954
      • },
      • "first_winter": {
      • "year": 1921
      • },
      • "total": 306
      • }
      • },
      • {
      • "name": "Manaslu",
      • "height": "8163m",
      • "location": "Nepal"
      • }
      • ])

      As it turns out, MongoDB will not return any errors and both documents will be inserted successfully:

      Output

      { "acknowledged" : true, "insertedIds" : [ ObjectId("618ffd0bbfa69c93a8980444"), ObjectId("618ffd0bbfa69c93a8980445") ] }

      As this output indicates, both of these documents are valid JSON, which is enough to insert them into the collection. However, this isn’t enough to keep the database logically consistent and meaningful. In the next steps, you’ll build schema validation rules to make sure the data documents in the peaks collection follow a few essential requirements.

      Step 2 — Validating String Fields

      In MongoDB, schema validation works on individual collections by assigning a JSON Schema document to the collection. JSON Schema is an open standard that allows you to define and validate the structure of JSON documents. You do this by creating a schema definition that lists a set of requirements that documents in the given collection must follow to be considered valid.

      Any given collection can only use a single JSON Schema, but you can assign a schema when you create the collection or any time afterwards. If you decide to change your original validation rules later on, you will have to replace the original JSON Schema document with one that aligns with your new requirements.

      To assign a JSON Schema validator document to the peaks collection you created in the previous step, you could run the following command:

      • db.runCommand({
      • "collMod": "collection_name",
      • "validator": {
      • $jsonSchema: {JSON_Schema_document}
      • }
      • })

      The runCommand method executes the collMod command, which modifies the specified collection by applying the validator attribute to it. The validator attribute is responsible for schema validation and, in this example syntax, it accepts the $jsonSchema operator. This operator defines a JSON Schema document which will be used as the schema validator for the given collection.

      Warning: In order to execute the collMod command, your MongoDB user must be granted the appropriate privileges. Assuming you followed the prerequisite tutorial on How To Secure MongoDB on Ubuntu 20.04 and are connected to your MongoDB instance as the administrative user you created in that guide, you will need to grant it an additional role to follow along with the examples in this guide.

      First, switch to your user’s authentication database. This is admin in the following example, but connect to your own user’s authentication database if different:

      Output

      switched to db admin

      Then run a grantRolesToUser() method and grant your user the dbAdmin role over the database where you created the peaks collection. The following example assumes the peaks collection is in the test database:

      • db.grantRolesToUser(
      • "AdminSammy",
      • [ { role : "dbAdmin", db : "test" } ]
      • )

      Alternatively, you can grant your user the dbAdminAnyDatabase role. As this role’s name implies, it will grant your user dbAdmin privileges over every database on your MongoDB instance:

      • db.grantRolesToUser(
      • "AdminSammy",
      • [ "dbAdminAnyDatabase" ]
      • )

      After granting your user the appropriate role, navigate back to the database where your peaks collection is stored:

      Output

      switched to db test

      Be aware that you can also assign a JSON Schema validator when you create a collection. To do so, you could use the following syntax:

      • db.createCollection(
      • "collection_name", {
      • "validator": {
      • $jsonSchema: {JSON_Schema_document}
      • }
      • })

      Unlike the previous example, this syntax doesn’t include the collMod command, since the collection doesn’t yet exist and thus can’t be modified. As with the previous example, though, collection_name is the name of the collection to which you want to assign the validator document and the validator option assigns a specified JSON Schema document as the collection’s validator.

      Applying a JSON Schema validator from the start like this means every document you add to the collection must satisfy the requirements set by the validator. When you add validation rules to an existing collection, though, the new rules won’t affect existing documents until you try to modify them.

      The JSON schema document you pass to the validator attribute should outline every validation rule you want to apply to the collection. The following example JSON Schema will make sure that the name field is present in every document in the collection, and that the name field’s value is always a string:

      Your first JSON Schema document validating the name field

      {
          "bsonType": "object",
          "description": "Document describing a mountain peak",
          "required": ["name"],
          "properties": {
              "name": {
                  "bsonType": "string",
                  "description": "Name must be a string and is required"
              }
          },
      }
      

      This schema document outlines certain requirements that certain parts of documents entered into the collection must follow. The root part of the JSON Schema document (the fields before properties, which in this case are bsonType, description, and required) describes the database document itself.

      The bsonType property describes the data type that the validation engine will expect to find. For the database document itself, the expected type is object. This means that you can only add objects — in other words, complete, valid JSON documents surrounded by curly braces ({ and }) — to this collection. If you were to try to insert some other kind of data type (like a standalone string, integer, or an array), it would cause an error.

      In MongoDB, every document is an object. However, JSON Schema is a standard used to describe and validate all kinds of valid JSON documents, and a plain array or a string is valid JSON, too. When working with MongoDB schema validation, you’ll find that you must always set the root document’s bsonType value as object in the JSON Schema validator.

      Next, the description property provides a short description of the documents found in this collection. This field isn’t required, but in addition to being used to validate documents, JSON Schemas can also be used to annotate the document’s structure. This can help other users understand what the purpose of the documents are, so including a description field can be a good practice.

      The next property in the validation document is the required field. The required field can only accept an array containing a list of document fields that must be present in every document in the collection. In this example, ["name"] means that the documents only have to contain the name field to be considered valid.

      Following that is a properties object that describes the rules used to validate document fields. For each field that you want to define rules for, include an embedded JSON Schema document named after the field. Be aware that you can define schema rules for fields that aren’t listed in the required array. This can be useful in cases where your data has fields that aren’t required, but you’d still like for them to follow certain rules when they are present.

      These embedded schema documents will follow a similar syntax as the main document. In this example, the bsonType property will require every document’s name field to be a string. This embedded document also contains a brief description field.

      To apply this JSON Schema to the peaks collection you created in the previous step, run the following runCommand() method:

      • db.runCommand({
      • "collMod": "peaks",
      • "validator": {
      • $jsonSchema: {
      • "bsonType": "object",
      • "description": "Document describing a mountain peak",
      • "required": ["name"],
      • "properties": {
      • "name": {
      • "bsonType": "string",
      • "description": "Name must be a string and is required"
      • }
      • },
      • }
      • }
      • })

      MongoDB will respond with a success message indicating that the collection was successfully modified:

      Output

      { "ok" : 1 }

      Following that, MongoDB will no longer allow you to insert documents into the peaks collection if they don’t have a name field. To test this, try inserting the document you inserted in the previous step that fully describes a mountain, aside from missing the name field:

      • db.peaks.insertOne(
      • {
      • "height": 8611,
      • "location": ["Pakistan", "China"],
      • "ascents": {
      • "first": {
      • "year": 1954
      • },
      • "first_winter": {
      • "year": 1921
      • },
      • "total": 306
      • }
      • }
      • )

      This time, the operation will trigger an error message indicating a failed document validation:

      Output

      WriteError({ "index" : 0, "code" : 121, "errmsg" : "Document failed validation", . . . })

      MongoDB won’t insert any documents that fail to pass the validation rules specified in the JSON Schema.

      Note: Starting with MongoDB 5.0, when validation fails the error messages point towards the failed constraint. In MongoDB 4.4 and earlier, the database provides no further details on the failure reason.

      You can also test whether MongoDB will enforce the data type requirement you included in the JSON Schema by running the following insertOne() method. This is similar to the last operation, but this time it includes a name field. However, this field’s value is a number instead of a string:

      • db.peaks.insertOne(
      • {
      • "name": 123,
      • "height": 8611,
      • "location": ["Pakistan", "China"],
      • "ascents": {
      • "first": {
      • "year": 1954
      • },
      • "first_winter": {
      • "year": 1921
      • },
      • "total": 306
      • }
      • }
      • )

      Once again, the validation will fail. Even though the name field is present, it doesn’t meet the constraint that requires it to be a string:

      Output

      WriteError({ "index" : 0, "code" : 121, "errmsg" : "Document failed validation", . . . })

      Try once more, but with the name field present in the document and followed by a string value. This time, name is the only field in the document:

      • db.peaks.insertOne(
      • {
      • "name": "K2"
      • }
      • )

      The operation will succeed, and the document will receive the object identifier as usual:

      Output

      { "acknowledged" : true, "insertedId" : ObjectId("61900965bfa69c93a8980447") }

      The schema validation rules pertain only to the name field. At this point, as long as the name field fulfills the validation requirements, the document will be inserted without error. The rest of the document can take any shape.

      With that, you’ve created your first JSON Schema document and applied the first schema validation rule to the name field, requiring it to be present and a string. However, there are different validation options for different data types. Next, you’ll validate number values stored in each document’s height field.

      Step 3 — Validating Number Fields

      Recall from Step 1 when you inserted the following document into the peaks collection:

      Mountain with a string value for its height

      {
          "name": "Manaslu",
          "height": "8163m",
          "location": "Nepal"
      }
      

      Even though this document’s height value is a string instead of a number, the insertMany() method you used to insert this document was successful. This was possible because you haven’t yet added any validation rules for the height field.

      MongoDB will accept any value for this field — even values that don’t make any sense for this field, like negative values — as long as the inserted document is written in valid JSON syntax. To work around this, you can extend the schema validation document from the previous step to include additional rules regarding the height field.

      Start by ensuring that the height field is always present in newly-inserted documents and that it’s always expressed as a number. Modify the schema validation with the following command:

      • db.runCommand({
      • "collMod": "peaks",
      • "validator": {
      • $jsonSchema: {
      • "bsonType": "object",
      • "description": "Document describing a mountain peak",
      • "required": ["name", "height"],
      • "properties": {
      • "name": {
      • "bsonType": "string",
      • "description": "Name must be a string and is required"
      • },
      • "height": {
      • "bsonType": "number",
      • "description": "Height must be a number and is required"
      • }
      • },
      • }
      • }
      • })

      In this command’s schema document, the height field is included in the required array. Likewise, there’s a height document within the properties object that will require any new height values to be a number. Again, the description field is auxiliary, and any description you include should only be to help other users understand the intention behind the JSON Schema.

      MongoDB will respond with a short success message to let you know that the collection was successfully modified:

      Output

      { "ok" : 1 }

      Now you can test the new rule. Try inserting a document with the minimal document structure required to pass the validation document. The following method will insert a document containing the only two mandatory fields, name and height:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300
      • }
      • )

      The insertion will succeed:

      Output

      { acknowledged: true, insertedId: ObjectId("61e0c8c376b24e08f998e371") }

      Next, try inserting a document with a missing height field:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak"
      • }
      • )

      Then try another that includes the height field, but this field contains a string value:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": "8300m"
      • }
      • )

      Both times, the operations will trigger an error message and fail:

      Output

      WriteError({ "index" : 0, "code" : 121, "errmsg" : "Document failed validation", . . . })

      However, if you try inserting a mountain peak with a negative height, the mountain will save properly:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": -100
      • }
      • )

      To prevent this, you could add a few more properties to the schema validation document. Replace the current schema validation settings by running the following operation:

      • db.runCommand({
      • "collMod": "peaks",
      • "validator": {
      • $jsonSchema: {
      • "bsonType": "object",
      • "description": "Document describing a mountain peak",
      • "required": ["name", "height"],
      • "properties": {
      • "name": {
      • "bsonType": "string",
      • "description": "Name must be a string and is required"
      • },
      • "height": {
      • "bsonType": "number",
      • "description": "Height must be a number between 100 and 10000 and is required",
      • "minimum": 100,
      • "maximum": 10000
      • }
      • },
      • }
      • }
      • })

      The new minimum and maximum attributes set constraints on values included in height fields, ensuring they can’t be lower than 100 or higher than 10000. This range makes sense in this case, as this collection is used to store information about mountain peak heights, but you could choose any values you like for these attributes.

      Now, if you try inserting a peak with a negative height value again, the operation will fail:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": -100
      • }
      • )

      Output

      WriteError({ "index" : 0, "code" : 121, "errmsg" : "Document failed validation", . . .

      As this output shows, your document schema now validates string values held in each document’s name field as well as numeric values held in the height fields. Continue reading to learn how to validate array values stored in each document’s location field.

      Step 4 — Validating Array Fields

      Now that each peak’s name and height values are being verified by schema validation constraints, we can turn our attention to the location field to guarantee its data consistency.

      Specifying the location for mountains is more tricky than one might expect, since peaks span more than one country, and this is the case for many of the famous eight-thousanders. Because of this, it would make sense store each peak’s location data as an array containing one or more country names instead of being just a string value. As with the height values, making sure each location field’s data type is consistent across every document can help with summarizing data when using aggregation pipelines.

      First, consider some examples of location values that users might enter, and weigh which ones would be valid or invalid:

      • ["Nepal", "China"]: this is a two-element array, and would be a valid value for a mountain spanning two countries.
      • ["Nepal"]: this example is a single-element array, it would also be a valid value for a mountain located in a single country.
      • "Nepal": this example is a plain string. It would be invalid because although it lists a single country, the location field should always contain an array
      • []: an empty array, this example would not be a valid value. After all, every mountain must exist in at least one country.
      • ["Nepal", "Nepal"]: this two-element array would also be invalid, as it contains the same value appearing twice.
      • ["Nepal", 15]: lastly, this two-element array would be invalid, as one of its values is a number instead of a string and this is not a correct location name.

      To ensure that MongoDB will correctly interpret each of these examples as valid or invalid, run the following operation to create some new validation rules for the peaks collection:

      • db.runCommand({
      • "collMod": "peaks",
      • "validator": {
      • $jsonSchema: {
      • "bsonType": "object",
      • "description": "Document describing a mountain peak",
      • "required": ["name", "height", "location"],
      • "properties": {
      • "name": {
      • "bsonType": "string",
      • "description": "Name must be a string and is required"
      • },
      • "height": {
      • "bsonType": "number",
      • "description": "Height must be a number between 100 and 10000 and is required",
      • "minimum": 100,
      • "maximum": 10000
      • },
      • "location": {
      • "bsonType": "array",
      • "description": "Location must be an array of strings",
      • "minItems": 1,
      • "uniqueItems": true,
      • "items": {
      • "bsonType": "string"
      • }
      • }
      • },
      • }
      • }
      • })

      In this $jsonSchema object, the location field is included within the required array as well as the properties object. There, it’s defined with a bsonType of array to ensure that the location value is always an array rather than a single string or a number.

      The minItems property validates that the array must contain at least one element, and the uniqueItems property is set to true to ensure that elements within each location array will be unique. This will prevent values like ["Nepal", "Nepal"] from being accepted. Lastly, the items subdocument defines the validation schema for each individual array item. Here, the only expectation is that every item within a location array must be a string.

      Note: The available schema document properties are different for each bsonType and, depending on the field type, you will be able to validate different aspects of the field value. For example, with number values you could define minimum and maximum allowable values to create a range of acceptable values. In the previous example, by setting the location field’s bsonType to array, you can validate features particular to arrays.

      You can find details on all possible validation choices in the JSON Schema documentation.

      After executing the command, MongoDB will respond with a short success message that the collection was successfully modified with the new schema document:

      Output

      { "ok" : 1 }

      Now try inserting documents matching the examples prepared earlier to test how the new rule behaves. Once again, let’s use the minimal document structure, with only the name, height, and location fields present.

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300,
      • "location": ["Nepal", "China"]
      • }
      • )

      The document will be inserted successfully as it fulfills all the defined validation expectations. Similarly, the following document will insert without error:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300,
      • "location": ["Nepal"]
      • }
      • )

      However, if you were to run any of the following insertOne() methods, they would trigger a validation error and fail:

      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300,
      • "location": "Nepal"
      • }
      • )
      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300,
      • "location": []
      • }
      • )
      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300,
      • "location": ["Nepal", "Nepal"]
      • }
      • )
      • db.peaks.insertOne(
      • {
      • "name": "Test peak",
      • "height": 8300,
      • "location": ["Nepal", 15]
      • }
      • )

      As per the validation rules you defined previously, the location values provided in these operations are considered invalid.

      After following this step, three primary fields describing a mountain top are already being validated through MongoDB’s schema validation feature. In the next step, you’ll learn how to validate nested documents using the ascents field as an example.

      Step 5 — Validating Embedded Documents

      At this point, your peaks collection has three fields — name, height and location — that are being kept in check by schema validation. This step focuses on defining validation rules for the ascents field, which describes successful attempts at summiting each peak.

      In the example document from Step 1 that represents Mount Everest, the ascents field was structured as follows:

      The Everest document

      {
          "name": "Everest",
          "height": 8848,
          "location": ["Nepal", "China"],
          "ascents": {
              "first": {
                  "year": 1953,
              },
              "first_winter": {
                  "year": 1980,
              },
              "total": 5656,
          }
      }
      

      The ascents subdocument contains a total field whose value represents the total number of ascent attempts for the given mountain. It also contains information on the first winter ascent of the mountain as well as the first ascent overall. These, however, might not be essential to the mountain description. After all, some mountains might not have been ascended in winter yet, or the ascent dates are disputed or not known. For now, just assume the information that you will always want to have in each document is the total number of ascent attempts.

      You can change the schema validation document so that the ascents field must always be present and its value must always be a subdocument. This subdocument, in turn, must always contain a total attribute holding a number greater than or equal to zero. The first and first_winter fields aren’t required for the purposes of this guide, so the validation form won’t consider them and they can take flexible forms.

      Once again, replace the schema validation document for the peaks collection by running the following runCommand() method:

      • db.runCommand({
      • "collMod": "peaks",
      • "validator": {
      • $jsonSchema: {
      • "bsonType": "object",
      • "description": "Document describing a mountain peak",
      • "required": ["name", "height", "location", "ascents"],
      • "properties": {
      • "name": {
      • "bsonType": "string",
      • "description": "Name must be a string and is required"
      • },
      • "height": {
      • "bsonType": "number",
      • "description": "Height must be a number between 100 and 10000 and is required",
      • "minimum": 100,
      • "maximum": 10000
      • },
      • "location": {
      • "bsonType": "array",
      • "description": "Location must be an array of strings",
      • "minItems": 1,
      • "uniqueItems": true,
      • "items": {
      • "bsonType": "string"
      • }
      • },
      • "ascents": {
      • "bsonType": "object",
      • "description": "Ascent attempts information",
      • "required": ["total"],
      • "properties": {
      • "total": {
      • "bsonType": "number",
      • "description": "Total number of ascents must be 0 or higher",
      • "minimum": 0
      • }
      • }
      • }
      • },
      • }
      • }
      • })

      Anytime the document contains subdocuments under any of its fields, the JSON Schema for that field follows the exact same syntax as the main document schema. Just like how the same documents can be nested within one another, the validation schema nests them within one another as well. This makes it straightforward to define complex validation schemas for document structures containing multiple subdocuments in a hierarchical structure.

      In this JSON Schema document, the ascents field is included within the required array, making it mandatory. It also appears in the properties object where it’s defined with a bsonType of object, just like the root document itself.

      Notice that the definition for ascents validation follows a similar principle as the root document. It has the required field, denoting properties the subdocument must contain. It also defines a properties list, following the same structure. Since the ascents field is a subdocument, it’s values will be validated just like those of a larger document would be.

      Within ascents, there’s a required array whose only value is total, meaning that every ascents subdocument will be required to contain a total field. Following that, the total value is described thoroughly within the properties object, which specifies that this must always be a number with a minimum value of zero.

      Again, because neither the first nor the first_winter fields are mandatory for the purposes of this guide, they aren’t included in these validation rules.

      With this schema validation document applied, try inserting the sample Mount Everest document from the first step to verify it allows you to insert documents you’ve already established as valid:

      • db.peaks.insertOne(
      • {
      • "name": "Everest",
      • "height": 8848,
      • "location": ["Nepal", "China"],
      • "ascents": {
      • "first": {
      • "year": 1953,
      • },
      • "first_winter": {
      • "year": 1980,
      • },
      • "total": 5656,
      • }
      • }
      • )

      The document saves successfully, and MongoDB returns the new object identifier:

      Output

      { "acknowledged" : true, "insertedId" : ObjectId("619100f51292cb2faee531f8") }

      To make sure the last pieces of validation work properly, try inserting a document that doesn’t include the ascents field:

      • db.peaks.insertOne(
      • {
      • "name": "Everest",
      • "height": 8848,
      • "location": ["Nepal", "China"]
      • }
      • )

      This time, the operation will trigger an error message pointing out a failed document validation:

      Output

      WriteError({ "index" : 0, "code" : 121, "errmsg" : "Document failed validation", . . . })

      Now try inserting a document whose ascents subdocument is missing the total field:

      • db.peaks.insertOne(
      • {
      • "name": "Everest",
      • "height": 8848,
      • "location": ["Nepal", "China"],
      • "ascents": {
      • "first": {
      • "year": 1953,
      • },
      • "first_winter": {
      • "year": 1980,
      • }
      • }
      • }
      • )

      This will again trigger an error.

      As a final test, try entering a document that contains an ascents field with a total value, but this value is negative:

      • db.peaks.insertOne(
      • {
      • "name": "Everest",
      • "height": 8848,
      • "location": ["Nepal", "China"],
      • "ascents": {
      • "first": {
      • "year": 1953,
      • },
      • "first_winter": {
      • "year": 1980,
      • },
      • "total": -100
      • }
      • }
      • )

      Because of the negative total value, this document will also fail the validation test.

      Conclusion

      By following this tutorial, you became familiar with JSON Schema documents and how to use them to validate document structures before saving them into a collection. You then used JSON Schema documents to verify field types and apply value constraints to numbers and arrays. You’ve also learned how to validate subdocuments in a nested document structure.

      MongoDB’s schema validation feature should not be considered a replacement for data validation at the application level, but it can further safeguard against violating data constraints that are essential to keeping your data meaningful. Using schema validation can be a helpful tool for structuring one’s data while retaining the flexibility of a schemaless approach to data storage. With schema validation, you are in total control of those parts of the document structure you want to validate and those you’d like to leave open-ended.

      The tutorial described only a subset of MongoDB’s schema validation features. You can apply more constraints to different MongoDB data types, and it’s even possible to change the strictness of validation behavior and use JSON Schema to filter and validate existing documents. We encourage you to study the official official MongoDB documentation to learn more about schema validation and how it can help you work with data stored in the database.



      Source link

      How To Perform Full-text Search in MongoDB


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      MongoDB queries that filter data by searching for exact matches, using greater-than or less-than comparisons, or by using regular expressions will work well enough in many situations. However, these methods fall short when it comes to filtering against fields containing rich textual data.

      Imagine you typed “coffee recipe” into a web search engine but it only returned pages that contained that exact phrase. In this case, you may not find exactly what you were looking for since most popular websites with coffee recipes may not contain the exact phrase “coffee recipe.” If you were to enter that phrase into a real search engine, though, you might find pages with titles like “Great Coffee Drinks (with Recipes!)” or “Coffee Shop Drinks and Treats You Can Make at Home.” In these examples, the word “coffee” is present but the titles contain another form of the word “recipe” or exclude it entirely.

      This level of flexibility in matching text to a search query is typical for full-text search engines that specialize in searching textual data. There are multiple specialized open-source tools for such applications in use, with ElasticSearch being an especially popular choice. However, for scenarios that don’t require the robust search features found in dedicated search engines, some general-purpose database management systems offer their own full-text search capabilities.

      In this tutorial, you’ll learn by example how to create a text index in MongoDB and use it to search the documents in the database against common full-text search queries and filters.

      Prerequisites

      To follow this tutorial, you will need:

      Note: The linked tutorials on how to configure your server, install MongoDB, and secure the MongoDB installation refer to Ubuntu 20.04. This tutorial concentrates on MongoDB itself, not the underlying operating system. It will generally work with any MongoDB installation regardless of the operating system as long as authentication has been enabled.

      Step 1 — Preparing the Test Data

      To help you learn how to perform full-text searches in MongoDB, this step outlines how to open the MongoDB shell to connect to your locally-installed MongoDB instance. It also explains how to create a sample collection and insert a few sample documents into it. This sample data will be used in commands and examples throughout this guide to help explain how to use MongoDB to search text data.

      To create this sample collection, connect to the MongoDB shell as your administrative user. This tutorial follows the conventions of the prerequisite MongoDB security tutorial and assumes the name of this administrative user is AdminSammy and its authentication database is admin. Be sure to change these details in the following command to reflect your own setup, if different:

      • mongo -u AdminSammy -p --authenticationDatabase admin

      Enter the password you set during installation to gain access to the shell. After providing the password, your prompt will change to a greater-than sign:

      Note: On a fresh connection, the MongoDB shell will connect to the test database by default. You can safely use this database to experiment with MongoDB and the MongoDB shell.

      Alternatively, you could switch to another database to run all of the example commands given in this tutorial. To switch to another database, run the use command followed by the name of your database:

      To understand how full-text search can be applied to documents in MongoDB, you’ll need a collection of documents you can filter against. This guide will use a collection of sample documents that include names and descriptions of several different types of coffee drinks. These documents will have the same format as the following example document describing a Cuban coffee drink:

      Example Cafecito document

      {
          "name": "Cafecito",
          "description": "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam."
      }
      

      This document contains two fields: the name of the coffee drink and a longer description which provides some background information about the drink and its ingredients.

      Run the following insertMany() method in the MongoDB shell to create a collection named recipes and, at the same time, insert five sample documents into it:

      • db.recipes.insertMany([
      • {"name": "Cafecito", "description": "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam."},
      • {"name": "New Orleans Coffee", "description": "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory."},
      • {"name": "Affogato", "description": "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream."},
      • {"name": "Maple Latte", "description": "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup."},
      • {"name": "Pumpkin Spice Latte", "description": "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree."}
      • ])

      This method will return a list of object identifiers assigned to the newly inserted objects:

      Output

      { "acknowledged" : true, "insertedIds" : [ ObjectId("61895d2787f246b334ece911"), ObjectId("61895d2787f246b334ece912"), ObjectId("61895d2787f246b334ece913"), ObjectId("61895d2787f246b334ece914"), ObjectId("61895d2787f246b334ece915") ] }

      You can verify that the documents were properly inserted by running the find() method on the recipes collection with no arguments. This will retrieve every document in the collection:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." } . . .

      With the sample data in place, you’re ready to start learning how to use MongoDB’s full-text search features.

      Step 2 — Creating a Text Index

      To start using MongoDB’s full-text search capabilities, you must create a text index on a collection. Indexes are special data structures that store only a small subset of data from each document in a collection separately from the documents themselves. There are several types of indexes users can create in MongoDB, all of which help the database optimize search performance when querying the collection.

      A text index, however, is a special type of index used to further facilitate searching fields containing text data. When a user creates a text index, MongoDB will automatically drop any language-specific stop words from searches. This means that MongoDB will ignore the most common words for the given language (in English, words like “a”, “an”, “the”, or “this”).

      MongoDB will also implement a form of suffix-stemming in searches. This involves MongoDB identifying the root part of the search term and treating other grammar forms of that root (created by adding common suffixes like “-ing”, “-ed”, or perhaps “-er”) as equivalent to the root for the purposes of the search.

      Thanks to these and other features, MongoDB can more flexibly support queries written in natural language and provide better results.

      Note: This tutorial focuses on English text, but MongoDB supports multiple languages when using full-text search and text indexes. To learn more about what languages MongoDB supports, refer to the official documentation on supported languages.

      You can only create one text index for any given MongoDB collection, but the index can be created using more than one field. In our example collection, there is useful text stored in both the name and description fields of each document. It could be useful to create a text index for both fields.

      Run the following createIndex() method, which will create a text index for the two fields:

      • db.recipes.createIndex({ "name": "text", "description": "text" });

      For each of the two fields, name and description, the index type is set to text, telling MongoDB to create a text index tailored for full-text search based on these fields. The output will confirm the index creation:

      Output

      { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }

      Now that you’ve created the index, you can use it to issue full-text search queries to the database. In the next step, you’ll learn how to execute queries containing both single and multiple words.

      Step 3 — Searching for One or More Individual Words

      Perhaps the most common search problem is to look up documents containing one or more individual words.

      Typically, users expect the search engine to be flexible in determining where the given search terms should appear. As an example, if you were to use any popular web search engine and type in “coffee sweet spicy”, you likely are not expecting results that will contain those three words in that exact order. It’s more likely that you’d expect a list of web pages containing the words “coffee”, “sweet”, and “spicy” but not necessarily immediately near each other.

      That’s also how MongoDB approaches typical search queries when using text indexes. This step outlines how MongoDB interprets search queries with a few examples.

      To begin, say you want to search for coffee drinks with spices in their recipe, so you search for the word spiced alone using the following command:

      • db.recipes.find({ $text: { $search: "spiced" } });

      Notice that the syntax when using full-text search is slightly different from regular queries. Individual field names — like name or description — don’t appear in the filter document. Instead, the query uses the $text operator, telling MongoDB that this query intends to use the text index you created previously. You don’t need to be any more specific than that because, as you may recall, a collection may only have a single text index. Inside the embedded document for this filter is the $search operator taking the search query as its value. In this example, the query is a single word: spiced.

      After running this command, MongoDB produces the following list of documents:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory." }

      There are two documents in the result set, both of which contain words resembling the search query. While the New Orleans Coffee document does have the word spiced in the description, the Pumpkin Spice Late document doesn’t.

      Regardless, it was still returned by this query thanks to MongoDB’s use of stemming. MongoDB stripped the word spiced down to just spice, looked up spice in the index, and also stemmed it. Because of this, the words spice and spices in the Pumpkin Spice Late document matched the search query successfully, even though you didn’t search for either of those words specifically.

      Now, suppose you’re particularly fond of espresso drinks. Try looking up documents with a two-word query, spiced espresso, to look for a spicy, espresso-based coffee.

      • db.recipes.find({ $text: { $search: "spiced espresso" } });

      The list of results this time is longer than before:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup." } { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." } { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory." }

      When using multiple words in a search query, MongoDB performs a logical OR operation, so a document only has to match one part of the expression to be included in the result set. The results contain documents containing both spiced and espresso or either term alone. Notice that words do not necessarily need to appear near each other as long as they appear in the document somewhere.

      Note: If you try to execute any full-text search query on a collection for which there is no text index defined, MongoDB will return an error message instead:

      Error message

      Error: error: { "ok" : 0, "errmsg" : "text index required for $text query", "code" : 27, "codeName" : "IndexNotFound" }

      In this step, you learned how to use one or multiple words as a text search query, how MongoDB joins multiple words with a logical OR operation, and how MongoDB performs stemming. Next, you’ll use a complete phrase in a text search query and begin using exclusions to narrow down your search results further.

      Step 4 — Searching for Full Phrases and Using Exclusions

      Looking up individual words might return too many results, or the results may not be precise enough. In this step, you’ll use phrase search and exclusions to control search results more precisely.

      Suppose you have a sweet tooth, it’s hot outside, and coffee topped with ice cream sounds like a nice treat. Try finding an ice cream coffee using the basic search query as outlined previously:

      • db.recipes.find({ $text: { $search: "ice cream" } });

      The database will return two coffee recipes:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }

      While the Affogato document matches your expectations, Cafecito isn’t made with ice cream. The search engine, using the logical OR operation, accepted the second result just because the word cream appears in the description.

      To tell MongoDB that you are looking for ice cream as a full phrase and not two separate words, use the following query:

      • db.recipes.find({ $text: { $search: ""ice cream"" } });

      Notice the backslashes preceding each of the double quotes surrounding the phrase: "ice cream". The search query you’re executing is "ice cream", with double quotes denoting a phrase that should be matched exactly. The backslashes () escape the double quotes so they’re not treated as a part of JSON syntax, since these can appear inside the $search operator value.

      This time, MongoDB returns a single result:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." }

      This document matches the search term exactly, and neither cream nor ice alone would be enough to count as a match.

      Another useful full-text search feature is the exclusion modifier. To illustrate how to this works, first run the following query to get a list of all the coffee drinks in the collection based on espresso:

      • db.recipes.find({ $text: { $search: "espresso" } });

      This query returns four documents:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup." } { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }

      Notice that two of these drinks are served with milk, but suppose you want a milk-free drink. This is a case where exclusions can come in handy. In a single query, you can join words that you want to appear in the results with those that you want to be excluded by prepending the word or phrase you want to exclude with a minus sign (-).

      As an example, say you run the following query to look up espresso coffees that do not contain milk:

      • db.recipes.find({ $text: { $search: "espresso -milk" } });

      With this query, two documents will be excluded from the previously returned results:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream." } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }

      You can also exclude full phrases. To search for coffees without ice cream, you could include -"ice cream" in your search query. Again, you’d need to escape the double quotes with backslashes, like this:

      • db.recipes.find({ $text: { $search: "espresso -"ice cream"" } });

      Output

      { "_id" : ObjectId("61d48c31a285f8250c8dd5e6"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup." } { "_id" : ObjectId("61d48c31a285f8250c8dd5e7"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree." } { "_id" : ObjectId("61d48c31a285f8250c8dd5e3"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam." }

      Now that you’ve learned how to filter documents based on a phrase consisting of multiple words and how to exclude certain words and phrases from search results, you can acquaint yourself with MongoDB’s full-text search scoring.

      Step 5 — Scoring the Results and Sorting By Score

      When a query, especially a complex one, returns multiple results, some documents are likely to be a better match than others. For example, when you look for spiced espresso drinks, those that are both spiced and espresso-based are more fitting than those without spices or not using espresso as the base.

      Full-text search engines typically assign a relevance score to the search results, indicating how well they match the search query. MongoDB also does this, but the search relevance is not visible by default.

      Search once again for spiced espresso, but this time have MongoDB also return each result’s search relevance score. To do this, you could add a projection after the query filter document:

      • db.recipes.find(
      • { $text: { $search: "spiced espresso" } },
      • { score: { $meta: "textScore" } }
      • )

      The projection { score: { $meta: "textScore" } } uses the $meta operator, a special kind of projection that returns specific metadata from returned documents. This example returns the documents’ textScore metadata, a built-in feature of MongoDB’s full-text search engine that contains the search relevance score.

      After executing the query, the returned documents will include a new field named score, as was specified in the filter document:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam.", "score" : 0.5384615384615384 } { "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup.", "score" : 0.55 } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree.", "score" : 2.0705128205128203 }

      Notice how much higher the score is for Pumpkin Spice Latte, the only coffee drink that contains both the words spiced and espresso. According to MongoDB’s relevance score, it’s the most relevant document for that query. However, by default, the results are not returned in order of relevance.

      To change that, you could add a sort() clause to the query, like this:

      • db.recipes.find(
      • { $text: { $search: "spiced espresso" } },
      • { score: { $meta: "textScore" } }
      • ).sort(
      • { score: { $meta: "textScore" } }
      • );

      The syntax for the sorting document is the same as that of the projection. Now, the list of documents is the same, but their order is different:

      Output

      { "_id" : ObjectId("61895d2787f246b334ece915"), "name" : "Pumpkin Spice Latte", "description" : "It wouldn't be autumn without pumpkin spice lattes made with espresso, steamed milk, cinnamon spices, and pumpkin puree.", "score" : 2.0705128205128203 } { "_id" : ObjectId("61895d2787f246b334ece914"), "name" : "Maple Latte", "description" : "A wintertime classic made with espresso and steamed milk and sweetened with some maple syrup.", "score" : 0.55 } { "_id" : ObjectId("61895d2787f246b334ece913"), "name" : "Affogato", "description" : "An Italian sweet dessert coffee made with fresh-brewed espresso and vanilla ice cream.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece912"), "name" : "New Orleans Coffee", "description" : "Cafe Noir from New Orleans is a spiced, nutty coffee made with chicory.", "score" : 0.5454545454545454 } { "_id" : ObjectId("61895d2787f246b334ece911"), "name" : "Cafecito", "description" : "A sweet and rich Cuban hot coffee made by topping an espresso shot with a thick sugar cream foam.", "score" : 0.5384615384615384 }

      The Pumpkin Spice Latte document appears as the first result since it has the highest relevance score.

      Sorting results according to their relevance score can be helpful. This is especially true with queries containing multiple words, where the most fitting documents will usually contain multiple search terms while the less relevant documents might contain only one.

      Conclusion

      By following this tutorial, you’ve acquainted yourself with MongoDB’s full-text search features. You created a text index and wrote text search queries using single and multiple words, full phrases, and exclusions. You’ve also assessed the relevance scores for returned documents and sorted the search results to show the most relevant results first. While MongoDB’s full-text search features may not be as robust as those of some dedicated search engines, they are capable enough for many use cases.

      Note that there are more search query modifiers — such as case and diacritic sensitivity and support for multiple languages — within a single text index. These can be used in more robust scenarios to support text search applications. For more information on MongoDB’s full-text search features and how they can be used, we encourage you to check out the official official MongoDB documentation.



      Source link