go-redis/redis · error

FT.AGGREGATE: Collect must follow a GroupBy step

Error message

FT.AGGREGATE: Collect must follow a GroupBy step

What it means

Recorded (and later returned by Run) when AggregateBuilder.Collect is called but the most recent pipeline step is not a GROUPBY. Collect builds a REDUCE COLLECT clause that attaches to a GROUPBY step; like Reduce/ReduceAs, misordering defers the error to Run.

Source

Thrown at search_builders.go:328

	}
	g := b.options.Steps[n-1].GroupBy
	g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args, As: alias})
	return b
}

// Collect adds a REDUCE COLLECT clause to the last step, which must be a
// GROUPBY. The COLLECT options (FIELDS/DISTINCT/SORTBY/LIMIT/AS) are rendered
// and the argument count is computed automatically; field and sort names are
// normalized to a single "@" prefix. Set FTAggregateCollect.As to alias the
// output column.
//
// If the last step is not a GROUPBY, or the options are invalid (no FIELDS
// selector), Run returns the recorded error without issuing the command.
// COLLECT requires Redis 8.8+ with unstable features enabled.
func (b *AggregateBuilder) Collect(o FTAggregateCollect) *AggregateBuilder {
	n := len(b.options.Steps)
	if n == 0 || b.options.Steps[n-1].GroupBy == nil {
		b.setErr(fmt.Errorf("FT.AGGREGATE: Collect must follow a GroupBy step"))
		return b
	}
	reducer, err := NewCollectReducer(o)
	if err != nil {
		b.setErr(err)
		return b
	}
	g := b.options.Steps[n-1].GroupBy
	g.Reduce = append(g.Reduce, reducer)
	return b
}

// SortBy adds SORTBY <field> ASC|DESC. Consecutive SortBy calls (with no
// other step in between) are merged into a single SORTBY clause so fields
// act as tiebreakers. A SortBy call after a non-SortBy step starts a new
// SORTBY step.
//
// Note: this is a semantics change from earlier experimental versions of

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Call b.GroupBy(...) immediately before b.Collect(o) so COLLECT attaches to a GROUPBY step.
  2. Remember COLLECT is a reducer, not a standalone step; pair it with GroupBy every time.
  3. Add a test that builds the pipeline and asserts Run does not return the ordering error.

Example fix

// before
b := c.NewAggregateBuilder(ctx, idx, q)
b.Collect(redis.FTAggregateCollect{Fields: []string{"@sku"}}) // no GroupBy

// after
b := c.NewAggregateBuilder(ctx, idx, q)
b.GroupBy("@order").Collect(redis.FTAggregateCollect{Fields: []string{"@sku"}})
Defensive patterns

Strategy: validation

Validate before calling

// COLLECT is a REDUCE clause; ensure a GroupBy precedes it.
if len(steps) == 0 || steps[len(steps)-1].GroupBy == nil {
    return errors.New("Collect requires a preceding GroupBy")
}

Try / catch

res, err := b.Run()
if err != nil && strings.Contains(err.Error(), "Collect must follow a GroupBy step") {
    // add a GroupBy before Collect
}

Prevention

When it happens

Trigger: Calling b.Collect(o) when the last step is not a GroupBy, or the builder has no steps. COLLECT also requires Redis 8.8+ with unstable features; the ordering check fires before any version validation.

Common situations: Reordered pipeline where Collect was placed before GroupBy; using Collect as if it were a top-level step rather than a REDUCE clause; refactoring that moved Collect above its GroupBy.

Related errors


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