Tutorial

How To Run a Meilisearch Frontend Using InstantSearch on Ubuntu 22.04

Published on April 26, 2022

Senior DevOps Technical Writer

How To Run a Meilisearch Frontend Using InstantSearch on Ubuntu 22.04

Introduction

Meilisearch is an open source, standalone search engine written in the highly performant Rust programming language. Compared with other popular search engines, Meilisearch is focused on keeping deployment straightforward – it provides features like fuzzy matching and schema-less indexing out of the box, and is managed by a single command-line binary. It includes its own web frontend for demo purposes, and can be integrated with the InstantSearch library for more complex web deployments.

In the previous tutorial in this series, you installed and configured Meilisearch on a Ubuntu 22.04 server. You also experimented with loading data and querying Meilisearch using its included non-production web frontend in development mode. In this tutorial, you’ll configure Meilisearch for a production deployment, using the InstantSearch frontend, and then explore how to embed it within a larger web deployment.

Prerequisites

To follow this tutorial, you will need:

Step 1 — Obtaining Meilisearch API Keys and Enabling Production Mode

In the previous tutorial in this series, you configured Meilisearch to run with docker-compose using environment variables. To finish setting up Meilisearch for production use, you need to add the MEILI_ENV environment variable to your configuration.

Return to your meilisearch-docker directory using the cd command, and then using nano or your favorite text editor, open the meilisearch.env configuration file:

  1. cd ~/meilisearch-docker
  2. nano meilisearch.env

Add a line to the end of the file, containing MEILI_ENV="production". Enabling this setting will disable the built-in search preview interface, as well as optimize some internal logging parameters:

~meilisearch-docker/meilisearch.env
MEILI_MASTER_KEY="secret_key"
MEILI_ENV="production"

Save and close the file by pressing Ctrl+X, then when prompted, Y and then ENTER. Next, restart your Meilisearch container with these new configuration changes:

  1. docker compose down
  2. docker compose up --detach

Verify that it restarted successfully by using docker-compose ps:

  1. docker compose ps
Output
NAME COMMAND SERVICE STATUS PORTS sammy-meilisearch-1 "tini -- /bin/sh -c …" meilisearch running 127.0.0.1:7700->7700/tcp

In the previous tutorial, you tested Meilisearch with a local index. You then created a new docker-compose.yml configuration. To ensure that your Meilisearch instance has some example data loaded, re-run the following curl -X POST commands.

Note: The Meilisearch project provides a sample JSON-formatted data set scraped from TMDB, The Movie Database. If you don’t already have it, download the data from docs.meilisearch.com using the wget command:

  1. wget https://docs.meilisearch.com/movies.json

This time, include your secret_key as a part of the Authorization: Bearer secret_key HTTP header.

The first command loads the movies.json file into Meilisearch:

  1. curl -X POST 'http://localhost:7700/indexes/movies/documents' -H 'Content-Type: application/json' -H 'Authorization: Bearer secret_key' --data-binary @movies.json

The second command updates your Meilisearch configuration to allow filtering by genre and release date, and sorting by release date.

  1. curl -X POST 'http://localhost:7700/indexes/movies/settings' -H 'Content-Type: application/json' -H 'Authorization: Bearer secret_key' --data-binary '{ "filterableAttributes": [ "genres", "release_date" ], "sortableAttributes": [ "release_date" ] }'

Finally, before you continue to the next step, obtain a read-only authentication key with more limited permissions. You will use this key to perform search queries with your frontend interface, so it only needs read-only permissions. Meilisearch automatically creates one read-only key for you when running in production, which you can retrieve from the /keys endpoint using the following curl command:

  1. curl -X GET 'http://localhost:7700/keys' -H 'Authorization: Bearer secret_key'
Output
{ "results": [ { "description": "Default Search API Key (Use it to search from the frontend)", "key": "SwlztWf7e71932abed4ecafa6cb32ec06446c3117bd49f5415f822f4f126a29c528a7313", "actions": [ "search" ], "indexes": [ "*" ], "expiresAt": null, "createdAt": "2022-03-10T22:02:28Z", "updatedAt": "2022-03-10T22:02:28Z" }, { "description": "Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)", "key": "mOTFYUKeea1169e07be6e89de180de4809be5a91be667af364e45a046850bbabeef669a5", "actions": [ "*" ], "indexes": [ "*" ], "expiresAt": null, "createdAt": "2022-03-10T22:02:28Z", "updatedAt": "2022-03-10T22:02:28Z" } ] }

Make a note of the Default Search API Key. You will use it in place of the default_search_api_key placeholder to configure your frontend in the next steps. You can also create or delete Meilisearch API keys by following the authentication documentation.

Now that the Meilisearch index is running in production mode, you can configure access to your Meilisearch server using Nginx.

Step 2 — Installing Nginx and Configuring a Reverse Proxy over HTTPS

