Add Meilisearch Search to a Django App in Production
Run Meilisearch for Django search in production: deploy it with a master key and persistent storage, index models from signals or Celery, use scoped API keys for frontend search, configure filterable and sortable attributes, and rebuild indexes safely.
Problem statement
PostgreSQL full-text search covers a lot, but you want typo tolerance, instant results as users type, and faceted filters without tuning tsvector weights. Meilisearch is a search engine built for exactly that, with a simple HTTP API. Adding it to Django means running it reliably, keeping its index in sync with your models, protecting the master key, and letting the frontend query it directly without exposing write access.
- Run Meilisearch with
MEILI_ENV=production, a strongMEILI_MASTER_KEY, and a persistent data volume. - Use the
meilisearchPython client from Django with an admin API key (not the master key). - Index documents from Celery tasks triggered by
post_saveandpost_deletewithtransaction.on_commit. - Configure
searchableAttributes,filterableAttributes, andsortableAttributesper index. - Give the frontend a search-only key (or tenant tokens) and keep admin keys server-side.
Step-by-step solution
1. Run Meilisearch
services:
meilisearch:
image: getmeili/meilisearch:v1.12
environment:
MEILI_ENV: production
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
MEILI_NO_ANALYTICS: "true"
volumes:
- meili_data:/meili_data
restart: unless-stopped
volumes:
meili_data: {}
Pin a version; upgrades between versions may require a dump and import. In production mode, Meilisearch refuses to start without a master key of sufficient length. Put it behind your private network or a reverse proxy with TLS; do not expose it with the master key in use. Managed Meilisearch Cloud is an alternative.
2. Create keys and connect Django
Use the master key once to create scoped keys:
curl -X POST "$MEILI_URL/keys" -H "Authorization: Bearer $MEILI_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"description":"django backend","actions":["documents.*","indexes.*","settings.*","tasks.get"],"indexes":["products"],"expiresAt":null}'
import meilisearch
MEILI = meilisearch.Client(os.environ["MEILI_URL"], os.environ["MEILI_ADMIN_KEY"])
3. Keep the index in sync
Index after commit, in the background:
# search/tasks.py
@shared_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=5)
def index_product(product_id):
try:
p = Product.objects.get(pk=product_id, is_active=True)
except Product.DoesNotExist:
MEILI.index("products").delete_document(product_id)
return
MEILI.index("products").add_documents([{
"id": p.pk, "name": p.name, "description": p.description,
"category": p.category.slug, "price": float(p.price), "in_stock": p.stock > 0,
}])
@receiver([post_save, post_delete], sender=Product)
def product_changed(sender, instance, **kwargs):
transaction.on_commit(lambda: index_product.delay(instance.pk))
Meilisearch processes writes asynchronously as tasks; add_documents returns a task you can check, which is useful in rebuild scripts.
4. Configure index settings
MEILI.index("products").update_settings({
"searchableAttributes": ["name", "description", "category"],
"filterableAttributes": ["category", "in_stock"],
"sortableAttributes": ["price"],
"rankingRules": ["words", "typo", "proximity", "attribute", "sort", "exactness"],
})
Keep settings in code (a management command) so every environment matches. Changing settings triggers reindexing inside Meilisearch, which can take time on large indexes.
5. Rebuild without downtime
Build a new index and swap it in:
MEILI.create_index("products_new", {"primaryKey": "id"})
# apply settings, add documents in batches of a few thousand
MEILI.swap_indexes([{"indexes": ["products", "products_new"]}])
MEILI.delete_index("products_new")
Searches keep hitting the old index until the atomic swap.
Explanation
Meilisearch keeps its own copy of the data, optimized for prefix and typo-tolerant search, stored on disk with memory-mapped files. Django remains the source of truth; the search index is derived data you can always rebuild. Indexing through Celery after commit avoids slowing down requests and avoids indexing data from rolled-back transactions. Scoped keys let browsers query Meilisearch directly, which gives instant search without routing every keystroke through Django.
Edge cases / notes
- Memory and disk. Index size can be several times the raw data; give the instance enough RAM for the hot part of the index.
- Permissions. For data that differs per user, use tenant tokens with embedded filters rather than a single public search key.
- Backups. Snapshots or dumps can be scheduled, but rebuilding from Django is the simplest recovery.
Next steps
- Compare with the built-in option in Django PostgreSQL Full-Text Search in Production.
- Another engine: Use Typesense With Django.
- For larger search needs, see Run OpenSearch or Elasticsearch for Django.
FAQ
Should the frontend use the master key?
Never. Create a search-only key or tenant tokens for browsers.
How do I keep Meilisearch in sync with the database?
Index from Celery tasks triggered after commit, and run periodic full reindexes to catch drift.
Can I rebuild the index without downtime?
Yes. Build a new index and use swap_indexes to replace the old one atomically.
Do I still need PostgreSQL full-text search?
Not for user-facing search if Meilisearch covers it, but simple admin searches can stay in the ORM.