Implementing Search Functionality with Meilisearch, Prisma and Express

Implementing Search Functionality with Meilisearch, Prisma and Express

Implementing Search Functionality with Meilisearch, Prisma and Express

Introduction

Search functionality is one of the most critical features of the modern web. It enables visitors to quickly and easily find information they are looking for without having to figure out how items are categorized on your page manually. A robust search experience can lead to increased engagement, higher conversion rates, and improve the accessibility and usability of your website. Ultimately, it can lead to more traffic and revenue for your business. The importance of a robust search experience cannot be overstated.

On small websites like blogs, personal websites and small e-commerce sites, users can get by with search boxes powered by a simple Javascript library like lunr.js. Libraries such as these are very beneficial, since they need very little to get off the ground – they can be easily configured, and, very often, do not even need a backend. In fact, depending on the site of the website, it’s possible to implement search functionality using equality operators and “fuzzy matching” algorithms. There’s no need for a library of any sort.

The problem, however, is that these solutions don’t scale. As the size of the website grows, the amount of data to sift through becomes too much for a simple Javascript library to handle. The search experience becomes slow, and the results become less relevant. That’s where programs like Algolia, ElasticSearch,
and Solr come in. They are designed to handle large amounts of data, and are far more versatile than lightweight Javascript libraries.

Despite their advantages, however, these programs come with their own set of downsides. They often aren’t free (Algolia), and often require a lot of expertise to properly set up, deploy, properly utilize and maintain (ElasticSearch). Such capability is usually outside the capabilities of small inexperienced teams and individuals.

Meilisearch is a free, robust, open source library designed for use on small-to-medium-sized projects. It is easy to set up, simple to use, and scales easily with the size of your project.

Among the features it offers are:

  • Typo tolerance
  • Highly-customizable search and ranking
  • Synonyms

In this guide, we will be covering how to set up search functionality for a fictional e-commerce website. We will explain how search works, best practices to consider when implementing search functionality, and how to implement the same using Meilisearch.

How Search Functionality Works

How Search Functionality Works

How Search Functionality Works

Consider a fictional e-commerce website that sells shoes. The website has a database of all the shoes it sells, and, therefore, users should be able to browse through the shoes and find whatever they are looking for. After interviewing customers, we find out that users are most likely to search for shoes by name, or by brand.

The website owners have collected relevant information about the shoes and stored them in a JSON file, but they are not programmers, so they have no idea how to import the data into a database, or how to make it searchable. They have instructed us to create a backend that will allow them to import the data, and find a way to expose the data so that they can fetch, create, update or delete data as they please. They also want to be able to search for shoes by name, brand, or any other relevant information.

After careful consideration of the project owner’s requirements, we have decided to use a relational database to store the data. However, since the relational database is not designed to be searched, we will also need to use a search engine tailored for this functionality. That’s where Meilisearch comes to the rescue. We will need to ensure that the database and the search engine are in sync, so that the search results are always up-to-date.

Here is how it’s going to work:

  • The website owners will import the data into the database using a simple form.
  • The data will be stored in the database.
  • The data will be duplicated in the search engine.
  • The search engine will be queried whenever a user searches for a shoe.
  • The search results will be displayed to the user.

Setting up an SQLite Database

For this project, we will be using a simple SQLite database to store our data. SQLite is a lightweight relational database that, as compared to something like Postgres, is easy to set up and use. It doesn’t require much expertise to use, but in production, you’ll probably want something that scales more easily. For our purposes, however, SQLite is more than adequate.

Ensure you have SQLite installed on your machine. If you don’t, you can install it using the following command:

To create a new database, run the following command:

Then, to view the list of databases:

Next, let’s create a table to store our products:

Voila! Our database is now ready to be used. We can now import our data into the database. We could do this directly using SQL, but since we will be setting up an API to manipulate the data later, it makes more sense to implement this functionality using an ORM.

A Brief Introduction to Prisma

 

Introduction to Prisma

Introduction to Prisma

Prisma is an open-source, type-safe, and modern ORM (Object-Relational Mapping) tool that is designed to simplify database access and management in modern web applications. It provides a powerful and intuitive API that allows developers to easily and efficiently interact with their databases.

Out of the box, Prisma is type-safe. It generates a strongly-typed data access layer based on the database schema, which ensures that developers can avoid runtime errors that are common with dynamic ORM libraries. This also enables powerful tooling such as auto-completion and type-checking, making it easier for developers to write reliable and maintainable code.

It also works with any database. Prisma supports both SQL and NoSQL databases, and provides a flexible and powerful query API that allows developers to write complex database queries without sacrificing performance.

To get started, we need to install Prisma and the SQLite driver:

Then, we need to initialize Prisma:

This will create a new folder named “prisma” at the root directory of our project and a “schema.prisma” file inside it. This is where we will define our database schema.

Now, we need to set up DATABASE_URL in our .env file:

We can now pull the schema from our database into our schema.prisma file:

This command will result in the following schema:

The products model is automatically generated by Prisma based on the schema of the products table in our database.

