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

meilisearch illustration for: Prerequisites

To follow this tutorial, you will need:

  • You will also want to have registered a domain name before completing the last steps of this tutorial. To learn more about setting up a domain name with the cloud provider, please refer to our Introduction to DNS hosting.

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:

				
					cd ~/meilisearch-docker
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:

				
					[label ~meilisearch-docker/meilisearch.env]
MEILI_MASTER_KEY="secret_key"
&lt;^&gt;MEILI_ENV="production"&lt;^&gt;
				
			

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:

				
					docker compose down
docker compose up --detach
				
			

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

				
					docker compose ps
				
			
				
					[secondary_label Output]
NAME 	COMMAND 	SERVICE 	STATUS 	PORTS
sammy-meilisearch-1 "tini -- /bin/sh -c …" meilisearch 	running 	127.0.0.1:7700-&gt;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:

				
					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:

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

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

				
					curl -X POST 'http://localhost:7700/indexes/movies/settings' -H 'Content-Type: application/json' -H 'Authorization: Bearer &lt;^&gt;secret_key&lt;^&gt;' --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:

				
					curl -X GET 'http://localhost:7700/keys' -H 'Authorization: Bearer &lt;^&gt;secret_key&lt;^&gt;'
				
			
				
					[secondary_label Output]
{
 "results": [
 {
 "description": "Default Search API Key (Use it to search from the frontend)",
 "key": "&lt;^&gt;SwlztWf7e71932abed4ecafa6cb32ec06446c3117bd49f5415f822f4f126a29c528a7313&lt;^&gt;",
 "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:

				
					sudo ufw allow "Nginx Full"
				
			

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

				
					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:

				
					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.

				
					[label /etc/nginx/sites-available/meili]
server {
 listen 80 default_server;
 listen [::]:80 default_server;
 server_name &lt;^&gt;your_domain&lt;^&gt;;
 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 &lt;^&gt;/indexes/movies/search&lt;^&gt; {
 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:

				
					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:

				
					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:

				
					sudo snap install --classic certbot
				
			
				
					[secondary_label 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:

				
					sudo certbot --nginx -d &lt;^&gt;your-domain&lt;^&gt;
				
			

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:

				
					[secondary_label Output]
…
Successfully deployed certificate for &lt;^&gt;your-domain&lt;^&gt; to /etc/nginx/sites-enabled/meili
Congratulations! You have successfully enabled HTTPS on &lt;^&gt;https://your-domain&lt;^&gt;

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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:

				
					[environment local]
curl \
 -X POST 'https://&lt;^&gt;your_domain&lt;^&gt;/indexes/movies/search' \
 -H 'Content-Type: application/json' \
 -H 'Authorization: Bearer &lt;^&gt;secret_key&lt;^&gt;' \
 --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:

				
					mkdir &lt;^&gt;~/my-instant-meili&lt;^&gt;
cd &lt;^&gt;~/my-instant-meili&lt;^&gt;
				
			

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

				
					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.

				
					[secondary_label 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 &lt;pkg&gt;` 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:

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

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

				
					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:

				
					[label package.json]
{
 "name": "my-instant-meili",
 "version": "1.0.0",
 "description": "",
 "main": "index.html",
 "scripts": {
 &lt;^&gt;"start": "parcel index.html --global instantMeiliSearch --public-url /dev",&lt;^&gt;
 &lt;^&gt;"build": "parcel build --global instantMeiliSearch index.html"&lt;^&gt;
 },
 "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:

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

 &lt;title&gt;MeiliSearch + InstantSearch&lt;/title&gt;
 &lt;/head&gt;
 &lt;body&gt;
 &lt;div class="ais-InstantSearch"&gt;
 &lt;h1&gt;MeiliSearch + InstantSearch.js&lt;/h1&gt;
 &lt;h2&gt;Search Movies!&lt;/h2&gt;

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

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:

				
					nano index.css
				
			

Add the following contents to the file:

				
					[label 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:

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

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

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

				
					npm start
				
			

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

				
					[secondary_label Output]
&gt; instant-demo@1.0.0 start /root/instant
&gt; 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:

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:

				
					nano index.html
				
			
				
					[label index.html]
…
 &lt;h2&gt;Search Movies!&lt;/h2&gt;

 &lt;^&gt;&lt;div class="left-panel"&gt;&lt;^&gt;
 &lt;^&gt;&lt;div id="clear-refinements"&gt;&lt;/div&gt;&lt;^&gt;

 &lt;^&gt;&lt;h2&gt;Genres&lt;/h2&gt;&lt;^&gt;
 &lt;^&gt;&lt;div id="genres-list"&gt;&lt;/div&gt;&lt;^&gt;
 &lt;^&gt;&lt;/div&gt;&lt;^&gt;

 &lt;div class="right-panel"&gt;
…
				
			

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

				
					nano app.js
				
			

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

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

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

				
					npm start
				
			

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:

				
					npm run-script build
				
			
				
					[secondary_label Output]
&gt; instant-demo@1.0.0 build /root/instant
&gt; 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:

				
					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.