Putting a web server such as Nginx in front of Meilisearch can improve performance and make it much more straightforward to secure a site over HTTPS. You’ll install Nginx and configure it to reverse proxy requests to Meilisearch, meaning it will take care of handling requests from your users to Meilisearch and back again.

If you are using a ufw firewall, you should make some changes to your firewall configuration at this point, to enable access to the default HTTP/HTTPS ports, 80 and 443. ufw has a stock configuration called “Nginx Full” which provides access to both of these ports:

  1. sudo ufw allow "Nginx Full"

Now, refresh your package list, then install Nginx using apt:

  1. sudo apt install nginx

Nginx allows you to add per-site configurations to individual files in a subdirectory called sites-available/. Using nano or your favorite text editor, create a new Nginx configuration at /etc/nginx/sites-available/meili:

  1. sudo nano /etc/nginx/sites-available/meili

Paste the following into the new configuration file, being sure to replace your_domain with your domain name.

/etc/nginx/sites-available/meili
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name your_domain;
    root /var/www/html;

    access_log /var/log/nginx/meilisearch.access.log;
    error_log /var/log/nginx/meilisearch.error.log;

    location / {
        try_files $uri $uri/ index.html;
    }

    location /indexes/movies/search {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass http://127.0.0.1:7700;
    }

    location /dev {
       proxy_set_header Connection "";
       proxy_set_header Host $http_host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;
       proxy_set_header X-Frame-Options SAMEORIGIN;
       proxy_http_version 1.1;
       proxy_pass http://127.0.0.1:1234;
    }
}

This is a minimal reverse proxy configuration. It listens for external requests on the default HTTP port, 80.

  • The location / block will serve an index page from Nginx’s default /var/www/html directory.

  • The location /indexes/movies/search block forwards requests to the Meilisearch backend, running on port 7700.

  • The location /dev/ block will be used to forward requests to the development version of your InstantSearch frontend later in this tutorial.

Note: If you ever wanted to add another index to this Meilisearch backend, you would need to add another block to this Nginx configuration, such as location /indexes/books/search {}, containing the same contents, in order to intercept the correct URLs.

Don’t forget to replace your_domain with your domain name, as this will be necessary to add HTTPS support on port 443. Then, save and close the file.

Next, you’ll need to activate this new configuration. Nginx’s convention is to create symbolic links (like shortcuts) from files in sites-available/ to another folder called sites-enabled/ as you decide to enable or disable them. Using full paths for clarity, make that link:

  1. sudo ln -s /etc/nginx/sites-available/meili /etc/nginx/sites-enabled/meili

By default, Nginx includes another configuration file at /etc/nginx/sites-available/default, linked to /etc/nginx/sites-enabled/default, which also serves its default index page. You’ll want to disable that rule by removing it from /sites-enabled, because it conflicts with our new Meilisearch configuration:

  1. sudo rm /etc/nginx/sites-enabled/default

Now you can proceed with enabling HTTPS. To do this, you’ll install certbot from the Let’s Encrypt project. Let’s Encrypt prefers to distribute Certbot via a snap package, so you can use the snap install command, available by default on Ubuntu 22.04:

  1. sudo snap install --classic certbot
Output
certbot 1.25.0 from Certbot Project (certbot-eff✓) installed

Next, run certbot in --nginx mode. Using the -d flag, specify your domain name:

  1. sudo certbot --nginx -d your-domain

You’ll be prompted to agree to the Let’s Encrypt terms of service, and to enter an email address.

Afterwards, you’ll be asked if you want to redirect all HTTP traffic to HTTPS. It’s up to you, but this is generally recommended and safe to do.

After that, Let’s Encrypt will confirm your request and Certbot will download your certificate:

Output
… Successfully deployed certificate for your-domain to /etc/nginx/sites-enabled/meili Congratulations! You have successfully enabled HTTPS on https://your-domain - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - If you like Certbot, please consider supporting our work by: * Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate * Donating to EFF: https://eff.org/donate-le - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Certbot will automatically reload Nginx with the new configuration and certificates.

Once you’ve opened a port in your firewall and set up your reverse proxy and certificates, you should be able to query Meilisearch remotely over HTTPS. You can test that by using cURL with the same syntax as before — an HTTPS URL will default to port 443, which certbot will have automatically added to your Nginx configuration:

  1. curl \
  2. -X POST 'https://your_domain/indexes/movies/search' \
  3. -H 'Content-Type: application/json' \
  4. -H 'Authorization: Bearer secret_key' \
  5. --data-binary '{ "q": "saint" }'

Now that your HTTPS configuration is in place, you’re ready to start building a frontend search app.

Step 3 — Installing instant-mellisearch in a New Node.js Project

In this step, you’ll create a new Node.js project using instant-mellisearch, an InstantSearch frontend for Meilisearch.

Make a new directory for this project using mkdir and change into it using cd:

  1. mkdir ~/my-instant-meili
  2. cd ~/my-instant-meili

Next, use npm init to initialize a Node.js project:

  1. npm init

You’ll be prompted to input some metadata for your new project. You can be as descriptive as possible here, on the assumption that you may eventually publish this project to a repository like Github or to the npm package registry. The one value you should definitely change from the default is the entry point, to index.html.

Output
This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. package name: (instant) my-instant-meili version: (1.0.0) description: entry point: (index.js) index.html test command: git repository: keywords: author: license: (ISC)

This will create a package.json file in your project directory. Before looking more closely at that file, you can use npm to install some dependencies for this project, which will be added to package.json and install into the node_modules subdirectory:

  1. npm i @meilisearch/instant-meilisearch @babel/core parcel-bundler

Now, using nano or your favorite text editor, open package.json:

  1. nano package.json

Your file should look like this, reflecting the changes you made during the npm init process and the dependencies you just installed. The one change you’ll want to make is to the scripts block. Replace the default entries with the start and build command below, which will let you use javascript’s parcel tool to serve your new app:

package.json
{
  "name": "my-instant-meili",
  "version": "1.0.0",
  "description": "",
  "main": "index.html",
  "scripts": {
    "start": "parcel index.html --global instantMeiliSearch --public-url /dev",
    "build": "parcel build --global instantMeiliSearch index.html"
  },
  "dependencies": {
    "@babel/core": "7.14.0",
    "@meilisearch/instant-meilisearch": "0.6.0",
    "parcel-bundler": "1.12.5"
  },
  "devDependencies": {
    "@babel/core": "7.2.0",
    "parcel-bundler": "^1.6.1"
  },
  "keywords": []
}

Save and close the file. If you are using nano, press Ctrl+X, then when prompted, Y and then ENTER.

Next, you’ll provide the first HTML, CSS, and javascript components for this app. You can paste the examples below into new files without making changes, as they provide a usable baseline configuration. You’ll have the opportunity to customize your search interface later.

First, open index.html and then add the following HTML into it:

  1. nano index.html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/algolia-min.css"
    />
    <link rel="stylesheet" href="./index.css" />

    <title>MeiliSearch + InstantSearch</title>
  </head>
  <body>
    <div class="ais-InstantSearch">
      <h1>MeiliSearch + InstantSearch.js</h1>
      <h2>Search Movies!</h2>

      <div class="right-panel">
        <div id="searchbox" class="ais-SearchBox"></div>
        <div id="hits"></div>
        <div id="pagination"></div>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4"></script>
    <script src="./app.js"></script>
  </body>
</html>

The index.html file loads both remote and local assets – as you can see above, you’ll also need to create index.css and app.js. Save and close that file, then create index.css:

  1. nano index.css

Add the following contents to the file:

index.css
body,
h1 {
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  padding: 1em;
}

.ais-ClearRefinements {
  margin: 1em 0;
}

.ais-SearchBox {
  margin: 1em 0;
}

.ais-Pagination {
  margin-top: 1em;
}

.left-panel {
  float: left;
  width: 200px;
}

.right-panel {
  margin-left: 210px;
}

.ais-InstantSearch {
  max-width: 960px;
  overflow: hidden;
  margin: 0 auto;
}

.ais-Hits-item {
  margin-bottom: 1em;
  width: calc(50% - 1rem);
}

.ais-Hits-item img {
  margin-right: 1em;
  width: 100%;
  height: 100%;
  margin-bottom: 0.5em;
}

.hit-name {
  margin-bottom: 0.5em;
}

.hit-description {
  font-size: 90%;
  margin-bottom: 0.5em;
  color: grey;
}

.hit-info {
  font-size: 90%;
}

You can change the CSS parameters as desired, or keep the defaults. Save and close that file, then finally create app.js:

  1. nano app.js
app.js
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch";

const search = instantsearch({
  indexName: 'movies',
  searchClient: instantMeiliSearch(
    "https://your domain",
    "default_search_api_key"
  ),
})

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox"
  }),
  instantsearch.widgets.configure({
    hitsPerPage: 6,
    snippetEllipsisText: "...",
    attributesToSnippet: ["description:50"]
  }),
  instantsearch.widgets.hits({
    container: "#hits",
    templates: {
      item: `
        <div>
          <div class="hit-name">
            {{#helpers.highlight}}{ "attribute": "title" }{{/helpers.highlight}}
          </div>
          <img src="{{poster}}" align="left" />
          <div class="hit-description">
            {{#helpers.snippet}}{ "attribute": "overview" }{{/helpers.snippet}}
          </div>
          <div class="hit-info">Genre: {{genres}}</div>
        </div>
      `
    }
  }),
  instantsearch.widgets.pagination({
    container: "#pagination"
  })
]);