If we chose to generate the database access objects at this point, our models inside the code would be named products, which isn’t a very intuitive name. We can change this by adding the @@map directive to the model:

Finally, we can generate our database access objects:

Creating a Basic CRUD API

Our project requires a basic API that will allow project owners to create, read, update and delete products.

One of the biggest downsides of using NodeJS is that it’s very bare-bones. It doesn’t have a very comprehensive standard library, so a lot of features that other languages have out-of-the-box need to be implemented manually. In addition, you’ll be hard-pressed to find an equivalent of the Django ORM, or the Rails Active Record. For that reason, it’s useful to have knowledge of how to structure your NodeJS project.

In brief:

  • Routes: Routes are responsible for handling requests and passing them on to the appropriate controller.
  • Controllers: Controllers are responsible for handling requests and returning responses. They are the middleman between routes and complex “business logic”.
  • Services: Services are responsible for handling complex “business logic”. They are the middleman between controllers and the database.
  • Models: Models are responsible for handling database operations. They are the middleman between services and the database. One of the great things about Prisma is that it handles all the database operations for you, so we don’t really need to manually create models. However, depending on your use case, it may still be a great idea to create models.

First, let’s install some dependencies:

Then, we need to initialize Typescript, so we can have type safety in our project:

This produces the default recommended Typescript configuration, which we can trim down and modify to suit our needs:

Next, we need a bare-bones version of our server. We will be using Express for this:

All this does for now is start a server on port 1337, and respond with a simple “Hello world” message when a user visits the root route.

Next, let’s create a controller that will allow clients to create, read, update and delete products.

Notice that the controller is very simple. It doesn’t contain any business logic, and, for the sake of brevity, it doesn’t contain any error handling or validation. All it does is call the appropriate service method, and return the result. All the business logic takes place inside services.

Since we are using Prisma, we don’t need to manually create database models. Therefore, all database operations will take place inside the services.

To conclude this section, let’s create a route for our controller:

This route will be mounted on the /products route, so the full route for the “getOne” will be /products/1, for instance.

Let’s implement those endpoints in the main file:

Now, the only thing left to do is set up Meilisearch and start indexing our products.

Setting up Meilisearch

Meilisearch

Meilisearch

Meilisearch stores data in the form of records referred to as “documents”, much like MongoDB. Documents are further grouped into collections referred to as “indexes”.

In our application, for instance, we will have an index referred to as “products”. Every index must have a primary key – that is a unique identifier for each document. In our case, we will use the product’s ID as the primary key.

Individual products (henceforth referred to as “documents”) will look like so:

To take advantage of the powerful capabilities Meilisearch offers us, we need to set it up first. First, let’s create a ISearchService class that will handle all the Meilisearch-related functionality. We will also create a MsSearchService interface that implements the ISearchService class. This is a good practice to follow, as it allows us to easily swap out the implementation of our MsSearchService class without having to change the rest of our codebase.

For now, we only need three methods:

  • createIndex – creates a new index
  • addDocuments – adds documents to an index
  • search – searches an index

Now, let’s create our MsSearchService class:

As simply as that, we are now ready to use MeiliSearch to power our search functionality. But first, we need some data to work with.

Adding products to MeiliSearch

At this point, we need to modify our service slightly, so that products are added, updated, and deleted to and from MeiliSearch whenever a corresponding action is performed on the API.

We now need to add the search functionality to our routes and controller. First, let’s add the functionality to our
controller:

Then, let’s add the route to our routes file:

Now, we can test our search functionality. To do that, we need to add some products to our database. Luckily, we already have a JSON file with some products in it. You can find it inside the db folder of the project.

Once we’ve made sure Meilisearch is running, we can use our API to add these products to our database.

Now, let’s try to search for a product:

If everything went well, you should see a response similar to this:

Note: jq is a command-line JSON processor that we use to format the output from curl. You can install it by running sudo apt install jq on Linux.

Meilisearch also comes with an in-built preview feature that allows us to test the search functionality without having to deploy our own backend. It can be accessed by visiting http://localhost:7700 Here it is in action against our database:

Milisearch application screen

Milisearch application screen

Explore the code on Github at your own leisure.

Conclusion

In this guide, we’ve learned how to use MeiliSearch to power our search functionality. We’ve also learned how to set up and structure a basic API using Express and TypeScript. We’ve also learned how to add, update, and delete documents from MeiliSearch using our API.

If you want to learn more about MeiliSearch, you can check out their documentation.

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

Related articles

26.03.2024

An Introduction to Clustering in Node.js

Picture your Node.js app starts to slow down as it gets bombarded with user requests. It's like a traffic jam for your app, and no developer likes ...

15.03.2024

JAMstack Architecture with Next.js

The Jamstack architecture, a term coined by Mathias Biilmann, the co-founder of Netlify, encompasses a set of structural practices that rely on ...

Rendering Patterns: Static and Dynamic Rendering in Nextjs

Next.js is popular for its seamless support of static site generation (SSG) and server-side rendering (SSR), which offers developers the flexibility ...

No comments yet

Sign in

Forgot password?

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy

Password recovery

You can also try to

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy