👉
This post was originally published in May 2020 by guest author Riccardo Giorato. At the time, Meilisearch was on v0.09. It has been updated by Carolina Ferreira to work with Meilisearch v0.29. You can find the first version of the post on GitHub.

Introduction

In this tutorial, you'll learn how to easily create a search-based web app with instant and reliable results thanks to the power of Meilisearch.

We will cover the basic steps to add our data to Meilisearch, create a custom front-end search, and move on to customization at the end.

For this tutorial, we are going to create an instant search experience for a sports brand. Here is a preview of what you will be building:

Searching for multiple items on the decathlon demo using Meilisearch

Prerequisites

Before getting started, ensure that you have Node.js >= 14 installed on your machine.

You can follow this tutorial and write the code as you go through the steps, using this GitHub project.

Finally, this tutorial assumes that you are already familiar with React. If that is not the case, you can check the React Documentation to learn more.

Getting Started

Clone the repository

Clone the GitHub repository using the following command:

git clone https://github.com/meilisearch/tutorials.git
cd src/react-decathlon

Run a new Docker image

If you cloned the repository to set up the Meilisearch instance, just execute the following command inside the main folder:

npm run setup_meili

If you didn't clone the repository and want to launch Meilisearch using Docker, execute this command:

docker run -it --rm \
    -p 7700:7700 \
    -v $(pwd)/meili_data:/meili_data \
    getmeili/meilisearch:v0.28 \
    meilisearch --env="development"
👉
By default, Meilisearch's API is unprotected. You will need a master key in production. You can learn more about it in our documentation.

You can check if Meilisearch is running by visiting: http://localhost:7700/

Create an index in Meilisearch

An index is an entity where documents are stored, like an array of objects with some specific settings attached to it, and a unique primary key.

Each document indexed must have a primary field, a special field that must be present in all documents. This field holds the unique value of the document: its id.

Meilisearch can infer the primary key from your dataset, provided that it contains the id substring. You can also set it explicitly.

Below is a sample document to add to Meilisearch.

{
  "id": 100013768717,
  "name": "Fitness Foldable Shoe Bag",
  "url": "https://www.decathlon.com/products/gym-foldable-shoe-bag",
  "vendor": "Domyos",
  "category": "Sport bag",
  "tags": [
    "Artistic Gymnastics",
    "Boy's",
    "CARDIO_FITNESS_ACCESSORIES",
    "CARDIO_FITNESS_BAGS",
    "CODE_R3: 11782"
  ],
  "images": "https://cdn.shopify.com/s/files/1/1330/6287/products/sac_20a_20chaussure_20kaki_20_7C_20001_20_7C_20PSHOT_20_490180e6-44e4-4340-8e3d-c29eb70c6ac8.jpg?v=1584683232",
  "creation_date": "2020-04-03T15:58:48-07:00",
  "price": "2.49"
}

You can easily create this index with a REST client like Postman, but in this tutorial, we will use the Meilisearch Javascript SDK to do it directly from Node.js.

const { MeiliSearch } = require('meilisearch')

;(async () => {
  try {
    const config = {
      host: 'http://127.0.0.1:7700'
    };

    const meili = new MeiliSearch(config);
    
    await meili.createIndex('decathlon'); 
    
    // or you can set the primary key explicitly: 
    // await meili.createIndex({ uid: "decathlon", primaryKey: "id" });
        
  } catch (e) {
    console.error(e);
    console.log("Meili error: ", e.message);
  }
})();

You can read more about the properties of indexes in the Meilisearch documentation.

Index documents

Meilisearch receives documents in JSON format and stores them for searching purposes. These documents are composed of fields that can hold any type of data. Meilisearch also accepts datasets in the following formats: NDJSON and CSV. You can read more about the format in the documentation.

For this tutorial, you can download this dataset full of sportswear items: decathlon.json

