Demo app¶
The repository ships a small catalog in example/.
Plain HTML, no extra frontend. Use it to try every public feature and to
manually test new APIs — live pages print the call, raw() / explain(),
and the result.
Run it¶
You need uv, Docker, and a clone of this repo.
$ git clone git@github.com:saadmk11/redis-search-django.git
$ cd redis-search-django
$ uv sync --group example
$ docker compose up -d
$ uv run python example/manage.py migrate
$ uv run python example/manage.py loaddata catalog
$ uv run python example/manage.py redisearch rebuild
$ uv run python example/manage.py runserver
Open http://127.0.0.1:8000/.
What each command does
| Step | Why |
|---|---|
uv sync --group example |
Installs Django, the package, and Pillow |
docker compose up -d |
Starts Redis Stack on 6379 (Insight on 8001) |
migrate |
Creates the SQLite schema |
loaddata catalog |
Loads dummy rows into SQLite only |
redisearch rebuild |
Creates both Redis indexes and writes every available product |
runserver |
Serves the demo at port 8000 |
loaddata never writes Redis. Always follow it with redisearch rebuild (or
populate if the indexes already exist).
After you change example/core/documents.py (new fields or a new Document),
run redisearch rebuild again. Indexes that embed Object(..., required=False)
must be rebuilt so {alias}_pk is created with INDEXMISSING (needed for
category__isnull).
Create a superuser if you prefer Django admin:
Dummy data¶
The catalog fixture is:
| Model | Count | Notes |
|---|---|---|
| Categories | 5 | Electronics, Clothing, Books, Home, Sports |
| Tags | 9 | HASH + JSON copies |
| Vendors | 42 | One per product (O2O) |
| Products | 42 | 38 available, 4 hidden, 2 with category=null |
Hidden products (available=False) stay in SQLite and are omitted from
Redis (get_queryset / should_index). The two products with no category
are there so you can try __isnull.
There is no embedding column on Product. On save, prepare_embedding
joins name + description and a local embed() function turns that text into
32 floats. Redis stores the vector. knn() searches it. No API key and no
local model.
Other prepare_* hooks in the demo:
| Hook | Stored value |
|---|---|
prepare_name |
Title UPPERCASE |
prepare_sku |
SKU-0001 style tag |
prepare_department |
Category slug, or omitted when there is no category |
prepare_location |
lon,lat Geo point from the category (or omitted) |
TagHashDocument is a second index of Tag with Index.storage = "hash" so
HASH vs JSON can be compared without changing the product graph.
What each page is¶
| URL | What it shows |
|---|---|
/ |
Keyword search, meaning (knn()), facets, sort, paginator, to_queryset(), aggregates, highlight |
/aggregations/ |
Aggregate().group_by with COUNT / AVG / MIN / MAX / SUM / TOLIST |
/async/ |
acount / aexists / afirst / alast / aget_by_pk / afacets / aaggregate / aexplain / ato_queryset |
/async/search/ |
Same filters as /, served through SearchListViewMixin.aget |
/lab/ |
Feature matrix: every public API, what it is, how to test it |
/lab/query/ |
Playground + canned demos: lookups, extra(), none(), HASH, Geo |
/lab/index/ |
Registry, IndexManager info/check/drift, REDIS_SEARCH, apply_index_action |
/catalog/ |
CRUD for products, categories, tags, vendors (AUTO_INDEX) |
/catalog/products/<id>/ |
Django row vs objects.get(pk=) (sku, Geo, embedding, full payload) |
/admin/ |
Same models; saves still fire AUTO_INDEX |
Search, aggregations, lab, async, product detail, and catalog CRUD
use SearchDebugMixin. A pill in the bottom-left corner lists Redis search /
aggregate / get and index writes (JSON.SET / HSET / DEL) on that
page. Click it or press Alt+R. See debug overlay.
Category and Vendor documents are embedded, so creating an unused category or vendor does not write a Redis key (the overlay stays at 0 queries). Saving a product, tag, or a category/vendor that already has products does write.
Things to try¶
Keyword + meaning¶
On /, use the Meaning box (leave keyword empty):
| Query | Nearby products |
|---|---|
wireless headphones |
earbuds, speaker, microphone |
trail running |
trail shoes, running shorts, hiking pants |
redis search |
Redis Internals, Search Handbook, Indexing Strategies |
kitchen coffee |
pour-over, mug, skillet |
Add a price or category filter first — Redis applies that, then ranks by meaning.
Query lab¶
On /lab/query/:
| Demo | Expected |
|---|---|
objects.search("coffee") |
kitchen / mug / pour-over |
name__exact="REDIS INTERNALS" |
one hit (stored uppercase) |
department__isnull=True |
Gift Card + Mystery Sample (no category) |
category__isnull=True |
same two products |
available=False |
count 0 (keys never written) |
extra() then filter() |
ValueError |
HASH TagHashDocument |
9 tags, storage=hash |
CRUD / signals¶
- Catalog → Add vendor, then Add product. The new product appears in
search after save (
AUTO_INDEX). - Edit the product, set available off, save. Redis drops the key
(
should_index). - Rename a category. Related products reindex with the new name and a new Geo coordinate.
- Add or rename a tag. The HASH document updates; products with that tag reindex.
- Delete a product. The Redis document is removed.
- On
/lab/index/, POSTupsert/delete/reindex_relatedto runapply_index_actionwithout going through a model form.
After a create / update / delete, the next opted-in page still shows the POST writes in the overlay.