DEV Community

Shivay Lamba
Shivay Lamba

Posted on • Updated on

Integrate a seamless search experience in an NextJS e-commerce application with Meilisearch

Drastically improve your users' product discovery experience with our easy-to-use and easy-to-deploy search engine.

Introduction

Searching is a crucial feature of any e-commerce application which directly affects conversions. A good search experience requires quick and accurate search results. However, it requires investing time and developer resources to build. Here's when Meilisearch enters the picture—providing an open-source search engine that is lightning fast, hyper-relevant, and typo-tolerant with little to no setup time.

In this tutorial, we'll add a seamless search experience with Meilisearch to a light e-commerce application. We will import a list of 1,000 products to Meilisearch. Users can search through these products and benefit from advanced features like filtering and sorting options.

Upon completion, you will have an app similar to our demo application at http://ecommerce.meilisearch.com, which provides rapid search results on a catalog of over 1.4 million products.

*Prerequisites*

  1. NodeJs
  2. Next.js - React Framework
  3. Meilisearch

Getting Started

Let's get started with the installation of the necessary tools.

1. *Install and launch Meilisearch*

You have many ways to install Meilisearch. One of the methods is to use cURL for the installation.

You can paste the following code into your terminal:

# Install Meilisearch
curl -L https://install.meilisearch.com | sh

# Launch Meilisearch
./meilisearch
Enter fullscreen mode Exit fullscreen mode

This command will launch a local Meilisearch server at http://localhost:7700/.

2. Adding data to Meilisearch

Create and navigate to a new folder called seed. You can use the following command to install Meilisearch's Javascript client using npm.

npm install meilisearch
Enter fullscreen mode Exit fullscreen mode

We have gathered a list of 1,000 products from various Amazon datasets and compiled them in a data.json file. We will add these products to the Meilisearch index.

You can download this data.json file from GitHub.