Use the following script to upload all the objects from this JSON file to Meilisearch. Remember to change the path to your JSON file before running it!

const { MeiliSearch } = require('meilisearch')

;(async () => {
  try {
    const config = {
      host: 'http://127.0.0.1:7700'
    };

    const meili = new MeiliSearch(config);

    const decathlon = require("../decathlon.json"); // path to json file

    const index = meili.index("decathlon");
    
    await index.addDocuments(decathlon);
        
  } catch (e) {
    console.error(e);
    console.log("Meili error: ", e.message);
  }
})();

Prepare the React app

We'll need a standard React app. You can use the project you cloned before in the Getting started section.

If you prefer to start from an empty app, you can create your own using Create React App with the command below. You can name the application however you desire.

npx create-react-app meili_react_demo
cd meili_react_demo

Include Tailwind CSS

To speed up the styling process, add Tailwind CSS style directly into the <head> element of the index.html file:

  <script src="https://cdn.tailwindcss.com"></script>

Configure App.js state

Then, modify the App.js file using this code to set up a simple search form and a few state variables to handle every aspect of the search.

import React, { useState, useEffect } from 'react'
import { MeiliSearch } from 'meilisearch'
import Item from './components/Item'

// TODO configure the MeiliSearch Client

const index = client.index('decathlon')

function App () {
  const [searchedWord, setSearch] = useState('')
  const [resultSearch, setResults] = useState([])

  // TODO add function to send searchedWord to Meilisearch

  return (
    <div className='mx-auto'>
      <div className='header font-sans text-white items-center justify-center'>
        <header className='py-12'>
          <img
            className='h-20 w-auto items-center justify-center p-2 mx-auto'
            src='/wide_logo.png'
            style={{ filter: 'invert(0%)' }}
            alt='Decathlon logo'
          />
          <h1 className='flex flex-wrap flex-grow text-3xl w-full justify-center p-4'>
            Stop looking for an item — find it and work hard!
          </h1>
          <div className='border rounded overflow-hidden w-full flex justify-center mx-auto searchBox mt-6'>
            <button className='flex items-center justify-center px-4 shadow-md bg-white text-black'>
              <svg
                className='h-4 w-4 text-grey-dark'
                fill='currentColor'
                xmlns='http://www.w3.org/2000/svg'
                viewBox='0 0 24 24'
              >
                <path d='M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z' />
              </svg>
            </button>
            <input
              type='text'
              value={searchedWord}
              onChange={(event) => setSearch(event.target.value)}
              className='px-6 py-4 w-full text-black'
              placeholder='Product, sport, color, …'
            />
          </div>
        </header>
      </div>
      <div>
        <div className='flex flex-wrap searchResults'>
          // TODO iterate over the search results to display them with the Item component
        </div>
      </div>
    </div>
  )
}

export default App

This code should output this beautiful header with a search form.

Decathlon header with a search bar

Search results in React

Connecting React with Meilisearch using the Javascript SDK is a simple operation that can be done in just a few steps.

Meilisearch client

Install the Meilisearch SDK using the following command:

// if you use npm
npm install meilisearch
// if you use yarn
yarn add meilisearch

Set up the Meilisearch client with the server URL. In our case, it was the localhost Docker machine. Finally, load the right index from the backend.

Replace this comment in App.js with the code snippet below:

"// TODO configure the Meilisearch Client"

import { MeiliSearch } from "meilisearch";

const client = new MeiliSearch({
  host: "http://127.0.0.1:7700/",
});

const index = client.index("decathlon");

Send the search query

Add a useEffect hook to execute the search of the typed words into Meilisearch. All the results will be set to a simple state variable called resultsSearch.

Replace this comment in App.js with the code snippet below:

"// TODO add function to send searchedWord to Meilisearch"

  useEffect(() => {
    // Create a scoped async function in the hook
    async function searchWithMeili() {
      const search = await index.search(searchedWord);
      setResults(search.hits);
    }
    // Execute the created function directly
    searchWithMeili();
  }, [searchedWord]);

