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.

Django + MeilisearchModel savedsignal or taskCelery taskupsert documentMeilisearchindex, settingsFrontendscoped search keyDjango pushes documents; browsers query Meilisearch with a search-only key.
  1. Run Meilisearch with MEILI_ENV=production, a strong MEILI_MASTER_KEY, and a persistent data volume.
  2. Use the meilisearch Python client from Django with an admin API key (not the master key).
  3. Index documents from Celery tasks triggered by post_save and post_delete with transaction.on_commit.
  4. Configure searchableAttributes, filterableAttributes, and sortableAttributes per index.
  5. 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"])
who holds which keyHolderCan doMaster key!ops onlycreate keysAdmin keyDjango, Celeryindex, settingsSearch keybrowsersearch onlyTenant tokenbrowser, per userfiltered searchOnly search-scoped credentials ever reach the frontenddjango-deployment.com
Rotate the admin key like any other secret.

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.

from save to searchable1Model saved in transactionpost_save signal2on_commit enqueues taskonly if committed3Celery builds documentfresh from database4Meilisearch indexes itasync task5Nightly full checkfixes any driftBackground indexing keeps requests fast and survives search outagesdjango-deployment.com
Bulk updates with queryset.update() skip signals; reindex those explicitly.

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

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.

Not for user-facing search if Meilisearch covers it, but simple admin searches can stay in the ORM.

Keep going