Aggregate your data

Use FT.AGGREGATE to group, summarize, and transform your data with GROUPBY, REDUCE, and APPLY.

This is step 4 of the Redis Search tutorial. It builds on the index and the queries from the previous step.

FT.SEARCH answers "which records match?". Often you want to answer a different kind of question:

  • How many products are in each category?
  • What is the average price per category?
  • Which brand has the highest average rating?

These are aggregation questions. They summarize across many documents instead of returning them one by one. The FT.AGGREGATE command handles them by running your results through a pipeline of steps. The three you will use most are:

  • GROUPBY — collect documents into groups that share a field value.
  • REDUCE — compute something for each group, such as a count or an average.
  • APPLY — calculate a new value from existing fields.

Count documents per group

The most common aggregation is a grouped count. This groups every product by category and counts how many fall into each. REDUCE COUNT 0 counts the documents in each group, and AS count names the result:

Grouped count: Use GROUPBY with REDUCE COUNT to count documents in each group
FT.AGGREGATE idx:catalog "*" GROUPBY 1 @category REDUCE COUNT 0 AS count
req = aggregations.AggregateRequest("*").group_by(
    "@category", reducers.count().alias("count")
)
res = index.aggregate(req).rows
print(res)
# >>> [['category', 'Audio', 'count', '3'], ['category', 'Computers', 'count', '2'], ...]

The "*" after the index name is a query expression, exactly like in FT.SEARCH. Here it means "aggregate over all documents", but you could narrow the input first, for example @price:[0 100] to aggregate only the cheaper products. GROUPBY 1 @category reads as "group by one field: category".

Compute an average per group

Swap COUNT for a different reducer to compute other summaries. This calculates the average price in each category and sorts the groups from most to least expensive with SORTBY:

Grouped average: Use REDUCE AVG to average a numeric field per group, then order groups with SORTBY
FT.AGGREGATE idx:catalog "*" GROUPBY 1 @category REDUCE AVG 1 @price AS avg_price SORTBY 2 @avg_price DESC
req = (
    aggregations.AggregateRequest("*")
    .group_by("@category", reducers.avg("@price").alias("avg_price"))
    .sort_by(aggregations.Desc("@avg_price"))
)
res = index.aggregate(req).rows
print(res)
# >>> [['category', 'Computers', 'avg_price', '864.495'], ...]

REDUCE AVG 1 @price reads as "apply the AVG reducer to one field: price". The SORTBY 2 @avg_price DESC clause sorts by the computed avg_price value; the 2 is the number of arguments that follow (@avg_price and DESC). Other reducers include SUM, MIN, MAX, and COUNT_DISTINCT; see the aggregation reference for the full list.

Calculate new values with APPLY

APPLY evaluates an expression against each record and adds the result as a new field. This takes the Audio products, loads their name and price, and computes a 10%-off sale_price:

Calculated field: Use APPLY to derive a new value (a discounted price) from an existing field
FT.AGGREGATE idx:catalog "@category:{Audio}" LOAD 2 name price APPLY "@price - (@price * 0.1)" AS sale_price
req = (
    aggregations.AggregateRequest("@category:{Audio}")
    .load("name", "price")
    .apply(sale_price="@price - (@price * 0.1)")
)
res = index.aggregate(req).rows
print(res)
# >>> [['name', 'Aurora AcousticPro Headphones', 'price', '199.99', 'sale_price', '179.991'], ...]

The LOAD 2 name price clause pulls those two fields into the pipeline so the expression can use them and so they appear in the output. APPLY does not group anything; it transforms each record in place.

Build a pipeline

The real power of FT.AGGREGATE is chaining these steps. This finds the average rating per brand and returns the highest-rated brands first — a simple "best brands" leaderboard:

Pipeline: Combine GROUPBY, REDUCE, and SORTBY to rank brands by average rating
FT.AGGREGATE idx:catalog "*" GROUPBY 1 @brand REDUCE AVG 1 @rating AS avg_rating SORTBY 2 @avg_rating DESC
req = (
    aggregations.AggregateRequest("*")
    .group_by("@brand", reducers.avg("@rating").alias("avg_rating"))
    .sort_by(aggregations.Desc("@avg_rating"))
)
res = index.aggregate(req).rows
print(res)
# >>> [['brand', 'Clackr', 'avg_rating', '4.8'], ['brand', 'Pixma', 'avg_rating', '4.55'], ...]

(The output is truncated; eight brands are returned in all.) You can keep extending the pipeline — apply multiple reducers under one GROUPBY, chain a second GROUPBY, add FILTER and LIMIT steps, and more. See the aggregation queries guide for deeper examples.

Try it in Redis Insight:
Aggregation results are tabular by nature, so they are especially easy to read in the Redis Insight Search workspace. Paste any FT.AGGREGATE command from this page into the query editor to see each group as a row.

Next steps

You can now find, filter, and summarize structured data. The final step goes beyond keywords and exact values to search by meaning. Continue to vector and hybrid search.

RATE THIS PAGE
Back to top ↑