Use Typesense With Django for Fast Search

Use Typesense with Django in production: run a Typesense node or cluster with an API key and persistent data, define collection schemas, sync models from Celery, use scoped search keys in the frontend, and rebuild collections with aliases for zero downtime.

Problem statement

You want typo-tolerant, instant search with facets for a Django catalog or content site, and you prefer a strict schema and built-in clustering. Typesense is an open-source search engine with an in-memory index, typed collection schemas, and a Raft-based high-availability mode. Integrating it with Django means running it with persistence and an API key, defining schemas that match your models, keeping documents in sync, and letting the frontend search safely.

Django + TypesenseCollection schematyped fieldsCelery syncupsert documentsTypesensein-memory indexFrontendscoped search keyTypesense needs a schema per collection; Django keeps documents in sync.
  1. Run Typesense with --api-key, --data-dir on a persistent volume, and enough RAM for the index.
  2. Define a collection schema with typed fields, facets, and a default sorting field.
  3. Upsert documents from Celery tasks after commit; delete on model deletion.
  4. Generate scoped search keys for the frontend, with embedded filters for per-user data.
  5. Rebuild by creating a new collection and pointing an alias at it.

Step-by-step solution

1. Run Typesense

services:
  typesense:
    image: typesense/typesense:28.0
    command: --data-dir /data --api-key=${TYPESENSE_API_KEY} --enable-cors
    volumes: [typesense_data:/data]
    restart: unless-stopped
volumes:
  typesense_data: {}

Pin a release. Typesense keeps the index in RAM and persists to disk for restarts, so memory must hold the indexed fields. For high availability, run three nodes with a --nodes file so they form a Raft cluster; clients list all nodes. Typesense Cloud is the managed option.

2. Define the schema

import typesense

ts = typesense.Client({
    "nodes": [{"host": "typesense.internal", "port": "8108", "protocol": "http"}],
    "api_key": os.environ["TYPESENSE_ADMIN_KEY"],
    "connection_timeout_seconds": 2,
})

schema = {
    "name": "products_v3",
    "fields": [
        {"name": "name", "type": "string"},
        {"name": "description", "type": "string", "optional": True},
        {"name": "category", "type": "string", "facet": True},
        {"name": "price", "type": "float"},
        {"name": "in_stock", "type": "bool", "facet": True},
        {"name": "popularity", "type": "int32"},
    ],
    "default_sorting_field": "popularity",
}
ts.collections.create(schema)
ts.aliases.upsert("products", {"collection_name": "products_v3"})

Typed schemas reject documents with wrong types, which catches serialization bugs early. Querying through the alias products lets you swap collections later.

design differencesTypesenseMeilisearchSchematyped, requiredschemalessStorageRAM, disk-backeddisk, memory-mappedHigh availabilityRaft clustersingle node (OSS)Zero-downtime rebuildaliasesindex swapBoth give instant, typo-tolerant search; they differ in schema and HAdjango-deployment.com
Check current releases; both projects add features quickly.

3. Sync documents

@shared_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=5)
def sync_product(product_id):
    try:
        p = Product.objects.select_related("category").get(pk=product_id, is_active=True)
    except Product.DoesNotExist:
        ts.collections["products"].documents[str(product_id)].delete()
        return
    ts.collections["products"].documents.upsert({
        "id": str(p.pk), "name": p.name, "description": p.description or "",
        "category": p.category.slug, "price": float(p.price),
        "in_stock": p.stock > 0, "popularity": p.sales_count,
    })

Trigger it with transaction.on_commit from post_save and post_delete signals. Document IDs must be strings. Deleting a missing document raises ObjectNotFound; catch it.

4. Scoped search keys

Create a search-only key with the admin key, then derive scoped keys per user that embed filters:

search_key = ts.keys.create({"description": "search", "actions": ["documents:search"], "collections": ["products"]})["value"]
scoped = ts.keys.generate_scoped_search_key(search_key, {"filter_by": "in_stock:true"})

Scoped keys are generated locally without a network call and cannot be widened by the client. Pass them to the frontend, which queries Typesense directly.

5. Rebuild with aliases

To change the schema or reindex everything, create products_v4, import documents in batches (documents.import_ with action="upsert"), then point the alias at it:

ts.aliases.upsert("products", {"collection_name": "products_v4"})
ts.collections["products_v3"].delete()

Clients querying products switch to the new collection immediately.

alias-based reindex1Create products_v4new schema2Bulk import documentsbatches of 1000s3Compare countsagainst database4Point alias to v4instant switch5Delete products_v3free memorySearches never see a half-built collectiondjango-deployment.com
Keep syncing changes to both collections during long rebuilds.

Explanation

Typesense holds the searchable index in memory for speed and writes to disk so it can recover after restarts. A typed schema per collection lets it build efficient structures for filtering, faceting, and sorting. Django stays the source of truth: documents are derived from models and pushed after commit. Scoped keys let browsers query Typesense directly with restrictions encoded in the key, so instant search does not route every keystroke through Django.

Edge cases / notes

  • Memory sizing. Index only fields you search, filter, or display; large text fields use RAM.
  • Bulk updates. queryset.update() does not send signals; schedule a reindex after bulk changes.
  • Ports and TLS. Typesense listens on 8108; put it behind TLS if clients connect over the internet.

Next steps

FAQ

Does Typesense need a schema?

Yes. Each collection has typed fields, which catches type errors at indexing time.

Can browsers query Typesense directly?

Yes, with search-only or scoped keys. Never expose the admin key.

How do I avoid downtime when reindexing?

Build a new collection and switch an alias to it.

Is Typesense highly available?

With a three-node Raft cluster, yes. A single node is simpler but has no failover.

Keep going