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.
To follow this tutorial, you will need:
An Ubuntu 22.04 server set up by following the Ubuntu 22.04 initial server setup guide, including a sudo non-root user and a firewall.
Meilisearch running on your server, configured with key-based authentication. If you need to set up Meilisearch, then follow the previous tutorial in this series, How To Deploy and Configure Meilisearch On Ubuntu 22.04 with Docker.
Docker and Docker-compose installed following How To Install and Use Docker on Ubuntu 22.04 and the first step of How To Install and Use Docker-Compose on Ubuntu 22.04. This was also covered in the previous tutorial in this series.
Node.js installed along with its npm
package manager. You can install Node.js and npm
by following our How To Install Node.js on Ubuntu 22.04.
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 DigitalOcean, please refer to our Introduction to DigitalOcean DNS.
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:
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:
- docker compose down
- docker compose up --detach
Verify that it restarted successfully by using docker-compose ps
:
- docker compose ps
OutputNAME 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:
- 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 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.
- 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:
- 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.
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.
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:
- 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
Outputcertbot 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 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:
- curl \
- -X POST 'https://your_domain/indexes/movies/search' \
- -H 'Content-Type: application/json' \
- -H 'Authorization: Bearer secret_key' \
- --data-binary '{ "q": "saint" }'
Now that your HTTPS configuration is in place, you’re ready to start building a frontend search app.
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 ~/my-instant-meili
- cd ~/my-instant-meili
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
.
OutputThis 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:
- 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:
{
"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:
- nano 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
:
- nano index.css
Add the following contents to the file:
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
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:
- 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:
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.
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
…
<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.
- nano app.js
Note that the container:
blocks match the <div/>
blocks in the HTML.
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:
- 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.
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
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:
- 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.
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.
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