Each record is associated with a product. Each product has a brand, category, tag, price, rating, and other related information. We will make these attributes sortable and filterable in our Meilisearch instance via [updateFilterableAttributes](https://docs.meilisearch.com/reference/api/filterable_attributes.html#update-filterable-attributes) and [updateSortableAttributes](https://docs.meilisearch.com/reference/api/sortable_attributes.html#update-sortable-attributes) methods.

/* Here's an example of a product record in the data.json file */

[
    {
        "id": "711decb2a3fdcbbe44755afc5af25e2f",
    "title": "Kitchenex Stainless Steel Flatware Pie Server and Pie Cutter Set of 2",
    "description": "The Kitchen Stainless Pie Server is both flashy and chic...",
        "category": "Home & Kitchen",
    "brand": "Dr. Pet",
    "price": 16.84,
        "tag": "Kitchen",
    "rating": 4.7,
    "reviews_count": 7
    },
    ...999 more items
]
Enter fullscreen mode Exit fullscreen mode

Let’s create a script to add the data from the data.json file to a products index in Meilisearch. Here’s the code of the script:

const { MeiliSearch } = require("meilisearch");

const client = new MeiliSearch({
  host: "http://localhost:7700",
  apiKey: "", // No API key has been set
});

const INDEX_NAME = "products";

const index = client.index(INDEX_NAME);

const data = require("./data.json");

(async () => {
  console.log(`Adding Filterable and Sortable Attributes to "${INDEX_NAME}"`);
  await index.updateFilterableAttributes([
    "brand",
    "category",
    "tag",
    "rating",
    "reviews_count",
    "price",
  ]);
  await index.updateSortableAttributes(["reviews_count", "rating", "price"]);

  console.log(`Adding Documents to "${INDEX_NAME}"`);
  await index.updateDocuments(data);
    console.log('Documents Added');
})();
Enter fullscreen mode Exit fullscreen mode

If you called your script seed.js, you could run the command below to start adding data to your Meilisearch instance:

node seed.js
Enter fullscreen mode Exit fullscreen mode

Wait until the data is ingested into the Meilisearch instance. It is usually done in a few seconds. You may go to [http://localhost:7700/](http://localhost:7700/) to check the list of 1000 products in the Meilisearch.

Image description

3. Set up the project

Let's build an e-commerce application in Next.js. N*ext.js* is an open-source React framework that enables server-side rendering and static website generation.

We can set up a Next.js application using the create-next-app tool. Navigate to your project base directory in the terminal, and use the following command to install a Next.js application:

npx create-next-app@latest ecommerce
Enter fullscreen mode Exit fullscreen mode

It may take a few minutes to complete. This command will create a folder named ecommerce in your project base directory with all the boilerplate code. Navigate to the ecommerce folder and install the following npm libraries:

  1. react-instantsearch-dom - an open-source library that uses InstantSearch.js to build search interfaces in front-end applications quickly.
  2. @meilisearch/instant-meilisearch - It's a client for InstantSearch.js. It establishes communication between Meilisearch and InstantSearch.

You can install these libraries using the following command:

npm i --save react-instantsearch-dom @meilisearch/instant-meilisearch
Enter fullscreen mode Exit fullscreen mode

You may add some pre-defined stylesheet files. You can download all the CSS modules from here and paste all the files into the styles folder in the base directory.

Important Note:

As of Next.js v12 and above, react-instantsearch-dom is not working as expected when React’s Strict mode is enabled (refer to link). A quick and easy fix for this is to disable strict mode. You can disable it by adding reactStrictMode: false to next.config.js. You may find this file at the root of your project.

/* next.config.js */

module.exports = {
  reactStrictMode: false, // set this to false
  images: {
    domains: [''],
  },
}
Enter fullscreen mode Exit fullscreen mode

To apply modifications, restart the Next.js server.

4. Building and integrating components

We have completed the initial step of seeding the data to the Meilisearch instance and installing the necessary tools. From here, we will start integrating the components required for creating a search experience in the application.

4.1 Adding search box and connecting components to Meilisearch

We can create a navigation bar that contains a search box to perform searches within the application. We will use the SearchBox component from the react-instantsearch-dom library to do so.

You can create a navbar.jsx file in the components/layout folder and use the following code in it:

import React from 'react';
import { SearchBox } from 'react-instantsearch-dom';
import styles from '../../styles/nav-bar.module.css';

export const NavBar = () => {
  return (
    <div className={styles.container}>
      <SearchBox />
    </div>
  );
};

export default NavBar;
Enter fullscreen mode Exit fullscreen mode

We must connect the SearchBox component to Meilisearch. We will create a searchClient with the necessary credentials and pass it to the InstantSearch component as a prop along with the index name.

To do this, you can create a layout.jsx file in the components/layout/ folder and use the following code in it:

import React from 'react';
import NavBar from './navbar';
import { InstantSearch } from 'react-instantsearch-dom';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';

const searchClient = instantMeiliSearch('http://localhost:7700', '');

const Layout = ({ children }) => {
  return (
    <InstantSearch indexName="products" searchClient={searchClient}>
      <NavBar />
      <main>{children}</main>
    </InstantSearch>
  );
};

export default Layout;
Enter fullscreen mode Exit fullscreen mode

Next.js uses the App component to initialize pages. Every time the server is launched, _app.js is the primary file rendered. We will add the Layout component and a few stylesheet files to the _app.js file. Adding the Layout component will supply the search results to all the child components.

You may replace the code in the _app.js file with the following code:

import Layout from "../components/layout/layout";
import "../styles/globals.css";
import "../styles/searchBoxAIS.css";

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

4.2 Creating various search filters

Let’s add a filter functionality to the application. We will need a few components from the react-instantsearch-dom library:

  1. The RefinementList component can be used to filter the data based on facets. The number of items related to the given filter will also be displayed.

    <RefinementList attribute="category" />
    
  2. The RatingMenu component creates a rating list. We need to define what’s the maximum rating. It displays a select menu (starting from 1 star till max) to select the rating. The component only works with integers.

     <RatingMenu attribute="rating" max={5} />
    
  3. The ClearRefinements component will be used to provide a button that will clear all the applied filters within the application. This is handy when we've applied many filters and want to delete them all at once.

    <ClearRefinements />
    

We will add all three components to implement the filter functionality. You can create a new file SearchFilters.jsx in the components/home/ folder and use the following code:

import {
  ClearRefinements,
  RatingMenu,
  RefinementList,
} from 'react-instantsearch-dom';

const SearchFilters = () => (
  <div>
    <h2>
      <span>Filters</span>
      <ClearRefinements />
    </h2>
    <h4>Categories</h4>
    <RefinementList attribute="category" />
    <h4>Tags</h4>
    <RefinementList attribute="tag" />
    <h4>Brands</h4>
    <RefinementList attribute="brand" />
    <h4>Rating</h4>
    <RatingMenu attribute="rating" max={5} />
  </div>
);

export default SearchFilters;
Enter fullscreen mode Exit fullscreen mode

4.3 Implementing a product card component

We will create a component named Hit to represent a single product. We may design a card view for the component that will include the basic information for the product, like - title, description, rating, review count, price, brand, and image. All these details can be retrieved from the component’s product prop. After that, we can display the results by looping over the component.

We can create a file hits.jsx in the components/home/ folder. You can paste the following lines of code into the file:

import styles from "../../styles/searchResult.module.css";

const Hit = ({ product }) => {
  const { rating, images, title, description, brand, price, reviews_count } = product;

  return (
    <div className={styles.card}>
      <div
        className={styles.productResImg}
        style={{ backgroundImage: `url(${images[0]})` }}
      />
      <div className={styles.productResCntnt}>
        <h6 className={styles.productResBrand}>{brand}</h6>
        <div className={styles.productResTitl}>{title}</div>
        <div className={styles.productResDesc}>
          {description.substring(0, 50)}...
        </div>
        <div className={styles.productResPrice}>${price}</div>
        <div className={styles.productResReview}>
          {reviews_count ? (
            <div className={styles.productRateWrap}>
              <span className={styles.productRate}>
                {reviews_count} review
              </span>{" "}
              <span>{rating}</span>
            </div>
          ) : (
            "No Review"
          )}
        </div>
      </div>
    </div>
  );
};

export default Hit;
Enter fullscreen mode Exit fullscreen mode

4.4 Designing a page layout to display the list of products

It’s time to design the product list page with a sort feature and pagination to display a limited number of products on a page. For this, we can use the following components from the react-instantsearch-dom library:

  1. The Pagination component, by default, allows you to display a list of 20 products per page. We can also provide the showLast property to display an estimate of the number of pages.

    <Pagination showLast={true} />
    
  2. The SortBy component will sort the search results based on the facets. We need to provide a Meilisearch index and a list of value and label.

    Let's say we need to sort the category in the index products alphabetically. You may use the following code:

    <SortBy
      defaultRefinement="products"
      items={[
        { value: 'category', label: 'Sort category alphabetically' },
      ]}
    />
    

    The SortBy has a few properties, including the defaultRefinement attribute for providing the Meilisearch index and the items attribute to provide a list of value and label.

    The value includes the document's attribute, and as the name suggests, we will use the label to add label text in a dropdown list.

  3. The connectStateResults hook will retrieve the search results from the Meilisearch instance using InstantSearch. The connectStateResults function has some arguments, including:

    • searchState provides the information about the user's input in the provided search box.
    • searchResults gives the result obtained after querying the Meilisearch instance.
    • The searching argument represents the loading state for the result retrieval from the Meilisearch.

We will use the searchState argument to get the list of products and loop the list using the map function over the Hit component to display the result. We will use the SortBy component to sort based on price and reviews_count.

We can create a file SearchResult.jsx in the component/home/ folder and use the following code:

import {
  Pagination,
  SortBy,
  connectStateResults,
} from 'react-instantsearch-dom';
import SearchFilters from './SearchFilters';
import Hit from "./hits.jsx";
import styles from '../../styles/searchResult.module.css';

const Results = connectStateResults(
  ({ searchState, searchResults, searching }) => {
    const hits = searchResults?.hits;

    if (searching) {
      // This will show the loading state till the results are retrieved
            return 'Loading';
    }

    return (
      <>
        <SearchFilters />
        <div className={styles.products}>
                    {/* Condition for rendering component based on results */}
          {searchResults?.nbHits !== 0 ? (
            <>
              <div className={styles.resultPara}>
                <span>
                  Showing {searchResults?.hits.length || 0} of{' '}
                  {searchResults?.nbHits || 0}{' '}
                  {searchState.query &&
                    !searching &&
                    `for "${searchState.query}"`}
                </span>
                <div>
                  <SortBy
                    defaultRefinement="products"
                    items={[
                      { value: 'products', label: 'Sort' },
                      {
                        value: 'products:price:desc',
                        label: 'Price: High to Low',
                      },
                      {
                        value: 'products:price:asc',
                        label: 'Price: Low to High',
                      },
                      {
                        value: 'products:reviews_count:desc',
                        label: 'Most Reviews',
                      },
                    ]}
                  />
                </div>
              </div>
              <div className={styles.grid}>
                                {/* Using search result list, we will loop over the Hit component */}
                {hits?.length > 0 &&
                  hits.map((product) => (
                    <Hit key={product.id} product={product} />
                  ))}
              </div>
            </>
          ) : (
            <p className={styles.paragraph}>
              No results have been found for {searchState.query}.
            </p>
          )}
                    {/* Adding pagination over the results */}
          <Pagination showLast={true} />
        </div>
      </>
    );
  }
);
export default Results;
Enter fullscreen mode Exit fullscreen mode

4.5 Displaying results on the application

We have created all the required components to display the results. We need to add the Results component to the index.jsx file to display the result on the screen. You can copy and paste the following code into the index.jsx file.

import styles from '../styles/home.module.css'
import Results from '../components/home/SearchResult'

export default function Home () {
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <div className={styles.mainContent}>
          <Results />
        </div>
      </main>
    </div>
  )
}

export const getStaticProps = async () => {
  return {
    props: {}
  }
}
Enter fullscreen mode Exit fullscreen mode

You can use the following command to run the application:

npm run dev 
Enter fullscreen mode Exit fullscreen mode

Go to http://localhost:3000. The output will resemble the image provided below:

Image description

You can find the complete code here, https://github.com/shivaylamba/demos/tree/main/src/ecommerce.

Conclusion

We created a lightning-fast search experience for an e-commerce application. You can now take the learnings from above and integrate a similar search experience into your applications. You may play with typo-tolerance, geo-search filters, and many other features to better suit your needs.

Here are some references of real-world examples of Meilisearch used in e-commerce experiences:

  1. https://www.minipouce.fr/i/marketplace/explore
  2. https://palmes.co/

We're excited to see what you come up with. Share your experience and Meilisearch integration in your e-commerce application on the Slack community.

If you have any queries or suggestions, please let us know on Slack. For more information on Meilisearch, check out our Github repository and official documentation.”

Top comments (0)