{"id":"d2acf1b2772fa495","repo":"go-redis/redis","slug":"redis-ft-aggregate-collect-asc-and-desc-are-mutu","errorCode":null,"errorMessage":"redis: FT.AGGREGATE COLLECT: ASC and DESC are mutually exclusive","messagePattern":"redis: FT\\.AGGREGATE COLLECT: ASC and DESC are mutually exclusive","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"search_collect.go","lineNumber":116,"sourceCode":"\tdefault:\n\t\treturn nil, fmt.Errorf(\"redis: FT.AGGREGATE COLLECT requires FieldsAll or a non-empty Fields list\")\n\t}\n\n\t// DISTINCT (optional, forward-compatible).\n\tif o.Distinct {\n\t\targs = append(args, \"DISTINCT\")\n\t}\n\n\t// SORTBY (optional). sort_narg counts each field plus its optional\n\t// direction token.\n\tif len(o.SortBy) > 0 {\n\t\tsortTokens := make([]interface{}, 0, len(o.SortBy)*2)\n\t\tfor _, s := range o.SortBy {\n\t\t\tif strings.TrimLeft(s.FieldName, \"@\") == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"redis: FT.AGGREGATE COLLECT: empty field name in SortBy\")\n\t\t\t}\n\t\t\tif s.Asc && s.Desc {\n\t\t\t\treturn nil, fmt.Errorf(\"redis: FT.AGGREGATE COLLECT: ASC and DESC are mutually exclusive\")\n\t\t\t}\n\t\t\tsortTokens = append(sortTokens, ensureAtPrefix(s.FieldName))\n\t\t\tswitch {\n\t\t\tcase s.Desc:\n\t\t\t\tsortTokens = append(sortTokens, \"DESC\")\n\t\t\tcase s.Asc:\n\t\t\t\tsortTokens = append(sortTokens, \"ASC\")\n\t\t\t\t// neither set: ASC is the server default; emit nothing.\n\t\t\t}\n\t\t}\n\t\targs = append(args, \"SORTBY\", len(sortTokens))\n\t\targs = append(args, sortTokens...)\n\t}\n\n\t// LIMIT (optional).\n\tif o.Limit != nil {\n\t\targs = append(args, \"LIMIT\", o.Limit.Offset, o.Limit.Count)\n\t}","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/search_collect.go#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set exactly one of Asc or Desc on each SortBy entry (or leave both false for the server's ASC default).","Audit the call site that constructs FTAggregateSortBy to ensure toggling one direction clears the other.","If directions come from user input, coerce a single enum into Asc xor Desc before passing to NewCollectReducer."],"exampleFix":"// before\nSortBy: []FTAggregateSortBy{{FieldName: \"price\", Asc: true, Desc: true}}\n// after\nSortBy: []FTAggregateSortBy{{FieldName: \"price\", Desc: true}}","handlingStrategy":"validation","validationCode":"func validCollectDirections(sb []FTAggregateSortBy) error {\n\tfor i, s := range sb {\n\t\tif s.Asc && s.Desc {\n\t\t\treturn fmt.Errorf(\"SortBy[%d]: Asc and Desc both set\", i)\n\t\t}\n\t}\n\treturn nil\n}","typeGuard":"func directionIsUnique(s FTAggregateSortBy) bool { return !(s.Asc && s.Desc) }","tryCatchPattern":null,"preventionTips":["Expose direction as a single enum (asc/desc/none) and map it to Asc xor Desc at the boundary.","Never default both Asc and Desc to true.","When toggling one direction on, clear the other in the same assignment."],"tags":["ft-aggregate","collect","validation","api-misuse"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}