Showcase the results

You will iterate over the JSON objects returned by Meilisearch —they'll have the same structure as the uploaded JSON objects— and you'll display them in an Item component, linking to the product pages.

Let's create the Item component that will help us display our products. Create a components folder with an Item.js file inside, and copy-paste the following code snippet:

function Item ({ url, image, name, category, vendor, price, id }) {
  return (
    <div className='flex w-full sm:w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/6 p-3' key={id}>
      <a className='flex-1 rounded overflow-hidden shadow-lg' href={url}>
        <img
          className='w-full h-48 object-cover'
          src={image}
          alt={name}
          onError={(e) => {
            e.target.onerror = null
            e.target.src = '/wide_logo.png'
          }}
        />
        <div className='px-6 py-3'>
          <div className='font-bold text-sm mb-1 text-gray-600 capitalize'>
            {category}
          </div>
          {name}
          <div className='font-bold text-xl mb-2 text-gray-800'>
            {vendor} -
          </div>
          <p className='text-black text-xl font-bold text-base py-2'>
            $ {price}
          </p>
        </div>
      </a>
    </div>
  )
}

export default Item

Then, replace this comment in App.js with the code snippet below:

{resultSearch?.map((result) => (
    <Item
      url={result.url}
      image={result.images}
      name={result.name}
      category={result.category}
      vendor={result.vendor}
      price={result.price}
      key={result.id}
      />
))}

You can have a look at the fullcode on GitHub.

With Meilisearch, you get a ton of customization options for fine-tuning your search experience. We will take a look at a few features here. You can read more about them in the documentation.

Search Ranking

We will start with changing the ranking rules, the criteria Meilisearch uses to sort the documents you uploaded whenever a search query is made. The order of the ranking rules influences the relevancy of your search results. You can learn more about it in the documentation.

Let's use the following order:

[
  "words",
  "typo",
  "proximity",
  "attribute",
  "sort",
  "exactness",
  “desc(creation_date)”
]

This uses the default order along with a custom rule: creation_date. This rule ranks items by their creation date if all previous values are identical.

Searchable attributes

You can also configure searchable attributes. These are attributes whose values Meilisearch searches for matching query words. By default, all attributes are searchable, but you can configure it so that it only searches the name, vendor, category, and tags fields, leaving out images and URL:

searchableAttributes: ["name", "vendor", "category", "tags"]

Displayed attributes

Displayed attributes are attributes that Meilisearch can return to the user in the front-end application with the displayedAttributes array. Like searchable attributes, all attributes are displayed by default.

    "displayedAttributes": [
      "name",
      "vendor",
      "category",
      "tags",
      "images",
      "url"
    ]

Upload the new settings to Meilisearch

It’s time to customize our Meilisearch index with the search settings explained above.

const { MeiliSearch } = require('meilisearch')

;(async () => {
  try {
    const config = {
      host: 'http://127.0.0.1:7700'
    };

    const meili = new MeiliSearch(config);
    
    const index = meili.index("decathlon");

    const newSettings = {
      rankingRules: [
        "words",
        "typo",
        "proximity",
        "attribute",
        "sort",
        "exactness",
        "desc(creation_date)"
      ],
      searchableAttributes: ["name", "vendor", "category", "tags"],
      displayedAttributes: [
        "name",
        "vendor",
        "category",
        "tags",
        "images",
        "url"
      ]
    };

    await index.updateSettings(newSettings);
        
  } catch (e) {
    console.error(e);
    console.log("Meili error: ", e.message);
  }
})();

Conclusion

This quick search wouldn’t be possible without an incredible team working on this great project night and day! If you enjoy contributing to the Meilisearch family, check out the following repositories:

General discussion is very welcome on GitHub discussions or our community Slack.

Don’t forget to leave a star on the main project on Github!