Aggregations¶
FT.AGGREGATE is a first-class API. You never pass raw @category_name
unless you opt into AggregateRequest.
Facets¶
One FT.AGGREGATE per facet (RediSearch groups one way per request).
Arguments are Django lookups.
from shop.documents import ProductDocument
ProductDocument.objects.filter(name__search="shoes").facets(
"category__name",
"tags__name",
)
# async: await qs.afacets("category__name", "tags__name")
Returns:
{
"category__name": [
{"value": "Shoes", "count": 112},
{"value": "Clothes", "count": 40},
],
"tags__name": [{"value": "Blue", "count": 14}],
}
| Input lookup | Grouped RediSearch alias |
|---|---|
category__name |
@category_name |
tags__name |
@tags_name |
vendor__name |
@vendor_name |
available |
@available |
The current queryset’s filter (and extra()) is the aggregate query string.
Unfiltered:
In a list view:
{% for row in facets.category__name %}
<label>
<input type="checkbox" name="category" value="{{ row.value }}" />
{{ row.value }} ({{ row.count }})
</label>
{% endfor %}
Aggregate¶
Fluent builder. Lookups are Django names; the compiler maps them to AS
aliases.
from redis_search_django import Aggregate
from shop.documents import ProductDocument
ProductDocument.objects.filter(available=True).aggregate(
# async: await qs.aaggregate(Aggregate()...)
Aggregate()
.group_by("vendor__name")
.count("count")
.avg("price", "avg_price")
.sum("price", "sum_price")
.min("price", "min_price")
.max("price", "max_price")
.sort_by("-count")
.limit(10)
.load("name", "price")
)
Typical row:
Redis returns reducer values as strings. Cast in your app
(int(row["count"]), float(row["avg_price"])).
Builder methods¶
| Method | Meaning |
|---|---|
group_by(lookup) |
GROUPBY 1 @alias |
count(alias="count") |
REDUCE COUNT |
avg(lookup, alias) |
REDUCE AVG |
sum(lookup, alias) |
REDUCE SUM |
min(lookup, alias) |
REDUCE MIN |
max(lookup, alias) |
REDUCE MAX |
tolist(lookup, alias) |
REDUCE TOLIST |
sort_by(field) |
One field; leading - = descending |
limit(n) |
LIMIT 0 n |
load(*lookups) |
LOAD those aliases |
sort_by is single-field, same as search.
How lookups become aliases¶
Flattening is not configurable:
| User lookup / facet arg | JSON path | AS alias |
|---|---|---|
pk |
$.pk |
pk |
name |
$.name |
name |
vendor__name |
$.vendor.name |
vendor_name |
category__name |
$.category.name |
category_name |
tags__name |
$.tags[*].name |
tags_name |
A concrete field named category_name plus Object category.name is a
register-time ImproperlyConfigured (alias collision). Rename with
fields.Text(as_name="...").
Raw AggregateRequest¶
For APPLY, FILTER, WITHCURSOR, timeouts, or reducers this package does
not wrap, pass a request from this package (not from redis.commands...):
from redis_search_django import AggregateRequest, reducers
from shop.documents import ProductDocument
request = (
AggregateRequest("@available:{true}")
.group_by(["@vendor_name"], reducers.count().alias("count"))
.sort_by("@count", desc=True)
.limit(0, 10)
)
ProductDocument.objects.aggregate(request)
Trusted raw RediSearch
You are responsible for aliases (@vendor_name, not vendor__name) and
for not interpolating unsanitized user strings.
The queryset’s compiled filter is not applied when you pass a complete
AggregateRequest — that object carries its own query string.
To combine a Django-filtered queryset with a raw request, compile first and
either call aggregate() on that same queryset or pass query_params
explicitly:
qs = ProductDocument.objects.filter(name__search="shoes")
query, params = qs.raw()
qs.aggregate(
AggregateRequest(query).group_by(["@category_name"], reducers.count().alias("n"))
)
# or, on a different queryset:
ProductDocument.objects.aggregate(
AggregateRequest(query).group_by(["@category_name"], reducers.count().alias("n")),
query_params=params,
)
If the request query still has $p1 / $p2 / … and those names are not
bound, aggregate() raises MissingQueryParams (a FieldError)
immediately. Bind them with extra(query, params=...), call aggregate()
on the queryset that compiled the filter, or pass query_params. Prefer
Aggregate() when the filter uses __search — it always pairs the compiled
query with its PARAMS.
Reducers¶
Aggregate method |
redis-py reducers |
|---|---|
count |
reducers.count() |
avg |
reducers.avg |
sum |
reducers.sum |
min |
reducers.min |
max |
reducers.max |
tolist |
reducers.tolist |
Anything else: build an AggregateRequest with reducers imported from
redis_search_django.
from redis_search_django import AggregateRequest, reducers
AggregateRequest("*").group_by(
["@category_name"],
reducers.count().alias("n"),
reducers.avg("@price").alias("avg_price"),
)
Dialect and filters¶
Every aggregate the queryset builds uses dialect 2. The filter is the
same compiled string as FT.SEARCH for that queryset (Q tree or
extra()).
# These share the same filter expression
qs = ProductDocument.objects.filter(price__gte=10, available=True)
hits = list(qs[:20])
facets = qs.facets("category__name")
stats = qs.aggregate(Aggregate().group_by("vendor__name").avg("price", "avg_price"))
What is not wrapped¶
| Feature | Status |
|---|---|
APPLY |
Use AggregateRequest |
FILTER (post-group) |
Use AggregateRequest |
WITHCURSOR |
Use AggregateRequest |
| Query timeout | Use AggregateRequest |
Multi-field SORTBY |
Not on Aggregate.sort_by; raw request only |
| Hybrid / KNN aggregate | Out of scope |
facets() always uses COUNT. For average price per facet, use
Aggregate().group_by(...).avg(...).