go-redis/redis · error

FT.AGGREGATE: each step must set exactly one of Load…

Error message

FT.AGGREGATE: each step must set exactly one of Load, Apply, GroupBy, SortBy (got %d)

What it means

Returned by appendFTAggregateStep when a single FTAggregateStep does not set exactly one of Load, Apply, GroupBy, or SortBy (the count is reported in '%d'). Each Step must map to exactly one Redis pipeline stage, so zero or multiple set fields is ambiguous and rejected before serialization.

Solutions

  1. Populate exactly one of Load, Apply, GroupBy, or SortBy on each FTAggregateStep.
  2. Split a step that needs multiple stages into multiple Steps (the pipeline is ordered).
  3. Remove placeholder/empty entries from the Steps slice before calling FTAggregate.
  4. Add a unit assertion that each step sets exactly one field to catch regressions.

Example fix

// before
Steps: []FTAggregateStep{
    {},                                              // count 0 -> error
    {Apply: &FTAggregateApply{Field: "@price/2"},
     GroupBy: &FTAggregateGroupBy{Fields: []string{"cat"}}}, // count 2 -> error
}
// after
Steps: []FTAggregateStep{
    {GroupBy: &FTAggregateGroupBy{Fields: []string{"cat"}}},
    {Apply: &FTAggregateApply{Field: "@price/2", As: "half"}},
}
Defensive patterns

Strategy: validation

Validate before calling

func validateStepExactlyOne(step FTAggregateStep) error {
	set := 0
	if step.Load != nil { set++ }
	if step.Apply != nil { set++ }
	if step.GroupBy != nil { set++ }
	if step.SortBy != nil { set++ }
	if set != 1 {
		return fmt.Errorf("step must set exactly one of Load/Apply/GroupBy/SortBy, got %d", set)
	}
	return nil
}

Type guard

func stepIsWellFormed(step FTAggregateStep) bool {
	set := 0
	if step.Load != nil { set++ }
	if step.Apply != nil { set++ }
	if step.GroupBy != nil { set++ }
	if step.SortBy != nil { set++ }
	return set == 1
}

Prevention

When it happens

Trigger: Adding an FTAggregateStep with none of Load/Apply/GroupBy/SortBy set (count 0), or with two or more set (count >= 2). Triggered while iterating options.Steps in FTAggregateQuery.

Common situations: Empty step literal {{}} left as a placeholder. Reusing a step struct and setting Apply on top of an already-set GroupBy. Conditional logic that sometimes leaves all fields nil. Misunderstanding that each Step holds exactly one stage.

Related errors


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

Appendix: source

Thrown at search_commands.go:725

// appendFTAggregateStep appends the Redis command arguments for a single
// aggregation pipeline step. Each step must set exactly one of Load, Apply,
// GroupBy or SortBy.
func appendFTAggregateStep(args []interface{}, step FTAggregateStep) ([]interface{}, error) {
	set := 0
	if step.Load != nil {
		set++
	}
	if step.Apply != nil {
		set++
	}
	if step.GroupBy != nil {
		set++
	}
	if step.SortBy != nil {
		set++
	}
	if set != 1 {
		return args, fmt.Errorf("FT.AGGREGATE: each step must set exactly one of Load, Apply, GroupBy, SortBy (got %d)", set)
	}

	switch {
	case step.Load != nil:
		args = append(args, "LOAD")
		countIdx := len(args)
		args = append(args, 0)
		count := 0
		args = append(args, step.Load.Field)
		count++
		if step.Load.As != "" {
			args = append(args, "AS", step.Load.As)
			count += 2
		}
		args[countIdx] = count
	case step.Apply != nil:
		args = append(args, "APPLY", step.Apply.Field)
		if step.Apply.As != "" {

View on GitHub (pinned to 36d97525cd)