go-redis/redis · error
FT.AGGREGATE: Reduce must follow a GroupBy step
Error message
FT.AGGREGATE: Reduce must follow a GroupBy step
What it means
Recorded (and later returned by Run) when AggregateBuilder.Reduce is called but the most recent pipeline step is not a GROUPBY. REDUCE is a clause attached to a GROUPBY step; calling it without a preceding GroupBy is a misuse of the fluent API. The error is stored on the builder and surfaced from Run rather than panicking.
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 36d97525cd)
Solutions
- Call b.GroupBy(fields...) immediately before b.Reduce(...) so the last step is the GROUPBY the reducer attaches to.
- Inspect the builder's step order in tests; assert the step preceding Reduce is a GroupBy.
- Run a small unit test that builds the pipeline and calls Run against a mock to catch ordering errors early.
Example fix
// before
b := c.NewAggregateBuilder(ctx, idx, q)
b.Reduce(redis.SearchAggCount) // no GroupBy -> error at Run
res, err := b.Run()
// after
b := c.NewAggregateBuilder(ctx, idx, q)
b.GroupBy("@brand").Reduce(redis.SearchAggCount)
res, err := b.Run() Defensive patterns
Strategy: validation
Validate before calling
// Track step types in the builder and assert Reduce follows GroupBy.
func requiresGroupBy(steps []FTAggregateStep) bool {
n := len(steps)
return n > 0 && steps[n-1].GroupBy != nil
} Try / catch
res, err := b.Run()
if err != nil && strings.Contains(err.Error(), "Reduce must follow a GroupBy step") {
// fix the builder pipeline order; this is a programming error
} Prevention
- Chain GroupBy().Reduce(...) in a single fluent expression to make ordering obvious.
- Add a unit test that builds each pipeline and asserts Run does not return an ordering error.
- Code-review builder chains specifically for step ordering.
When it happens
Trigger: Calling b.Reduce(fn, args...) on an AggregateBuilder that has no steps yet, or whose last step is a SortBy/Load/Filter rather than GroupBy. The error does not surface until Run() is called.
Common situations: Builder pipeline reordered during refactoring (Reduce accidentally placed before GroupBy); copy-paste of an aggregation snippet missing the GroupBy line; chaining Reduce after a Load step.
Related errors
- FT.AGGREGATE: ReduceAs must follow a GroupBy step
- FT.AGGREGATE: Collect must follow a GroupBy step
- FT.AGGREGATE: SortByMax must follow a SortBy step
- redis: FT.AGGREGATE COLLECT: empty field name in Fields
- redis: FT.AGGREGATE COLLECT requires FieldsAll or a non-empt
AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06).
Data as JSON: /data/errors/ae4d5973bf3c9ea4.json.
Report an issue: GitHub.