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 b

View on GitHub (pinned to c5cad058c7)

Solutions

  1. Add a GroupBy step before Reduce: b.GroupBy("@field").Reduce(...)
  2. Reorder the builder chain so Reduce immediately follows GroupBy
  3. 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

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


AI-assisted analysis of redis/go-redis@c5cad058c7 (2026-09-01). Data as JSON: /api/errors/6cfb73b21e5481dc. Report an issue: GitHub.