- MeiliSearch là search engine viết bằng Rust, full-text typo-tolerant, response dưới 50ms cho 1 triệu document trên VPS 4GB RAM.
- Cài qua Docker Compose hoặc binary, mở port 7700, secure bằng MEILI_MASTER_KEY 32 ký tự.
- Tạo tenant token theo user_id để frontend search trực tiếp mà không lộ master key.
- Index batch 10k document mỗi lần, dùng searchableAttributes và filterableAttributes để tối ưu relevancy.
- Cloud VPS TND từ 199k/tháng đủ chạy MeiliSearch + app Node cho startup giai đoạn MVP đến 50k user.
Algolia tính tiền theo request, ElasticSearch ăn RAM khủng khiếp, còn database full-text search thì chậm và relevancy kém. Nếu bạn đang build app startup cần search ngon, dưới 100ms, có typo tolerance, faceted filtering, mà không muốn tốn 99 USD/tháng cho Algolia plan Pro, thì MeiliSearch là lựa chọn tuyệt vời. Viết bằng Rust, single binary 50MB, chạy mượt trên Cloud VPS 2GB RAM, và scale lên 10 triệu document chỉ cần 8GB RAM.
Mình đã chạy MeiliSearch production cho 3 app startup trong 2 năm qua: 1 marketplace giày, 1 platform khoá học, 1 SaaS quản lý tài liệu. Tổng index hơn 4 triệu document, latency p95 ổn định 30-60ms. Bài này mình chia sẻ workflow đầy đủ từ cài đặt, secure, indexing, đến tích hợp frontend, kèm code thật mình copy ra từ production.
1. MeiliSearch là gì và tại sao chọn nó thay Algolia/ElasticSearch
MeiliSearch là search-as-a-service open source viết bằng Rust, do team Pháp phát triển từ 2018. Triết lý của họ: ra kết quả ngay sau khi gõ ký tự đầu tiên, không cần config phức tạp, default settings đã đủ tốt cho 95% use case. So với competitor:
| Tiêu chí | MeiliSearch | ElasticSearch | Algolia |
|---|---|---|---|
| Ngôn ngữ | Rust | Java (JVM) | Proprietary cloud |
| RAM tối thiểu | 512MB | 4GB | N/A (cloud) |
| Setup time | 5 phút | 1-2 giờ | 30 phút (signup) |
| Typo tolerance | Mặc định bật | Cần config fuzzy | Mặc định bật |
| Faceted search | Có | Có (aggregation) | Có |
| Chi phí 1M doc | Free (self-host) | Free (self-host) | ~99 USD/tháng |
| License | MIT | SSPL/Elastic License | Closed |
Điểm yếu của MeiliSearch: không scale horizontal như ElasticSearch (chỉ chạy 1 node, có sharding nhưng còn beta), không có analytics phức tạp như Algolia dashboard. Nhưng cho startup giai đoạn 0 đến 100k user, MeiliSearch là sweet spot tuyệt vời.
2. Chọn cấu hình VPS phù hợp cho MeiliSearch
RAM là yếu tố quyết định. MeiliSearch load index vào memory, cho phép search siêu nhanh, đổi lại nếu index lớn hơn RAM thì performance tụt thê thảm. Công thức ước lượng: 1MB raw JSON sau index sẽ chiếm 2-3MB RAM (tùy số field searchable).
| Quy mô app | Số document | RAM khuyến nghị | Cloud VPS TND |
|---|---|---|---|
| MVP demo | 10k - 50k | 1GB | Cloud VPS 20 (199k) |
| Startup giai đoạn 1 | 100k - 500k | 2GB | Cloud VPS 40 (399k) |
| Startup tăng trưởng | 500k - 2M | 4GB | Cloud VPS 80 (799k) |
| Production lớn | 2M - 10M | 8-16GB | Cloud VPS 160/240 |
Mình thường khuyên chạy MeiliSearch chung VPS với app Node.js cho startup nhỏ, tách riêng VPS khi index vượt 1 triệu document. Disk SSD CEPH của TND đủ nhanh, không cần NVMe local.
3. Cài đặt MeiliSearch trên VPS bằng Docker Compose
Mình chọn Docker Compose vì dễ backup volume, dễ upgrade version, và dễ thêm reverse proxy Caddy hoặc Nginx. Đầu tiên tạo thư mục project và file docker-compose.yml:
mkdir -p /opt/meilisearch/data
cd /opt/meilisearch
openssl rand -hex 32
# Lưu output làm MEILI_MASTER_KEY, ví dụ: a3f2c1e8d9b4...
Tiếp theo tạo file docker-compose.yml với nội dung:
services:
meilisearch:
image: getmeili/meilisearch:v1.10
container_name: meilisearch
restart: unless-stopped
environment:
MEILI_MASTER_KEY: "your_32_char_random_key_here"
MEILI_ENV: production
MEILI_NO_ANALYTICS: "true"
MEILI_HTTP_ADDR: "0.0.0.0:7700"
volumes:
- ./data:/meili_data
ports:
- "127.0.0.1:7700:7700"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
interval: 30s
timeout: 5s
retries: 3
Lưu ý port bind 127.0.0.1 chứ không phải 0.0.0.0, để không expose ra public internet. Frontend sẽ truy cập qua reverse proxy có HTTPS. Chạy:
docker compose up -d
docker logs meilisearch
curl -s http://127.0.0.1:7700/health
Nếu thấy {"status":"available"} là OK. Nếu lỗi permission volume, chmod 700 thư mục data và thử lại.
4. Cấu hình reverse proxy với Caddy để có HTTPS auto
Caddy auto Let's Encrypt, config 3 dòng là xong. Cài Caddy:
dnf install caddy -y # AlmaLinux 9
systemctl enable --now caddy
File /etc/caddy/Caddyfile:
search.your-domain.com {
reverse_proxy 127.0.0.1:7700
header {
Strict-Transport-Security "max-age=31536000;"
X-Content-Type-Options "nosniff"
}
log {
output file /var/log/caddy/meilisearch.log
format json
}
}
Reload caddy: systemctl reload caddy. Trỏ A record search.your-domain.com tới IP VPS, đợi 30 giây Caddy sẽ tự issue cert. Test:
curl -s https://search.your-domain.com/health
5. Tạo index đầu tiên và push document từ Node.js
Cài client SDK:
npm install meilisearch
File seed.js push 10k product vào index:
import { MeiliSearch } from 'meilisearch'
import fs from 'node:fs'
const client = new MeiliSearch({
host: 'https://search.your-domain.com',
apiKey: process.env.MEILI_MASTER_KEY
})
const products = JSON.parse(fs.readFileSync('./products.json', 'utf-8'))
async function seed() {
const index = client.index('products')
await index.updateSettings({
searchableAttributes: ['name', 'description', 'brand', 'tags'],
filterableAttributes: ['category', 'price', 'in_stock'],
sortableAttributes: ['price', 'created_at'],
rankingRules: ['words', 'typo', 'proximity', 'attribute', 'sort', 'exactness'],
typoTolerance: {
enabled: true,
minWordSizeForTypos: { oneTypo: 4, twoTypos: 8 }
}
})
const batchSize = 1000
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize)
const task = await index.addDocuments(batch)
console.log(`Pushed batch ${i / batchSize + 1}, task ${task.taskUid}`)
}
}
seed().catch(console.error)
Mỗi document cần 1 field id unique (primary key). MeiliSearch tự detect nếu document có field tên id, hoặc bạn truyền explicit qua primaryKey option khi tạo index.
6. Tenant token cho phép frontend search trực tiếp
Anti-pattern phổ biến: frontend gọi backend, backend gọi MeiliSearch. Mỗi request thêm 50ms latency và load không cần thiết lên Node server. MeiliSearch giải bài này bằng tenant token: backend gen JWT short-lived (1 giờ), frontend dùng JWT đó gọi thẳng MeiliSearch.
// backend Node.js
import { MeiliSearch } from 'meilisearch'
const client = new MeiliSearch({
host: 'https://search.your-domain.com',
apiKey: process.env.MEILI_MASTER_KEY
})
export async function genSearchToken(userId, role) {
const rules = {
products: {
filter: role === 'admin' ? '' : 'in_stock = true'
}
}
const apiKey = await client.getKey('search-only-key-uid')
const token = await client.generateTenantToken(
apiKey.uid,
rules,
{ expiresAt: new Date(Date.now() + 3600 * 1000), apiKey: apiKey.key }
)
return token
}
Trước đó cần tạo 1 API key chỉ có quyền search:
curl -X POST 'https://search.your-domain.com/keys'
-H "Authorization: Bearer $MEILI_MASTER_KEY"
-H 'Content-Type: application/json'
--data '{"description":"Search only key","actions":["search"],"indexes":["products"],"expiresAt":null}'
Frontend dùng token tạm:
// frontend React + meilisearch instant-search
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch'
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
const { searchClient } = instantMeiliSearch(
'https://search.your-domain.com',
userSearchToken // fetch from backend on login
)
export default function Search() {
return (
)
}
7. Tối ưu relevancy với searchableAttributes và ranking rules
Mặc định MeiliSearch search trên tất cả field, nhưng bạn nên giới hạn rõ ràng. Thứ tự trong searchableAttributes có ý nghĩa: field đầu tiên có trọng số cao nhất.
await index.updateSearchableAttributes([
'title', // weight cao nhất
'subtitle',
'brand',
'description', // weight thấp nhất
])
Ranking rules quyết định thứ tự kết quả khi nhiều document match query. 6 rule mặc định theo thứ tự: words, typo, proximity, attribute, sort, exactness. Bạn có thể thêm custom rule như sales:desc để ưu tiên sản phẩm bán chạy.
await index.updateRankingRules([
'words', 'typo', 'proximity', 'attribute',
'sales:desc', // custom rule
'sort', 'exactness'
])
8. Sync database với MeiliSearch bằng webhook hoặc cron
Có 3 chiến lược sync:
- Realtime hook: trong controller create/update/delete của app, sau khi commit DB thì push vào MeiliSearch queue. Đơn giản nhất, hợp với app dưới 100k document.
- Background job: dùng BullMQ hoặc Sidekiq, mỗi lần DB thay đổi thì enqueue 1 job sync. Tránh block request user.
- Cron full reindex: 1 lần/ngày chạy job export DB ra JSON và push toàn bộ. An toàn nhất, dùng cho app có nhiều admin sửa data thẳng vào DB.
Mình prefer kết hợp: realtime hook + cron đêm full reindex backup. Code mẫu cho realtime hook (Express):
// services/search-sync.js
import { meili } from './meili-client.js'
export async function syncProduct(product) {
try {
await meili.index('products').addDocuments([{
id: product.id,
name: product.name,
brand: product.brand,
price: product.price,
in_stock: product.stock > 0,
tags: product.tags,
created_at: product.created_at.toISOString()
}])
} catch (e) {
console.error('Meili sync failed:', e.message)
// gửi vào dead letter queue retry sau
}
}
9. Backup và restore index MeiliSearch
MeiliSearch có 2 dạng backup: snapshot (binary, restore nhanh) và dump (JSON, portable cho version khác).
# Tạo dump
curl -X POST 'https://search.your-domain.com/dumps'
-H "Authorization: Bearer $MEILI_MASTER_KEY"
# Dump file sẽ ở thư mục data/dumps/ trong container
# Copy ra host và backup lên R2 hoặc storage box
docker cp meilisearch:/meili_data/dumps/ /opt/meilisearch/backups/
Cron job backup hằng ngày 3h sáng, push lên S3-compatible storage. Snapshot của Cloud VPS TND cũng giúp rollback nhanh khi MeiliSearch upgrade fail.
10. Monitor MeiliSearch với Grafana + Prometheus
MeiliSearch expose metrics qua endpoint /metrics theo format Prometheus. Bật bằng env MEILI_EXPERIMENTAL_ENABLE_METRICS=true. Quan trọng theo dõi:
- meilisearch_db_size_bytes: kích thước index trên disk
- meilisearch_used_db_size_bytes: phần index đang dùng
- meilisearch_nb_indexes: số lượng index
- meilisearch_http_requests_total: số request, phân theo status code
- meilisearch_http_response_time_seconds: latency p50/p95/p99
Alert khi p95 > 200ms hoặc db_size tăng đột biến. Mình từng phát hiện 1 bug trong app push duplicate document, index phình từ 800MB lên 5GB trong 2 ngày nhờ alert này.
11. Bài học production: 5 lỗi mình từng gặp
- Quên set MEILI_MASTER_KEY production: MeiliSearch chạy mode no-auth, ai cũng đọc/ghi được. Hậu quả: bị xóa hết index. Phải set ngay từ lần chạy đầu.
- Index quá nhiều field không cần search: searchableAttributes mặc định là tất cả field, làm index phình to. Giới hạn rõ ràng 3-5 field thật sự cần search.
- Push document size > 100MB: MeiliSearch reject, app crash. Batch nhỏ 1000 document một lần là sweet spot.
- Không bật typo tolerance cho ngôn ngữ tiếng Việt: tiếng Việt có dấu, user gõ không dấu thường xuyên. Set typoTolerance minWordSizeForTypos thấp xuống và normalize chữ trước khi index.
- Quên backup trước khi upgrade version: MeiliSearch v1.x to v2.x có breaking change index format. Luôn dump trước upgrade.
12. Tối ưu thêm: faceted search và highlighting
Faceted search cho phép user filter theo category, brand, price range. Setup:
const results = await index.search('iphone', {
filter: ['category = phone', 'price 5000000 TO 30000000'],
facets: ['brand', 'category', 'price'],
attributesToHighlight: ['name', 'description'],
highlightPreTag: '',
highlightPostTag: ''
})
console.log(results.facetDistribution)
// { brand: { Apple: 12, Samsung: 8 }, category: { phone: 20 } }
Highlighting trả về text có thẻ mark bao quanh từ khoá match, frontend render thẳng vào HTML, hiệu ứng Algolia-style chuyên nghiệp.
VPS chạy MeiliSearch + Node API mượt từ 199k/tháng
Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. MeiliSearch + app Node + Postgres chạy ngon trên cấu hình 4GB RAM, đủ cho startup 50k user đầu tiên.
Xem 8 cấu hình Cloud VPS →FAQ
MeiliSearch có chạy được trên VPS 1GB RAM không?
Được, với index dưới 100k document. Cloud VPS 20 của TND (1GB RAM, 199k/tháng) đủ cho MVP và staging. Khi production traffic tăng, upgrade lên Cloud VPS 40 (2GB) hoặc 80 (4GB) tuỳ index size.
So với ElasticSearch thì MeiliSearch yếu hơn ở điểm nào?
MeiliSearch không có aggregation phức tạp như ElasticSearch, không scale horizontal nhiều node, không có log analytics tích hợp. Nếu use case bạn cần ELK stack hay big data analytics, dùng ElasticSearch. Nếu chỉ cần search nhanh cho app, MeiliSearch thắng tuyệt đối về DX và chi phí.
Tenant token có an toàn cho frontend production không?
An toàn. Tenant token là JWT signed bởi API key của backend, có expiresAt và rules giới hạn index/filter. Token chỉ có quyền search read-only, không write được index. Đây là pattern Algolia và MeiliSearch đều khuyến nghị cho instant search frontend.
Làm sao sync MeiliSearch với PostgreSQL realtime?
3 cách: (1) hook trong app code sau commit DB thì push vào Meili, (2) PostgreSQL LISTEN/NOTIFY trigger gọi worker, (3) tool như pg2es hoặc Debezium đọc WAL. Cách 1 đơn giản nhất, cách 3 mạnh nhất nhưng phức tạp. Cho startup mình khuyên dùng cách 1 + cron backup full reindex hằng ngày.
MeiliSearch hỗ trợ tiếng Việt có dấu tốt không?
Tốt, từ v1.6 hỗ trợ tokenization tiếng Việt qua charabia engine. User gõ có dấu hoặc không dấu đều ra kết quả đúng nếu bạn normalize text trước khi index (lowercase, strip dấu tuỳ chọn). Mình recommend giữ nguyên dấu khi index, để MeiliSearch tự xử lý.