search.start();

This file contains the connection details for your Meilisearch index. Ensure that the indexName, the IP address, and the authentication key all match the values that you tested with curl. Then, save and close the file.

Note: The templates block near the bottom of this file handles how search results are displayed. Make a note of this if you ever add an additional field to your data set, or need to review how to display data other than this set of movies.

You can now test your new Meilisearch frontend with the npm start command that you configured in package.json earlier:

  1. npm start

npm start is a common convention for running Node.js apps. In this case, npm start has been configured to run parcel:

Output
> instant-demo@1.0.0 start /root/instant > parcel index.html --global instantMeiliSearch Server running at http://localhost:1234 ✨ Built in 2.16s.

You should now have a temporary parcel server, serving your frontend on https://your_domain/dev. Navigate to that URL in a browser, and you should be able to access your Meilisearch interface. Experiment by running a few queries:

Stock meilisearch interface

The parcel server will block your shell while it is running – you can press Ctrl+C to stop the process. You now have a working Meilisearch frontend that can be deployed to production. Before finalizing your deployment, in the next step you’ll add a few more optional features to your Meilisearch interface.

Step 4 — Customizing Your Meilisearch Interface

In this step, you’ll add a faceting interface to your Meilisearch frontend, and review some optional widgets.

Adding additional widgets to an InstantSearch interface has two steps: adding <div> containers to your HTML page, and tying those containers to features declared in the search.addWidgets() block of app.js. Because you already enabled faceting by genre in your Meilisearch index, you can add a faceting interface by using the refinementList and clearRefinements InstantSearch widgets.

First, open index.html and add the <div class="left-panel"/> block into the middle of the file as shown:

  1. nano index.html
index.html
…
      <h2>Search Movies!</h2>

      <div class="left-panel">
        <div id="clear-refinements"></div>

        <h2>Genres</h2>
        <div id="genres-list"></div>
      </div>

      <div class="right-panel">
…

Save and close the file, then open app.js and add the corresponding contents.

  1. nano app.js

Note that the container: blocks match the <div/> blocks in the HTML.

app.js
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch";
…
    container: "#searchbox"
  }),
  instantsearch.widgets.clearRefinements({
    container: "#clear-refinements"
  }),
  instantsearch.widgets.refinementList({
    container: "#genres-list",
    attribute: "genres"
  }),
  instantsearch.widgets.configure({
    hitsPerPage: 6,
…

After making those changes, restart your parcel server with npm start to see them reflected in the browser:

  1. npm start

Meilisearch interface with added genre filter

Many other InstantSearch widgets are also compatible with Meilisearch, and you can find their implementation details in the project documentation.

In the final step, you’ll redeploy your frontend to a permanent URL.

Step 5 — Deploying Your instant-mellisearch App in Production

When you edited package.json above, you provided a build command in addition to the start command. You can use npm run-script build now (only start gets the shortened npm start syntax), to package your app for production:

  1. npm run-script build
Output
> instant-demo@1.0.0 build /root/instant > parcel build --global instantMeiliSearch index.html ✨ Built in 7.87s. dist/app.426c3941.js.map 211.84 KB 43ms dist/app.426c3941.js 48.24 KB 5.28s dist/instant.0f565085.css.map 1.32 KB 4ms dist/index.html 872 B 2.41s dist/instant.0f565085.css 689 B 1.41s

This will generate a set of files that you can serve from a static web directory without needing to use parcel to run a temporary server. Recall that Nginx is still serving its default homepage on the default HTTP/HTTPS ports. You can copy the contents of the dist directory that you just generated into the directory that Nginx serves its default homepage from, /var/www/html. Nginx will automatically serve the index.html file from your Meilisearch frontend:

  1. sudo cp dist/* /var/www/html/.

You should now be able to navigate to https://your_domain in a browser to access your Meilisearch frontend. Because instant-meilisearch is compiled into a static web app which only needs to connect to a running meilisearch instance, you can deploy the frontend anywhere you want, including on another server, or on another static hosting provider. In the meantime, you can consider serving it with this Nginx configuration, or scale this deployment in any other way.

Conclusion

In this tutorial, you created and deployed a Meilisearch frontend for your existing Meilisearch server. You worked with reverse proxies and Node.js tooling, and you reviewed some additional details around Meilisearch authentication. You can now further customize your Meilisearch backend and frontend to create other interfaces for querying different types of data.

Next, you may want to experiment with web scraping, to identify other data sources which can be loaded into Meilisearch.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar

Senior DevOps Technical Writer

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
1 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Great guide, but I run into an error that I am not managing to solve: Uncaught SyntaxError: Unexpected token '<' (at style.e308ff8e.js:1:1) This happens on the line <!Doctype html>, and as a consequence, the css and custom javascript doesn’t work on the instantsearch interface… Could anyone help me out? thanks

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more