go-redis/redis · error

redis: FT.AGGREGATE COLLECT: ASC and DESC are mutually exclu

Error message

redis: FT.AGGREGATE COLLECT: ASC and DESC are mutually exclusive

What it means

Returned by NewCollectReducer when a SortBy entry in FTAggregateCollect has both Asc and Desc set to true. A COLLECT SORTBY field can carry at most one direction; setting both is contradictory, so the builder refuses to serialize it. This guard runs locally before any command is sent to Redis.

Source

Thrown at search_collect.go:116

	default:
		return nil, fmt.Errorf("redis: FT.AGGREGATE COLLECT requires FieldsAll or a non-empty Fields list")
	}

	// DISTINCT (optional, forward-compatible).
	if o.Distinct {
		args = append(args, "DISTINCT")
	}

	// SORTBY (optional). sort_narg counts each field plus its optional
	// direction token.
	if len(o.SortBy) > 0 {
		sortTokens := make([]interface{}, 0, len(o.SortBy)*2)
		for _, s := range o.SortBy {
			if strings.TrimLeft(s.FieldName, "@") == "" {
				return nil, fmt.Errorf("redis: FT.AGGREGATE COLLECT: empty field name in SortBy")
			}
			if s.Asc && s.Desc {
				return nil, fmt.Errorf("redis: FT.AGGREGATE COLLECT: ASC and DESC are mutually exclusive")
			}
			sortTokens = append(sortTokens, ensureAtPrefix(s.FieldName))
			switch {
			case s.Desc:
				sortTokens = append(sortTokens, "DESC")
			case s.Asc:
				sortTokens = append(sortTokens, "ASC")
				// neither set: ASC is the server default; emit nothing.
			}
		}
		args = append(args, "SORTBY", len(sortTokens))
		args = append(args, sortTokens...)
	}

	// LIMIT (optional).
	if o.Limit != nil {
		args = append(args, "LIMIT", o.Limit.Offset, o.Limit.Count)
	}

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Set exactly one of Asc or Desc on each SortBy entry (or leave both false for the server's ASC default).
  2. Audit the call site that constructs FTAggregateSortBy to ensure toggling one direction clears the other.
  3. If directions come from user input, coerce a single enum into Asc xor Desc before passing to NewCollectReducer.

Example fix

// before
SortBy: []FTAggregateSortBy{{FieldName: "price", Asc: true, Desc: true}}
// after
SortBy: []FTAggregateSortBy{{FieldName: "price", Desc: true}}
Defensive patterns

Strategy: validation

Validate before calling

func validCollectDirections(sb []FTAggregateSortBy) error {
	for i, s := range sb {
		if s.Asc && s.Desc {
			return fmt.Errorf("SortBy[%d]: Asc and Desc both set", i)
		}
	}
	return nil
}

Type guard

func directionIsUnique(s FTAggregateSortBy) bool { return !(s.Asc && s.Desc) }

Prevention

When it happens

Trigger: NewCollectReducer(FTAggregateCollect{SortBy: []FTAggregateSortBy{{FieldName: "price", Asc: true, Desc: true}}}), or the equivalent via AggregateBuilder.Collect. Triggered during argument building, not from a server reply.

Common situations: Defaulting both flags to true in a config struct. Reusing a FTAggregateSortBy value and flipping Desc on without clearing Asc. UI that exposes separate ASC/DESC toggles and allows both selected.

Related errors


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