Migrate from 0.1 to 1.0¶
0.1.0 (and the 0.2.x line) indexed Django models by subclassing redis-om
(JsonDocument / HashDocument / EmbeddedJsonDocument) and querying with
redis-om operators. 1.0 is a clean break: redis-py only, one Document
class, Django-style lookups.
There is no automated data migrator. 1.0 writes a new key prefix. After cutover, drop the old redis-om indexes.
Should you upgrade?¶
| You are on | Path |
|---|---|
| 0.1.0 | Jump straight to 1.0. Do not stop on 0.2 unless you only need Python/Django bumps on the old API |
| 0.2.x | Same 1.0 rewrite. 0.2 already dropped EOL Python/Django but still used redis-om |
| New project | Start on 1.0 |
Requirements jump
0.1.0 ran on older Python/Django. 1.0 needs Python 3.10–3.15 (free-threaded from 3.15t) and Django 5.2 / 6.0 / 6.1, plus Redis 8+ (Query Engine + JSON).
What changed¶
flowchart LR
subgraph old["0.1 / 0.2"]
A[JsonDocument / HashDocument]
B[redis-om Field]
C["find(name % q)"]
D["manage.py index"]
E[REDIS_OM_URL]
end
subgraph new["1.0"]
F[Document]
G[redis_search_django.fields]
H["objects.filter(name__search=q)"]
I["manage.py redisearch"]
J["REDIS_SEARCH dict"]
end
A --> F
B --> G
C --> H
D --> I
E --> J
| Area | 0.1.0 | 1.0 |
|---|---|---|
| Redis client | redis-om + Pydantic | redis-py only |
| Document classes | JsonDocument, HashDocument, EmbeddedJsonDocument |
one Document + embedded=True / Index.storage |
| Field declaration | name: str = Field(index=True, full_text_search=True) |
fields.Text(...) or Django.fields |
| Nested models | type annotations + EmbeddedJsonDocument |
fields.Object / fields.Nested |
| Query | find(), %, <<, >= |
Document.objects.filter(), Q, lookups |
| Pagination | RediSearchPaginator |
stock Django Paginator |
| Views | mixins.RediSearchListViewMixin |
views.SearchListViewMixin |
| Command | manage.py index |
manage.py redisearch … |
| Settings | REDIS_OM_URL, REDIS_SEARCH_AUTO_INDEX |
REDIS_SEARCH = {…} |
| Booleans | stored as int (redis-om NUMERIC) |
TAG true/false |
| Dates | redis-om / ISO depending on field | UTC unix timestamp (float) |
| Signals | global handlers on all models | per registered sender |
| Aggregations | bolted-on build_aggregate_request + reducers |
facets() / Aggregate |
import redis |
common in views | re-export from redis_search_django |
Checklist¶
- Python ≥ 3.10, Django ≥ 5.2, Redis 8+ with Query Engine + JSON
- Replace
redis-omwithredis-search-django1.0 (rediscomes in transitively) - Add
REDIS_SEARCHand deleteREDIS_OM_URL/REDIS_SEARCH_AUTO_INDEX - Rewrite every
documents.py - Replace
find()/%/<<withobjects.filter/Q - Replace
manage.py indexin scripts and docs - Point views at
SearchListViewMixinandget_search_queryset() - Replace facet code with
.facets(...)orAggregate -
redisearch rebuild(new prefix — old keys are not reused) -
FT.DROPINDEXold redis-om indexes; optionallySCAN/UNLINKredis_search:* - Confirm
QuerySet.update/bulk_*still need a manualpopulate
Dependencies and settings¶
| Removed | Use instead |
|---|---|
REDIS_OM_URL |
REDIS_SEARCH["URL"] or CONNECTION |
REDIS_SEARCH_AUTO_INDEX |
REDIS_SEARCH["AUTO_INDEX"] |
from redis import Redis |
from redis_search_django import Redis |
from redis.commands.search import reducers |
from redis_search_django import reducers |
If REDIS_OM_URL is still set, 1.0 logs a one-time warning and ignores
it.
Rewrite documents¶
from redis_om import Field
from redis_search_django.documents import EmbeddedJsonDocument, JsonDocument
from .models import Category, Product, Tag, Vendor
class CategoryDocument(EmbeddedJsonDocument):
custom_field: str = Field(index=True, full_text_search=True)
class Django:
model = Category
fields = ["name", "slug"]
@classmethod
def prepare_custom_field(cls, obj: Category) -> str:
return "CUSTOM FIELD VALUE"
class TagDocument(EmbeddedJsonDocument):
class Django:
model = Tag
fields = ["name"]
class VendorDocument(EmbeddedJsonDocument):
class Django:
model = Vendor
fields = ["name"]
class ProductDocument(JsonDocument):
vendor: VendorDocument
category: CategoryDocument | None = None
tags: list[TagDocument] = []
class Django:
model = Product
fields = ["name", "description", "price"]
related_models = {
Vendor: {"related_name": "product", "many": False},
Category: {"related_name": "product_set", "many": True},
Tag: {"related_name": "product_set", "many": True},
}
@classmethod
def get_queryset(cls):
return super().get_queryset().filter(available=True)
from django.db import models
from redis_search_django import fields
from redis_search_django.documents import Document
from .models import Category, Product, Tag, Vendor
class CategoryDocument(Document):
custom_field = fields.Text()
class Django:
model = Category
fields = ["name", "slug"]
embedded = True
@classmethod
def prepare_custom_field(cls, obj: Category) -> str:
return "CUSTOM FIELD VALUE"
class TagDocument(Document):
class Django:
model = Tag
fields = ["name"]
embedded = True
class VendorDocument(Document):
class Django:
model = Vendor
fields = ["name"]
embedded = True
class ProductDocument(Document):
vendor = fields.Object(VendorDocument)
category = fields.Object(CategoryDocument, required=False)
tags = fields.Nested(TagDocument)
class Django:
model = Product
fields = ["name", "description", "price", "available"]
select_related_fields = ["vendor", "category"]
prefetch_related_fields = ["tags"]
# related_models omitted → inferred from Object/Nested
@classmethod
def get_queryset(cls) -> models.QuerySet[Product]:
return super().get_queryset().filter(available=True)
@classmethod
def should_index(cls, instance: models.Model) -> bool:
return instance.available
| 0.1.0 | 1.0 |
|---|---|
JsonDocument |
Document (Index.storage defaults to "json") |
HashDocument |
Document + class Index: storage = "hash" |
EmbeddedJsonDocument |
Document + class Django: embedded = True |
field: Type = Field(...) |
fields.Text / Tag / Numeric / … or list the name in Django.fields |
vendor: VendorDocument |
vendor = fields.Object(VendorDocument) |
tags: list[TagDocument] = [] |
tags = fields.Nested(TagDocument) |
category: CategoryDocument \| None = None |
category = fields.Object(..., required=False) |
required related_models dict |
optional; inferred by default |
get_queryset() only on bulk index |
add should_index() for save() or unavailable products stay in Redis |
Hash documents cannot declare Nested fields.
Rewrite queries¶
| 0.1 redis-om | 1.0 lookup |
|---|---|
Document.name % "q" |
name__search="q" |
Document.name == "Acme" |
name="Acme" or name__exact="Acme" |
Document.price >= 10 |
price__gte=10 |
Document.price <= 100 |
price__lte=100 |
Document.category.name << ["Shoes"] |
category__name__in=["Shoes"] |
Document.tags.name << tags |
tags__name__in=tags |
Document.find() |
Document.objects.all() |
.sort_by("-price") |
.order_by("-price") (one field) |
.execute() |
iterate / slice / list() |
RediSearchPaginator |
django.core.paginator.Paginator |
Use redis_search_django.Q, not django.db.models.Q.
Inspect the compiled query without hitting Redis:
That is the closest equivalent to str(django_qs.query).
Rewrite aggregations¶
Import reducers / AggregateRequest from redis_search_django if you
still need a raw request. See aggregations.
Rewrite views¶
from redis_search_django.mixins import RediSearchListViewMixin
class SearchView(RediSearchListViewMixin, ListView):
document_class = ProductDocument
@cached_property
def search_query_expression(self):
q = self.request.GET.get("query")
if q:
return self.document_class.name % q | self.document_class.description % q
return None
from redis_search_django import Q
from redis_search_django.views import SearchListViewMixin
class SearchView(SearchListViewMixin, ListView):
document_class = ProductDocument
def get_search_queryset(self):
qs = ProductDocument.objects.all()
q = self.request.GET.get("query")
if q:
qs = qs.filter(Q(name__search=q) | Q(description__search=q))
return qs
def facets(self):
return self.get_search_queryset().facets("category__name", "tags__name")
| 0.1 | 1.0 |
|---|---|
redis_search_django.mixins |
redis_search_django.views |
search_query_expression |
get_search_queryset() |
RediSearchPaginator |
stock Paginator |
Management command¶
| 0.1 | 1.0 |
|---|---|
python manage.py index |
python manage.py redisearch rebuild (dev) or create + populate |
python manage.py index --only-migrate |
python manage.py redisearch update |
python manage.py index --models app.Model |
python manage.py redisearch populate --models app.Model |
update never writes documents. After an ALTER, run populate. After
an incompatible schema change, run redisearch reindex --blue-green
(zero-downtime) or rebuild. check exits 1 until that finishes.
Signals¶
| 0.1 | 1.0 |
|---|---|
| Handlers connected to every model | Only registered models + inferred related / M2M through tables |
| Writes inside the signal (ahead of rollback) | transaction.on_commit when inside atomic() |
| No extension point | REDIS_SEARCH["SIGNAL_PROCESSOR"] + dispatch() |
If you need Celery, do not look for an in-tree class. Subclass
BaseSignalProcessor and call apply_index_action from a task —
signals.
Documents imported after ready() no longer pick up signals automatically.
Redis keys¶
| Kind | 0.1 (redis-om) | 1.0 |
|---|---|---|
| Document | redis_search:{module}.{Class}:{pk} |
{PREFIX}:{app}.{model}.{document}:{pk} |
| Index | redis-om generated name | Alias idx:{app}.{model}.{document} → physical {alias}:{fingerprint} |
| Schema meta | none | {PREFIX}:meta:{alias} |
JSON shape changes too: booleans are JSON bools (not 0/1); dates are UTC
timestamps (not ISO unless you prepare_*).
Behavioral changes¶
get_queryset()is bulk-only. 0.1save()still indexed rows thatget_queryset()would exclude. 1.0 usesshould_index()on incremental writes.Noneis omitted from JSON so__isnullworks. Optional relations are missing keys, not"category": null.- Related delete + CASCADE no longer upserts a parent that Django is about to delete.
- Bulk ORM still does not update Redis (
update,bulk_create,QuerySet.delete). - Composite primary keys are rejected.
Cutover¶
# 1. Deploy 1.0 code (indexes are empty / new prefix)
$ python manage.py redisearch create
$ python manage.py redisearch populate
# 2. Smoke-test search
$ python manage.py redisearch check
$ python manage.py redisearch info
# 3. After you are happy, drop old redis-om indexes (names vary)
# redis-cli FT._LIST
# redis-cli FT.DROPINDEX <old-name> DD
Keep 0.1 and 1.0 indexes side by side only if you dual-write during a
rollout; they do not share prefixes. Dual-write is not built in — cut
over in a maintenance window or run populate immediately after deploy.