redis/go-redis · error
FT.AGGREGATE: Reduce must follow a GroupBy step
Error message
FT.AGGREGATE: Reduce must follow a GroupBy step
What it means
The FT.AGGREGATE builder records this error when Reduce() is called before any GroupBy step, because REDUCE clauses are attached to the last step and only GROUPBY steps can hold reducers. The error is surfaced later when Run() is called — the command is not sent.
Source
Thrown at search_builders.go:295
}
b.options.Steps = append(b.options.Steps, FTAggregateStep{Apply: a})
return b
}
// GroupBy adds a new GROUPBY <fields...> step.
func (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder {
b.options.Steps = append(b.options.Steps, FTAggregateStep{
GroupBy: &FTAggregateGroupBy{Fields: fields},
})
return b
}
// Reduce adds a REDUCE <fn> [<#args> <args...>] clause to the last step,
// which must be a GROUPBY. If it is not, Run will return an error.
func (b *AggregateBuilder) Reduce(fn SearchAggregator, args ...interface{}) *AggregateBuilder {
n := len(b.options.Steps)
if n == 0 || b.options.Steps[n-1].GroupBy == nil {
b.setErr(fmt.Errorf("FT.AGGREGATE: Reduce must follow a GroupBy step"))
return b
}
g := b.options.Steps[n-1].GroupBy
g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args})
return b
}
// ReduceAs does the same but also sets an alias: REDUCE <fn> … AS <alias>.
// The last step must be a GROUPBY; otherwise Run will return an error.
func (b *AggregateBuilder) ReduceAs(fn SearchAggregator, alias string, args ...interface{}) *AggregateBuilder {
n := len(b.options.Steps)
if n == 0 || b.options.Steps[n-1].GroupBy == nil {
b.setErr(fmt.Errorf("FT.AGGREGATE: ReduceAs must follow a GroupBy step"))
return b
}
g := b.options.Steps[n-1].GroupBy
g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args, As: alias})
return bView on GitHub (pinned to c5cad058c7)
Solutions
- Add a GroupBy step before Reduce: b.GroupBy("@field").Reduce(...)
- Reorder the builder chain so Reduce immediately follows GroupBy
- If no grouping is intended, remove the Reduce call or use Apply/SortBy instead
Example fix
// before
b := client.FTAggregate(ctx, "idx").Query("*").Reduce(redis.SearchSum, 1, "@price")
// after
b := client.FTAggregate(ctx, "idx").Query("*").
GroupBy("@category").Reduce(redis.SearchSum, 1, "@price").As("total") Defensive patterns
Strategy: validation
Validate before calling
func addReduce(b *redis.AggregateBuilder, groupBy string, fn redis.SearchAggregator, args ...interface{}) *redis.AggregateBuilder {
return b.GroupBy(groupBy).Reduce(fn, args...)
} Try / catch
cmd := b.Run(ctx)
if err := cmd.Err(); err != nil {
if strings.Contains(err.Error(), "Reduce must follow a GroupBy") {
return fmt.Errorf("bad FT.AGGREGATE query: %w", err)
}
return err
} Prevention
- Always chain GroupBy directly before Reduce/ReduceAs
- Read the builder docs: REDUCE attaches to the last step
- Add unit tests that call Run() on built aggregates to surface builder errors early
When it happens
Trigger: Calling AggregateBuilder.Reduce(fn, args...) when no GroupBy has been added yet (builder has zero steps or last step is not a GroupBy), at search_builders.go:295.
Common situations: Writing an aggregation assuming SQL-like implicit grouping; chaining Reduce after Filter/SortBy instead of GroupBy; copying a query and deleting the GroupBy line but keeping the Reduce.
Related errors
- FT.AGGREGATE: ReduceAs must follow a GroupBy step
- failed to create operation duration histogram: %w
- failed to create connection count metric: %w
- failed to create connection create time histogram: %w
- failed to create connection relaxed timeout metric: %w
AI-assisted analysis of redis/go-redis@c5cad058c7 (2026-09-01).
Data as JSON: /api/errors/6cfb73b21e5481dc.
Report an issue: GitHub.