
Meilisearch is an open-source search engine designed for developers. It returns results in milliseconds, handles typos gracefully, and supports faceted filtering — all on a self-hosted VPS. This guide covers a complete installation on Ubuntu, systemd service setup, Nginx reverse proxy, and integration with PHP and Node.js.
Why Meilisearch over SQL LIKE
- Speed — Index-based lookup responds in <50ms even with millions of records
- Typo-tolerance — "wirless" still finds "wireless"
- Relevance ranking — Results sorted by relevance automatically
- Faceted search — Filter by category, price, brand without complex SQL
- Highlight — Mark matched terms in results out of the box
System Requirements: Meilisearch loads its index entirely into RAM. Minimum 1 GB RAM is required; 2 GB or more is recommended for production. A VPS with 2+ GB RAM is the ideal choice.
Install Meilisearch on Ubuntu
Step 1: Download and Install
# Official installer
curl -L https://install.meilisearch.com | sh
# Move binary to PATH
sudo mv ./meilisearch /usr/local/bin/
# Verify
meilisearch --version
Step 2: Create a Dedicated User and Data Directory
sudo useradd -r -s /bin/false meilisearch
sudo mkdir -p /var/lib/meilisearch/data
sudo chown -R meilisearch:meilisearch /var/lib/meilisearch
Step 3: Create a systemd Service
sudo nano /etc/systemd/system/meilisearch.service
[Unit]
Description=Meilisearch Search Engine
After=network.target
[Service]
User=meilisearch
Group=meilisearch
WorkingDirectory=/var/lib/meilisearch
ExecStart=/usr/local/bin/meilisearch \
--db-path /var/lib/meilisearch/data \
--http-addr 127.0.0.1:7700 \
--master-key YOUR_MASTER_KEY_HERE \
--env production
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Replace YOUR_MASTER_KEY_HERE with a strong random key (at least 16 characters). Generate one with: openssl rand -hex 32
Step 4: Enable and Start
sudo systemctl daemon-reload
sudo systemctl enable meilisearch
sudo systemctl start meilisearch
sudo systemctl status meilisearch
# Test the API
curl http://127.0.0.1:7700/health
Nginx Reverse Proxy
Expose Meilisearch over HTTPS by adding an Nginx server block:
server {
listen 443 ssl;
server_name search.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/search.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/search.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:7700;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Index Documents with Node.js
npm install meilisearch
const { MeiliSearch } = require('meilisearch');
const client = new MeiliSearch({
host: 'https://search.yourdomain.com',
apiKey: 'YOUR_MASTER_KEY_HERE'
});
const index = client.index('products');
// Add documents
await index.addDocuments([
{ id: 1, name: 'iPhone 15', category: 'smartphone', price: 32900 },
{ id: 2, name: 'Samsung Galaxy S24', category: 'smartphone', price: 28900 },
]);
// Search
const results = await index.search('iphone', {
limit: 10,
attributesToHighlight: ['name']
});
console.log(results.hits);
Integrate with PHP
composer require meilisearch/meilisearch-php
<?php
require 'vendor/autoload.php';
use Meilisearch\Client;
$client = new Client('https://search.yourdomain.com', 'YOUR_MASTER_KEY_HERE');
$index = $client->index('products');
// Index documents
$index->addDocuments([
['id' => 1, 'name' => 'Test Product', 'price' => 999],
]);
// Search
$results = $index->search('product');
foreach ($results->getHits() as $hit) {
echo $hit['name'] . PHP_EOL;
}
API Key Security: For client-side JavaScript, create a search-only API key rather than exposing your Master Key. Use POST /keys with "actions":["search"] to generate a restricted key with limited scope.
Tuning Ranking Rules and Filterable Attributes
Meilisearch ships with sensible defaults, but you can customise ranking and filtering per index to match your specific use case. Before using any attribute in a filter query, you must declare it as filterable — Meilisearch needs to build a dedicated structure for fast filtering.
Declare Filterable and Sortable Attributes
const index = client.index('products');
// Declare attributes available for filtering
await index.updateFilterableAttributes([
'category',
'price',
'brand',
'in_stock'
]);
// Declare attributes available for sorting
await index.updateSortableAttributes([
'price',
'created_at',
'rating'
]);
// Search with filter and sort
const results = await index.search('laptop', {
filter: 'category = "electronics" AND price < 30000 AND in_stock = true',
sort: ['price:asc'],
limit: 20
});
Customise Ranking Rules
// View current ranking rules
await index.getRankingRules();
// Update ranking rules (append a custom rule)
await index.updateRankingRules([
'words',
'typo',
'proximity',
'attribute',
'sort',
'exactness',
'rating:desc' // Custom: boost higher-rated items
]);
Monitoring, Health Checks, and Backups
In production, monitoring and backups protect your search index from accidental data loss and ensure you catch performance issues early.
Health and Stats Endpoints
# Check Meilisearch health
curl -H 'Authorization: Bearer MASTER_KEY' \
http://127.0.0.1:7700/health
# Global stats (all indexes)
curl -H 'Authorization: Bearer MASTER_KEY' \
http://127.0.0.1:7700/stats
# Stats for a specific index
curl -H 'Authorization: Bearer MASTER_KEY' \
http://127.0.0.1:7700/indexes/products/stats
Create a Dump for Backup
# Trigger a dump via the API
curl -X POST 'http://127.0.0.1:7700/dumps' \
-H 'Authorization: Bearer MASTER_KEY'
# Dump files land in:
ls /var/lib/meilisearch/data/dumps/
# Alternatively, rsync the entire data directory
sudo rsync -av /var/lib/meilisearch/data/ /backup/meilisearch/
Automate Daily Backups with Cron
sudo crontab -e
# Back up every day at 02:00
0 2 * * * rsync -a /var/lib/meilisearch/data/ /backup/meilisearch/$(date +\%Y-\%m-\%d)/
| API Endpoint | Method | Purpose |
|---|---|---|
/health |
GET | Service health status |
/stats |
GET | Index stats and disk usage |
/dumps |
POST | Create a full data dump |
/indexes/{uid}/documents |
DELETE | Delete all documents in index |
Typo Tolerance Settings and Stop Words
Typo tolerance is one of Meilisearch's flagship features. You can customise how aggressively it corrects misspellings and define stop words — terms like "the", "and", "in" — that carry no search meaning and should be ignored when matching documents.
Configure Typo Tolerance per Index
await index.updateTypoTolerance({
enabled: true,
minWordSizeForTypos: {
oneTypo: 5, // allow 1 typo in words of 5+ chars
twoTypos: 9 // allow 2 typos in words of 9+ chars
},
disableOnWords: ['iphone', 'samsung'], // brand names must match exactly
disableOnAttributes: ['serial_number', 'sku']
});
Define Stop Words
await index.updateStopWords([
'the', 'a', 'an', 'is', 'in', 'on', 'at', 'for', 'with',
'and', 'or', 'of', 'to', 'from', 'by'
]);
// Inspect the current stop words
await index.getStopWords();
Stop words reduce index size and improve relevance — when Meilisearch ignores filler words, meaningful terms receive greater weight in the ranking formula. This is especially valuable for product catalogs where users search with natural language phrases.
Search-as-you-type and Instant Search on the Frontend
Instant search — showing results as the user types each character — is where Meilisearch shines most. Sub-50ms response times make the UI feel immediate even on a modest VPS. The key is combining a debounced input listener with a search-only API key exposed to the browser.
Vanilla JavaScript Instant Search
<input type="text" id="searchBox" placeholder="Search products...">
<div id="searchResults"></div>
<script>
const searchBox = document.getElementById('searchBox');
const resultsDiv = document.getElementById('searchResults');
let debounceTimer;
searchBox.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const query = this.value.trim();
if (query.length < 2) { resultsDiv.innerHTML = ''; return; }
const res = await fetch(
'https://search.yourdomain.com/indexes/products/search',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer SEARCH_ONLY_API_KEY'
},
body: JSON.stringify({
q: query,
limit: 8,
attributesToHighlight: ['name', 'description'],
highlightPreTag: '<mark>',
highlightPostTag: '</mark>'
})
}
);
const data = await res.json();
resultsDiv.innerHTML = data.hits.map(hit => `
<div class="result-item">
<strong>${hit._formatted?.name || hit.name}</strong>
<span>$${hit.price.toLocaleString()}</span>
</div>
`).join('');
}, 200); // 200ms debounce
});
</script>
The 200 ms debounce prevents a request on every keystroke. The _formatted field in the response contains the highlighted version of each matched attribute ready to render directly in the browser.
Nginx CORS Headers for Instant Search
# In your Meilisearch Nginx server block
add_header 'Access-Control-Allow-Origin' 'https://yourdomain.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
location / {
if ($request_method = 'OPTIONS') { return 204; }
proxy_pass http://127.0.0.1:7700;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Performance tip: If Meilisearch and your web app share the same VPS, route search requests through a PHP backend endpoint rather than calling Meilisearch directly from JavaScript. This hides the API key and eliminates CORS configuration entirely.
Updating and Deleting Documents
Meilisearch's document management API is straightforward. Every write operation (add, update, delete) is asynchronous — the API returns immediately with a task ID, and Meilisearch processes the change in the background. This design keeps the API fast even when indexing large batches.
Update and Delete Operations
// Update one or more documents (partial update supported)
await index.updateDocuments([
{ id: 1, name: 'iPhone 15 Pro', price: 42900 }
]);
// Delete a single document by its primary key
await index.deleteDocument(1);
// Delete multiple documents matching a filter
await index.deleteDocuments({ filter: 'price < 1000' });
// Check the status of an async task
const task = await index.addDocuments([...]);
const status = await client.getTask(task.taskUid);
console.log(status.status); // 'enqueued' | 'processing' | 'succeeded' | 'failed'
Check Task Queue
# List all tasks
curl -H 'Authorization: Bearer MASTER_KEY' \
'http://127.0.0.1:7700/tasks'
# Get a specific task by ID
curl -H 'Authorization: Bearer MASTER_KEY' \
'http://127.0.0.1:7700/tasks/TASK_ID'
For large imports, poll the task endpoint until its status becomes succeeded before running your first search query — documents are not searchable until indexing completes.
Multi-Index Search and Federation
Multi-search lets you query several indexes in a single HTTP request. This is ideal for a global search box that surfaces products, articles, and categories simultaneously — reducing round-trips from three requests to one.
// Search across multiple indexes in one call
const results = await client.multiSearch({
queries: [
{ indexUid: 'products', q: 'iphone', limit: 5 },
{ indexUid: 'articles', q: 'iphone', limit: 3 },
{ indexUid: 'categories', q: 'smartphone', limit: 3 }
]
});
console.log(results.results[0].hits); // products
console.log(results.results[1].hits); // articles
Multi-search is especially useful for instant search dropdowns that group results by type — products on top, then articles, then categories — giving users a Google-like experience with a single self-hosted engine running on your VPS.
Meilisearch vs Elasticsearch vs Typesense
When choosing a self-hosted search engine for your VPS, three options dominate the open-source space. Each has different strengths depending on project size and technical complexity.
| Feature | Meilisearch | Elasticsearch | Typesense |
|---|---|---|---|
| Setup difficulty | Low — single binary | High — needs Java, complex config | Low — similar to Meilisearch |
| Minimum RAM | 1 GB | 2–4 GB | 1 GB |
| Typo tolerance | Excellent (built-in) | Manual configuration | Good (built-in) |
| Best for | E-commerce, blogs, mid-size apps | Log analytics, enterprise | E-commerce, SaaS |
For developers starting a new project or running a mid-size application, Meilisearch offers the best balance of simplicity, speed, and low resource usage. Elasticsearch is the right choice for teams that need powerful log aggregation and analytics at enterprise scale.
Need VPS for Your Search Engine?
Linux VPS starting at 500 THB/month with Full Root Access and SSD — run Meilisearch, Elasticsearch, or any search stack you choose.
View VPS Plans