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 b

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Call b.GroupBy(fields...) immediately before b.Reduce(...) so the last step is the GROUPBY the reducer attaches to.
  2. Inspect the builder's step order in tests; assert the step preceding Reduce is a GroupBy.
  3. 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

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


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/ae4d5973bf3c9ea4.json. Report an issue: GitHub.