MeiliSearch is a search engine written in Rust, and it is also open source. Since it’s written in Rust, I don’t have to tell you how stable it is, and Meilisearch includes all the search features you can think of. One of the reasons I chose it was because I only wanted to use its API and search capabilities, but I wanted to fully customize a whole set of UI and interactions etc., meaning I didn’t want to develop a Search API.

Here, I document how to integrate MeiliSearch service to do a custom search box function. Since the official documentation is still relatively complete, this article will summarize some of the considerations and pitfalls encountered when customizing.

Front-end integration with MEILISEARCH

Deploying MeiliSearch

According to the official deployment guide, you can deploy MeiliSearch locally very easily. The official documentation also provides a variety of deployment methods, Docker, K8s, AWS, and more.

Note when deploying: If we are deploying in production mode, we need to set a Master Key, which is used to get the Search key and Admin key.

1
2
3
4
5
6
# Mac

# Update brew and install Meilisearch
brew update && brew install meilisearch

meilisearch --master-key={MASTER_KEY} --env production
  • Search key: can only be used for query and can be exposed directly to the configuration file.
  • Admin key: can be used for anything, but we should not use it for query operations when we use it in production and should not expose it in any of the places.
  • env: the default is development when the env parameter is not passed, you can see the MeiliSearch page on the corresponding port, when set to production it is a pure backend service.

Note that MasterK ey must be set when starting in production mode.

Preparing the document index

We need to upload the document index we use to search for the MeiliSearch service. An official example is provided and you can directly download it for testing.

It has the following format.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[
  {
    "id": 0,
    "title": "...",
    ...
  },
  {
    "id": 1,
    "title": "...",
    ...
  }
  ...
]

Upload document index to Meilisearch

Here are some APIs used, you can read the official documentation to see all of them.

  1. obtain Admin Key and Search Key.

    1
    
    curl -X GET 'http://localhost:7700/keys' -H 'Authorization: Bearer {MASTER_KEY}'
    
  2. Obtaining document index information.

    1
    
    curl -X GET 'http://localhost:7700/indexes' -H 'Authorization: Bearer {ADMIN_KEY}
    
  3. Adding or replacing document indexes.

    1
    2
    3
    4
    5
    
    curl \
    -X POST 'http://localhost:7700/indexes/{INDEX_NAME}/documents?primaryKey=id' \
    -H 'Authorization: Bearer {ADMIN_KeY}' \
    -H 'Content-Type: application/json' \
    --data-binary {YOUR_DOC_DATA}
    

    The parameter primaryKey is used to identify each document in the index, ensuring that there cannot be two identical documents in the same index.

  4. Deleting document indexes.

    1
    2
    
    curl -X DELETE 'http://localhost:7700/indexes/{INDEX_NAME}/documents' \
    -H 'Authorization: Bearer {ADMIN_KEY}'
    

Front-end integration

Officially available has examples for js, React, Vue, using the react- instantsearch-dom and instant-meilisearch libraries.

Since I found that the library react-instantsearch-dom doesn’t support TS projects when I was developing the actual project, it didn’t declare a corresponding type, so I opted for a different solution and instead used react-instantsearch-hooks-web to directly encapsulate my custom components using Hooks.

  1. Declare search instance

    1
    2
    3
    4
    5
    6
    
    import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
    
    export const searchClient = instantMeiliSearch(
    '{YOUR_SERVER}',
    '{YOUR_SEARCH_KEY}'
    );
    
  2. Encapsulates a custom search box

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    mport React from 'react';
    import { useSearchBox } from 'react-instantsearch-hooks-web';
    
    const CustomSearchBox: React.FC = () => {
    const { refine } = useSearchBox();
    
    return (
        <form noValidate action="" role="search">
        <Input
            autoFocus
            onChange={(event) => refine(event.currentTarget.value)}
            type="search"
            placeholder="Search the articles"
        />
        </form>
    );
    };
    
    export default CustomSearchBox;
    
  3. Encapsulating custom Hits

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    import React from 'react';
    import { useHits, Highlight } from 'react-instantsearch-hooks-web';
    
    const CustomHits: React.FC = () => {
    const { hits } = useHits();
        return (
        <div>
        {hits.map((item) => (
            <div key={item.__queryID}>
            <Highlight attribute="title" hit={item} />
            </div>
        ))}
        </div>
    );
    };
    
    export default CustomHits;
    
  4. Mounting custom components

    We need to wrap all custom components with InstantSearch. The indexName in the example below is the name used when uploading the document index, and searchClient is the search instance we just wrapped.

    1
    2
    3
    4
    5
    6
    7
    
    import { InstantSearch } from 'react-instantsearch-hooks-web';
    
    ...
    <InstantSearch indexName="{INDEX_NAME}" searchClient={searchClient}>
    <CustomSearchBox />
    <CustomHits />
    </InstantSearch>
    

Automating updates to the MEILISEARCH document index

If your Blog or document needs to be updated regularly, it is necessary to automate it, the main idea is as follows.

  1. write a script that regenerates a document index file when a document or Blog is updated.
  2. request the MeiliSearch API to update the data source.

Note that because the MeiliSearch Admin key cannot be exposed, we need to set an environment variable to store it.

Here’s an example of GitHub Action.

Node scripts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import axiox, { AxiosError, AxiosResponse } from 'axios';

const ENDPOINT = process.env.NEXT_PUBLIC_SEARCH_ENDPOINT;
const ADMIN_KEY = process.env.NEXT_PUBLIC_SEARCH_ADMIN_KEY;

axiox.defaults.timeout = 5000;

(async () => {
  if (!ENDPOINT || !ADMIN_KEY) {
    console.log('Missing env variables');
    return process.exit(1);
  }

  // posts
  const posts = []

  writeFile('posts.json', JSON.stringify(posts, null, 2)).then(() => {
    console.log('posts length: ', posts.length);
    // Update document index
    axiox
      .post(`${ENDPOINT}/indexes/{YOUR_INDEX_NAME}/documents?primaryKey=id`, posts, {
        headers: {
          Authorization: `Bearer ${ADMIN_KEY}`,
          'Content-Type': 'application/json',
        },
      })
      .then((res: AxiosResponse) => {
        const { status } = res;
        if (status === 202) {
          console.log('update success');
        }
      })
      .catch((err: AxiosError) => {
        console.log('update failed, the error info: ', err);
      });
  });
})();

GitHub Action:

1
2
3
4
5
6
7
8
...
      - name: Update Meilisearch Data
        env:
          SEARCH_ENDPOINT: ${{ secrets.SEARCH_ENDPOINT }}
          SEARCH_ADMIN_KEY: ${{ secrets.SEARCH_ADMIN_KEY }}
        run: |
                        NEXT_PUBLIC_SEARCH_ENDPOINT=$SEARCH_ENDPOINT NEXT_PUBLIC_SEARCH_ADMIN_KEY=$SEARCH_ADMIN_KEY node auto-update.js
...

Summary

The search function is still common in normal front-end development, and Meilisearch provides a solution where we only need to care about the UI style and not the request call of the search service itself, which is a very good experience. Even developing a global search is not a problem anymore.