{
  "schema_version": 2,
  "id": "commands/ft.aggregate",
  "title": "FT.AGGREGATE",
  "url": "https://redis.io/docs/latest/commands/ft.aggregate/",
  "summary": "Run a search query on an index and perform aggregate transformations on the results",
  "since": "1.1.0",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc",
    "oss",
    "kubernetes",
    "clients"
  ],
  "last_updated": "2026-09-18T09:47:58+01:00",
  "page_type": "content",
  "content_hash": "d1301602cdbc23d546fc2433a4d8d1aa12910fb53471f01e88a21a1b929f8c18",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Run a search query on an index and perform aggregate transformations on the results.\n\n> [!NOTE]\n> This command will only return keys to which the user has read access.\n\n[Examples](#examples)"
    },
    {
      "id": "required-arguments",
      "title": "Required arguments",
      "role": "content",
      "text": "<details open>\n<summary><code>index</code></summary>\n\nis index name against which the query is executed. You must first create the index using [`FT.CREATE`](https://redis.io/docs/latest/commands/ft.create).\n</details>\n\n<details open>\n<summary><code>query</code></summary> \n\nis base filtering query that retrieves the documents. It follows the exact same syntax as the search query, including filters, unions, not, optional, and so on.\n</details>"
    },
    {
      "id": "optional-arguments",
      "title": "Optional arguments",
      "role": "parameters",
      "text": "<details open>\n<summary><code>VERBATIM</code></summary>\n\nif set, does not try to use stemming for query expansion but searches the query terms verbatim.\n</details>\n\n<details open>\n<summary><code>LOAD {nargs} {identifier} AS {property} …</code></summary> \n\nloads document attributes from the source document. \n - `identifier` is either an attribute name for hashes and JSON or a JSON Path expression for JSON. \n - `property` is the optional name used in the result. If it is not provided, the `identifier` is used. This should be avoided.\n - If `*` is used as `nargs`, all attributes in a document are loaded.\n\nAttributes needed for aggregations should be stored as `SORTABLE`, where they are available to the aggregation pipeline with very low latency. `LOAD` hurts the performance of aggregate queries considerably because every processed record needs to execute the equivalent of [`HMGET`](https://redis.io/docs/latest/commands/hmget) against a Redis key, which when executed over millions of keys, amounts to high processing times.\n\n<details open>\n<summary><code>GROUPBY {nargs} {property}</code></summary> \n\ngroups the results in the pipeline based on one or more properties. Each group should have at least one _reducer_, a function that handles the group entries,\n  either counting them, or performing multiple aggregate operations (see below).\n</details>\n      \n<details open>\n<summary><code>REDUCE {func} {nargs} {arg} … [AS {name}]</code></summary>\n\nreduces the matching results in each group into a single record, using a reduction function. For example, `COUNT` counts the number of records in the group. The reducers can have their own property names using the `AS {name}` optional argument. If a name is not given, the resulting name will be the name of the reduce function and the group properties. For example, if a name is not given to `COUNT_DISTINCT` by property `@foo`, the resulting name will be `count_distinct(@foo)`.\n  \nSee [Supported GROUPBY reducers](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations#supported-groupby-reducers) for more details.\n</details>\n\n<details open>\n<summary><code>SORTBY {nargs} {property} {ASC|DESC} [MAX {num}]</code></summary> \n\nsorts the pipeline up until the point of `SORTBY`, using a list of properties. \n\n - By default, sorting is ascending, but `ASC` or `DESC ` can be added for each property. \n - `nargs` is the number of sorting parameters, including `ASC` and `DESC`, for example, `SORTBY 4 @foo ASC @bar DESC`.\n - `MAX` is used to optimized sorting, by sorting only for the n-largest elements. Although it is not connected to `LIMIT`, you usually need just `SORTBY … MAX` for common queries.\n\nAttributes needed for `SORTBY` should be stored as `SORTABLE` to be available with very low latency.\n\n**Sorting Optimizations**: performance is optimized for sorting operations on `DIALECT 4` in different scenarios:\n   - Skip Sorter - applied when there is no sort of any kind. The query can return after it reaches the `LIMIT` requested results.\n   - Partial Range - applied when there is a `SORTBY` clause over a numeric field, with no filter or filter by the same numeric field, the query iterate on a range large enough to satisfy the `LIMIT` requested results.\n   - Hybrid - applied when there is a `SORTBY` clause over a numeric field and another non-numeric filter. Some results will get filtered, and the initial range may not be large enough. The iterator is then rewinding with the following ranges, and an additional iteration takes place to collect the `LIMIT` requested results.\n   - No optimization - If there is a sort by score or by non-numeric field, there is no other option but to retrieve all results and compare their values.\n\n**Counts behavior**: optional `WITHCOUNT` argument returns accurate counts for the query results with sorting. This operation processes all results in order to get an accurate count, being less performant than the optimized option (default behavior on `DIALECT 4`)\n\nYou can also use `WITHOUTCOUNT` in place of `DIALECT 4` when used with either `FT.SEARCH` or `FT.AGGREGATE`.\n</details>\n\n<details open>\n<summary><code>APPLY {expr} AS {name}</code></summary> \n\napplies a 1-to-1 transformation on one or more properties and either stores the result as a new property down the pipeline or replaces any property using this\n  transformation. \n  \n`expr` is an expression that can be used to perform arithmetic operations on numeric properties, or functions that can be applied on properties depending on their types (see below), or any combination thereof. For example, `APPLY \"sqrt(@foo)/log(@bar) + 5\" AS baz` evaluates this expression dynamically for each record in the pipeline and store the result as a new property called `baz`, which can be referenced by further `APPLY`/`SORTBY`/`GROUPBY`/`REDUCE` operations down the\n  pipeline.\n\nSee [APPLY expressions](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations#apply-expressions) for details.\n</details>\n\n<details open>\n<summary><code>LIMIT {offset} {num}</code></summary> \n\nlimits the number of results to return just `num` results starting at index `offset` (zero-based). It is much more efficient to use `SORTBY … MAX` if you\n  are interested in just limiting the output of a sort operation.\n  If a key expires during the query, an attempt to `load` the key's value will return a null array. \n\nHowever, limit can be used to limit results without sorting, or for paging the n-largest results as determined by `SORTBY MAX`. For example, getting results 50-100 of the top 100 results is most efficiently expressed as `SORTBY 1 @foo MAX 100 LIMIT 50 50`. Removing the `MAX` from `SORTBY` results in the pipeline sorting _all_ the records and then paging over results 50-100.\n</details>\n\n<details open>\n<summary><code>FILTER {expr}</code></summary> \n\nfilters the results using predicate expressions relating to values in each result.\n  They are applied post query and relate to the current state of the pipeline.\n</details>\n\n<details open>\n<summary><code>WITHCURSOR {COUNT} {read_size} [MAXIDLE {idle_time}]</code></summary> \n\nScan part of the results with a quicker alternative than `LIMIT`.\nSee [Cursor API](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations#cursor-api) for more details.\n</details>\n\n<details open>\n<summary><code>TIMEOUT {milliseconds}</code></summary> \n\nif set, overrides the timeout parameter of the module.\n</details>\n\n<details open>\n<summary><code>PARAMS {nargs} {name} {value}</code></summary> \n\ndefines one or more value parameters. Each parameter has a name and a value. \n\nYou can reference parameters in the `query` by a `$`, followed by the parameter name, for example, `$user`. Each such reference in the search query to a parameter name is substituted by the corresponding parameter value. For example, with parameter definition `PARAMS 4 lon 29.69465 lat 34.95126`, the expression `@loc:[$lon $lat 10 km]` is evaluated to `@loc:[29.69465 34.95126 10 km]`. You cannot reference parameters in the query string where concrete values are not allowed, such as in field names, for example, `@loc`. To use `PARAMS`, set `DIALECT` to `2` or greater than `2`.\n</details>\n\n<details open>\n<summary><code>SCORER {scorer}</code></summary>\n\nuses a [built-in](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/scoring) or a [user-provided](https://redis.io/docs/latest/develop/ai/search-and-query/administration/extensions) scoring function.\n</details>\n\n<details open>\n<summary><code>ADDSCORES</code></summary>\n\nThe `ADDSCORES` option exposes the full-text score values to the aggregation pipeline.\nYou can use `@__score` in a pipeline as shown in the following example:\n\n`FT.AGGREGATE idx 'hello' ADDSCORES SORTBY 2 @__score DESC`\n</details>\n\n<details open>\n<summary><code>DIALECT {dialect_version}</code></summary> \n\nselects the dialect version under which to execute the query. If not specified, the query will execute under the default dialect version set during module initial loading or via [`FT.CONFIG SET`](https://redis.io/docs/latest/commands/ft.config-set) command.\n</details>"
    },
    {
      "id": "return-multiple-values",
      "title": "Return multiple values",
      "role": "returns",
      "text": "See [Return multiple values](https://redis.io/docs/latest/commands/ft.search#return-multiple-values/) in [`FT.SEARCH`](https://redis.io/docs/latest/commands/ft.search)\nThe `DIALECT` can be specified as a parameter in the FT.AGGREGATE command. If it is not specified, the `DEFAULT_DIALECT` is used, which can be set using [`FT.CONFIG SET`](https://redis.io/docs/latest/commands/ft.config-set) or by passing it as an argument to the `redisearch` module when it is loaded.\nFor example, with the following document and index:\n\n[code example]\nNotice the different replies, with and without `DIALECT 3`:\n\n[code example]"
    },
    {
      "id": "complexity",
      "title": "Complexity",
      "role": "performance",
      "text": "Non-deterministic. Depends on the query and aggregations performed, but it is usually linear to the number of results returned."
    },
    {
      "id": "examples",
      "title": "Examples",
      "role": "example",
      "text": "<details open>\n<summary><b>Sort page visits by day</b></summary>\n\nFind visits to the page `about.html`, group them by the day of the visit, count the number of visits, and sort them by day.\n\n\nFT.AGGREGATE idx \"@url:\\\"about.html\\\"\"\n    APPLY \"day(@timestamp)\" AS day\n    GROUPBY 2 @day @country\n      REDUCE count 0 AS num_visits\n    SORTBY 1 @day\n\n</details>\n\n<details open>\n<summary><b>Find most books ever published</b></summary>\n\nFind most books ever published in a single year.\n\n\nFT.AGGREGATE books-idx *\n    GROUPBY 1 @published_year\n      REDUCE COUNT 0 AS num_published\n    GROUPBY 0\n      REDUCE MAX 1 @num_published AS max_books_published_per_year\n\n</details>\n\n<details open>\n<summary><b>Reduce all results</b></summary>\n\nThe last example used `GROUPBY 0`. Use `GROUPBY 0` to apply a `REDUCE` function over all results from the last step of an aggregation pipeline -- this works on both the  initial query and subsequent `GROUPBY` operations.\n\nSearch for libraries within 10 kilometers of the longitude -73.982254 and latitude 40.753181 then annotate them with the distance between their location and those coordinates.\n\n\n FT.AGGREGATE libraries-idx \"@location:[-73.982254 40.753181 10 km]\"\n    LOAD 1 @location\n    APPLY \"geodistance(@location, -73.982254, 40.753181)\"\n\n\nHere, notice the required use of `LOAD` to pre-load the `@location` attribute because it is a GEO attribute.    \n\nNext, count GitHub events by user (actor), to produce the most active users.\n\n\n127.0.0.1:6379> FT.AGGREGATE gh \"*\" GROUPBY 1 @actor REDUCE COUNT 0 AS num SORTBY 2 @num DESC MAX 10\n 1) (integer) 284784\n 2) 1) \"actor\"\n    2) \"lombiqbot\"\n    3) \"num\"\n    4) \"22197\"\n 3) 1) \"actor\"\n    2) \"codepipeline-test\"\n    3) \"num\"\n    4) \"17746\"\n 4) 1) \"actor\"\n    2) \"direwolf-github\"\n    3) \"num\"\n    4) \"10683\"\n 5) 1) \"actor\"\n    2) \"ogate\"\n    3) \"num\"\n    4) \"6449\"\n 6) 1) \"actor\"\n    2) \"openlocalizationtest\"\n    3) \"num\"\n    4) \"4759\"\n 7) 1) \"actor\"\n    2) \"digimatic\"\n    3) \"num\"\n    4) \"3809\"\n 8) 1) \"actor\"\n    2) \"gugod\"\n    3) \"num\"\n    4) \"3512\"\n 9) 1) \"actor\"\n    2) \"xdzou\"\n    3) \"num\"\n    4) \"3216\"\n[10](10)) 1) \"actor\"\n    2) \"opstest\"\n    3) \"num\"\n    4) \"2863\"\n11) 1) \"actor\"\n    2) \"jikker\"\n    3) \"num\"\n    4) \"2794\"\n(0.59s)\n\n\n</details>\n\n<details open>\n<summary><b>Use the case function for conditional logic</b></summary>\n\n//Simple mapping\nFT.AGGREGATE products \"*\"\nAPPLY case(@price > 100, \"premium\", \"standard\") AS category\n\n//Nested conditions where an error should be returned\nFT.AGGREGATE orders \"*\"\nAPPLY case(@status == \"pending\", \n           case(@priority == \"high\", 1, 2), \n           case(@status == \"completed\", 3, 4)) AS status_code\n\n//Mapped approach\nFT.AGGREGATE orders \"*\"\nAPPLY case(@status == \"pending\", 1, 0) AS is_pending\nAPPLY case(@is_pending == 1 && @priority == \"high\", 1,2) AS status_high\nAPPLY case(@is_pending == 0 && @priority == \"high\", 3,4) AS status_completed\n\n\n</details>\n\n<details open>\n<summary><b>Collect top documents per group</b></summary>\n\nGroup movies by genre and collect the top 5 movies per group, sorted by rating in descending order. Return the top 50 groups.\n\n\nFT.AGGREGATE idx:movies \"*\"\n    LOAD 4 @genre @title @rating @year\n    GROUPBY 1 @genre\n      REDUCE COLLECT 12 FIELDS 3 @title @rating @year SORTBY 2 @rating DESC LIMIT 0 5 AS top_movies\n    SORTBY 1 @genre LIMIT 0 50\n\n\nIn this example:\n- `LOAD` brings `@genre`, `@title`, `@rating`, and `@year` into the pipeline. `COLLECT` projects from fields already available in the pipeline; it does not trigger an implicit document load. Using `FIELDS *` in place of the explicit field list would return only the fields the pipeline already has, as controlled by `LOAD` and any field-generating operations such as `APPLY`.\n- `FIELDS 3 @title @rating @year` projects those three fields into each collected entry.\n- Duplicates are retained (default behavior, no `DISTINCT` flag).\n- Documents within each group are sorted by `@rating` descending.\n- Only the top 5 documents per group are returned.\n- Only the top 50 groups are returned.\n</details>"
    },
    {
      "id": "redis-software-and-redis-cloud-compatibility",
      "title": "Redis Software and Redis Cloud compatibility",
      "role": "content",
      "text": "| Redis<br />Software | Redis Cloud<br />Flexible & Annual | Redis Cloud<br />Free & Fixed | <span style=\"min-width: 9em; display: table-cell\">Notes</span> |\n|:----------------------|:-----------------|:-----------------|:------|\n| <span title=\"Supported\">&#x2705; Supported</span> | <span title=\"Supported\">&#x2705; Supported</span> | <span title=\"Supported\">&#x2705; Supported</nobr></span> |  |"
    },
    {
      "id": "return-information",
      "title": "Return information",
      "role": "returns",
      "text": "**RESP2:**\n\nOne of the following:\n* [Array reply](https://redis.io/docs/latest/develop/reference/protocol-spec#arrays) where each row is an array reply representing a single aggregate result. The [integer reply](https://redis.io/docs/latest/develop/reference/protocol-spec#resp-integers) at position `1` does not represent a valid value.\n* [Simple error reply](https://redis.io/docs/latest/develop/reference/protocol-spec#simple-errors) in these cases: incorrect number of arguments, non-existent index, invalid query syntax.\n\n**RESP3:**\n\nOne of the following:\n* [Map](https://redis.io/docs/latest/develop/reference/protocol-spec#maps) with the following fields:\n    - `attributes`: [Array](https://redis.io/docs/latest/develop/reference/protocol-spec#arrays) of attribute names.\n    - `format`: [Simple string](https://redis.io/docs/latest/develop/reference/protocol-spec#simple-strings) - result format.\n    - `results`: [Array](https://redis.io/docs/latest/develop/reference/protocol-spec#arrays) of [maps](https://redis.io/docs/latest/develop/reference/protocol-spec#maps) containing aggregated data.\n    - `total_results`: [Integer](https://redis.io/docs/latest/develop/reference/protocol-spec#integers) - total number of results.\n    - `warning`: [Array](https://redis.io/docs/latest/develop/reference/protocol-spec#arrays) of warning messages.\n* [Simple error reply](https://redis.io/docs/latest/develop/reference/protocol-spec#simple-errors) in these cases: incorrect number of arguments, non-existent index, invalid query syntax."
    },
    {
      "id": "see-also",
      "title": "See also",
      "role": "related",
      "text": "[`FT.CONFIG SET`](https://redis.io/docs/latest/commands/ft.config-set) | [`FT.SEARCH`](https://redis.io/docs/latest/commands/ft.search)"
    },
    {
      "id": "related-topics",
      "title": "Related topics",
      "role": "related",
      "text": "- [Aggregations](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations)\n- [Key and field expiration behavior](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/expiration)\n- [RediSearch](https://redis.io/docs/latest/develop/ai/search-and-query)\n- [Search commands in MULTI/EXEC transactions and Lua scripts](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/transactions)"
    }
  ],
  "examples": [
    {
      "id": "return-multiple-values-ex0",
      "language": "sh",
      "code": "127.0.0.1:6379> JSON.SET doc:1 $ '[{\"arr\": [1, 2, 3]}, {\"val\": \"hello\"}, {\"val\": \"world\"}]'\nOK\n127.0.0.1:6379> FT.CREATE idx ON JSON PREFIX 1 doc: SCHEMA $..arr AS arr NUMERIC $..val AS val TEXT\nOK",
      "section_id": "return-multiple-values"
    },
    {
      "id": "return-multiple-values-ex1",
      "language": "sh",
      "code": "127.0.0.1:6379> FT.AGGREGATE idx * LOAD 2 arr val \n1) (integer) 1\n2) 1) \"arr\"\n   2) \"[1,2,3]\"\n   3) \"val\"\n   4) \"hello\"\n\n127.0.0.1:6379> FT.AGGREGATE idx * LOAD 2 arr val DIALECT 3\n1) (integer) 1\n2) 1) \"arr\"\n   2) \"[[1,2,3]]\"\n   3) \"val\"\n   4) \"[\\\"hello\\\",\\\"world\\\"]\"",
      "section_id": "return-multiple-values"
    }
  ]
}
