Introduction
If you’re reading this, chances are you’re already familiar with Elasticsearch and are now curious about Meilisearch. So, no need to start from the basics, let’s dive right into why Meilisearch might be the better choice!
Simplicity: Meilisearch has 90% fewer configuration options compared to Elasticsearch, meaning less time spent learning and setting up. For example, while Elasticsearch often requires 5–10 configurations just to index and search efficiently, Meilisearch works well out of the box with minimal tweaks. 😎
Performance: Meilisearch is designed for instant search and is optimized for low-latency results, typically under 50ms, which makes it feel snappier for end users. Elasticsearch, while powerful, often requires extra optimization to reach similar response times for smaller datasets.
Typo Tolerance: Meilisearch provides built-in typo tolerance without extra configuration, improving the user experience significantly.
If your use case doesn’t require massive scalability or advanced querying, Meilisearch is the simpler, faster, and more cost-effective option!
and if you are a Rustacean 🦀, yeah it is written in Rust language.
What’s the Plan?
We’re going to create a Nest application to seed Meilisearch, set up Meilisearch itself, and configure NGINX as reverse proxy in front of it.
NestJS Application
- The repository with the complete code is linked at the end of this article.
Start by preparing your NestJS application.
Install the Meilisearch JavaScript Client:
npm install meilisearch
Now we want to create an endpoint to generate apiKey for front-end application, so we need a controller like this
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('/')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('api-key')
async createApiKey() {
return this.appService.createApiKey();
}
}
then we need to add the functionality to our module service:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { MeiliSearch } from 'meilisearch';
@Injectable()
export class AppService implements OnModuleInit {
private meiliClient: MeiliSearch;
private index = 'movies';
constructor() {
// Create new Meilisearch Instance
this.meiliClient = new MeiliSearch({
host: 'http://meilisearch:7700',
apiKey: 'masterKey',
});
// Check MeiliSearch Health
this.meiliClient.health().then(console.log);
}
// Seed Meilisearch
async onModuleInit() {
await this.seed();
}
// Generate API key for front-end search
async createApiKey() {
const key = await this.meiliClient.createKey({
expiresAt: null,
indexes: [this.index],
name: 'Search API Key',
actions: ['search', 'documents.get'],
description: 'Use it to search from the frontend code',
});
return key;
}
// Seeding Functionality
private async seed() {
const index = this.meiliClient.index(this.index);
const documents = [
{ id: 1, title: 'Nick Mousavi', genres: ['Romance', 'Drama'] },
{ id: 2, title: 'Wonder Woman', genres: ['Action', 'Adventure'] },
{ id: 3, title: 'Life of Pi', genres: ['Adventure', 'Drama'] },
{ id: 4, title: 'Mad Max: Fury Road', genres: ['Adventure'] },
{ id: 5, title: 'Moana', genres: ['Fantasy', 'Action'] },
{ id: 6, title: 'Philadelphia', genres: ['Drama'] },
];
await index.addDocuments(documents);
}
}
MeiliSearch Option Object:
-
apiKey: 'masterKey'
is the api key that we will configure in meilisearch service indocker-compose.yml
file. -
host: 'http://meilisearch:7700'
is the Meilisearch address in our docker network.
createKey method options:
-
expiresAt
: When the key becomes invalid (null
means never expires) -
indexes
: Which Meilisearch indexes this key can access -
name
: Key identifier for your reference -
actions
: Permissions (search: can search, documents.get: can fetch individual documents) -
description
: Note about key's purpose
Now your NestJS application is ready to connect to MeiliSearch server and generate API keys.
Dockerizing
Here, we'll create a Dockerfile
to containerize our NestJS application for production. At the root of your NestJS project, create a file named Dockerfile
:
FROM node:22.12.0 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . ./
RUN npm run build && npm prune --production
FROM gcr.io/distroless/nodejs22-debian12 AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/package.json /app/package.json
Perfect, now our NestJS production build is ready to add to our docker-compose.yml
file.
NGINX Configuration
We'll use Nginx as a reverse proxy to route requests to our API and Meilisearch services in the Docker Compose setup. Create an nginx.conf
file at the root of the project and add the following content:
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
upstream api {
server api:3000;
}
upstream meilisearch {
server meilisearch:7700;
}
server {
listen 80;
# API endpoints
location /api/ {
proxy_pass http://api/;
proxy_set_header Host $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;
}
# Meilisearch endpoints
location /search/ {
proxy_pass http://meilisearch/;
proxy_set_header Host $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;
}
}
}
Now your NGINX webserver is ready to route requests to api
and meilisearch
services.
Docker Compose
We’ll use Docker Compose to manage all the services (API, NGINX, and Meilisearch) together.
Start by creating a docker-compose.yaml
file at the root of the project and add the following content. Don’t worry, I’ll explain each service step by step!
services:
# Nestjs API Service
api:
build:
context: .
target: production
dockerfile: ./Dockerfile
restart: unless-stopped
networks:
- biomousavi
depends_on:
- meilisearch
ports:
- 3000:3000
command: 'dist/main.js'
# Meilisearch Service
meilisearch:
image: getmeili/meilisearch:v1.12
volumes:
- meilisearch-volume:/meili_data
ports:
- '7700:7700'
environment:
- MEILI_MASTER_KEY=masterKey
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:7700']
interval: 10s
timeout: 5s
retries: 5
networks:
- biomousavi
# Nginx Service
nginx:
image: nginx:alpine
ports:
- '80:80'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- biomousavi
depends_on:
- api
- meilisearch
networks:
biomousavi:
driver: bridge
volumes:
meilisearch-volume:
This Docker Compose file defines three services:
API Service: Runs the NestJS application in production mode, exposing it on port 3000. It depends on the Meilisearch service to start first.
Meilisearch Service: Runs Meilisearch for search functionality, storing its data in a persistent volume and secured with a master key. It listens on port 7700.
Nginx Service: Acts as a reverse proxy, routing requests to the API and Meilisearch. It uses a custom Nginx configuration and exposes port 80 for external access.
Your file structure should be something similar to this:
Run The Application
To build and run the application, use one of the following commands:
docker compose up --build
#OR run in detached mode
docker compose up --build -d
Generate a New API Key
In your client application, send a GET
request to obtain a new API key for search requests:
curl 'http://localhost/api/api-key'
This request calls the NestJS API and returns a JSON response like this:
{
"name": "Search API Key",
"description": "Use it to search from the frontend code",
"key": "a5e62c45497396bb1b0535b4cc4b84dec37713c5bdb4b78f18624af6f33ebac7",
"uid": "82f8a1a4-77cd-481d-9fcb-21fdde13246f",
"actions": ["search", "documents.get"],
"indexes": ["movies"],
"expiresAt": null,
"createdAt": "2024-12-31T18:06:45.190728957Z",
"updatedAt": "2024-12-31T18:06:45.190728957Z"
}
Perform a Search Request
Use the obtained API key (key
) to search your index. Add it as a bearer token in the request:
curl 'http://localhost/search/indexes/movies/search?q=mad+max' -H 'Authorization: Bearer your_api_key'
- Replace
your_api_key
with thekey
from the previous response.
It sends request to Meilisearch service.
The response should be similar to the following JSON:
{
"hits": [
{
"id": 4,
"title": "Mad Max: Fury Road",
"genres": [
"Adventure"
]
}
],
"query": "mad max",
"processingTimeMs": 8,
"limit": 20,
"offset": 0,
"estimatedTotalHits": 1
}
Conclusion
By combining Docker, Meilisearch, NGINX, and NestJS, you can set up a scalable, production-ready search solution with minimal configuration. This guide should help you streamline the process of creating, deploying your search engine. Whether you're optimizing for performance or simplicity, this stack provides a solid foundation for your next project.
If you found this guide helpful ❤️ and saved you time, I'd be incredibly grateful if you could show some support by giving the repository a ⭐ star on GitHub!
Top comments (0)