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.
- Run Typesense with
--api-key,--data-diron a persistent volume, and enough RAM for the index. - Define a collection schema with typed fields, facets, and a default sorting field.
- Upsert documents from Celery tasks after commit; delete on model deletion.
- Generate scoped search keys for the frontend, with embedded filters for per-user data.
- 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.
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.
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
- Compare with Add Meilisearch Search to a Django App.
- Consider the built-in route in Django PostgreSQL Full-Text Search in Production.
- For analytics-style search, see Run OpenSearch or Elasticsearch for Django.
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.