{
  "id": "aggregation",
  "title": "Aggregate your data",
  "url": "https://redis.io/docs/latest/develop/get-started/search-tutorial/aggregation/",
  "summary": "Use FT.AGGREGATE to group, summarize, and transform your data with GROUPBY, REDUCE, and APPLY.",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc",
    "oss",
    "kubernetes",
    "clients"
  ],
  "last_updated": "2026-08-03T12:11:45-07:00",
  "page_type": "content",
  "content_hash": "881a2b74c6f897662edf9147f1d58700be8de4825d4b8f262918e8694aec928b",
  "sections": [
    {
      "id": "count-documents-per-group",
      "title": "Count documents per group",
      "role": "content",
      "text": "The most common aggregation is a grouped count. This groups every product by `category` and counts how many fall into each. `REDUCE COUNT 0` counts the documents in each group, and `AS count` names the result:\n\nGrouped count: Use GROUPBY with REDUCE COUNT to count documents in each group\n\n**Difficulty:** Beginner\n\n**Commands:** FT.AGGREGATE\n\n**Complexity:**\n- FT.AGGREGATE: O(1)\n\n**Available in:** Redis CLI, Python\n\n##### Redis CLI\n\n[code example]\n\n##### Python\n\n[code example]\n\n\n\nThe `\"*\"` after the index name is a query expression, exactly like in `FT.SEARCH`. Here it means \"aggregate over all documents\", but you could narrow the input first, for example `@price:[0 100]` to aggregate only the cheaper products. `GROUPBY 1 @category` reads as \"group by one field: `category`\"."
    },
    {
      "id": "compute-an-average-per-group",
      "title": "Compute an average per group",
      "role": "content",
      "text": "Swap `COUNT` for a different reducer to compute other summaries. This calculates the average price in each category and sorts the groups from most to least expensive with `SORTBY`:\n\nGrouped average: Use REDUCE AVG to average a numeric field per group, then order groups with SORTBY\n\n**Difficulty:** Intermediate\n\n**Commands:** FT.AGGREGATE\n\n**Complexity:**\n- FT.AGGREGATE: O(1)\n\n**Available in:** Redis CLI, Python\n\n##### Redis CLI\n\n[code example]\n\n##### Python\n\n[code example]\n\n\n\n`REDUCE AVG 1 @price` reads as \"apply the AVG reducer to one field: `price`\". The `SORTBY 2 @avg_price DESC` clause sorts by the computed `avg_price` value; the `2` is the number of arguments that follow (`@avg_price` and `DESC`). Other reducers include `SUM`, `MIN`, `MAX`, and `COUNT_DISTINCT`; see the [aggregation reference](https://redis.io/docs/latest/develop/ai/search-and-query/advanced-concepts/aggregations) for the full list."
    },
    {
      "id": "calculate-new-values-with-apply",
      "title": "Calculate new values with APPLY",
      "role": "content",
      "text": "`APPLY` evaluates an expression against each record and adds the result as a new field. This takes the Audio products, loads their name and price, and computes a 10%-off `sale_price`:\n\nCalculated field: Use APPLY to derive a new value (a discounted price) from an existing field\n\n**Difficulty:** Intermediate\n\n**Commands:** FT.AGGREGATE\n\n**Complexity:**\n- FT.AGGREGATE: O(1)\n\n**Available in:** Redis CLI, Python\n\n##### Redis CLI\n\n[code example]\n\n##### Python\n\n[code example]\n\n\n\nThe `LOAD 2 name price` clause pulls those two fields into the pipeline so the expression can use them and so they appear in the output. `APPLY` does not group anything; it transforms each record in place."
    },
    {
      "id": "build-a-pipeline",
      "title": "Build a pipeline",
      "role": "content",
      "text": "The real power of `FT.AGGREGATE` is chaining these steps. This finds the average rating per brand and returns the highest-rated brands first &mdash; a simple \"best brands\" leaderboard:\n\nPipeline: Combine GROUPBY, REDUCE, and SORTBY to rank brands by average rating\n\n**Difficulty:** Intermediate\n\n**Commands:** FT.AGGREGATE\n\n**Complexity:**\n- FT.AGGREGATE: O(1)\n\n**Available in:** Redis CLI, Python\n\n##### Redis CLI\n\n[code example]\n\n##### Python\n\n[code example]\n\n\n\n(The output is truncated; eight brands are returned in all.) You can keep extending the pipeline &mdash; apply multiple reducers under one `GROUPBY`, chain a second `GROUPBY`, add `FILTER` and `LIMIT` steps, and more. See the [aggregation queries](https://redis.io/docs/latest/develop/ai/search-and-query/query/aggregation) guide for deeper examples.\n\n\nAggregation results are tabular by nature, so they are especially easy to read in the [Redis Insight Search workspace](https://redis.io/docs/latest/develop/tools/insight/search-workspace). Paste any `FT.AGGREGATE` command from this page into the query editor to see each group as a row."
    },
    {
      "id": "next-steps",
      "title": "Next steps",
      "role": "content",
      "text": "You can now find, filter, and summarize structured data. The final step goes beyond keywords and exact values to search by *meaning*. Continue to [vector and hybrid search](https://redis.io/docs/latest/develop/get-started/search-tutorial/vector-search)."
    }
  ],
  "examples": [
    {
      "id": "count-documents-per-group-ex0",
      "language": "plaintext",
      "code": "> FT.AGGREGATE idx:catalog \"*\" GROUPBY 1 @category REDUCE COUNT 0 AS count\n1) (integer) 6\n2) 1) \"category\"\n   2) \"Audio\"\n   3) \"count\"\n   4) \"3\"\n3) 1) \"category\"\n   2) \"Computers\"\n   3) \"count\"\n   4) \"2\"\n4) 1) \"category\"\n   2) \"Accessories\"\n   3) \"count\"\n   4) \"2\"\n5) 1) \"category\"\n   2) \"Home\"\n   3) \"count\"\n   4) \"2\"\n6) 1) \"category\"\n   2) \"Wearables\"\n   3) \"count\"\n   4) \"2\"\n7) 1) \"category\"\n   2) \"Cameras\"\n   3) \"count\"\n   4) \"1\"",
      "section_id": "count-documents-per-group"
    },
    {
      "id": "count-documents-per-group-ex1",
      "language": "python",
      "code": "req = aggregations.AggregateRequest(\"*\").group_by(\n    \"@category\", reducers.count().alias(\"count\")\n)\nres = index.aggregate(req).rows\nprint(res)\n# >>> [['category', 'Audio', 'count', '3'], ['category', 'Computers', 'count', '2'], ...]",
      "section_id": "count-documents-per-group"
    },
    {
      "id": "compute-an-average-per-group-ex0",
      "language": "plaintext",
      "code": "> FT.AGGREGATE idx:catalog \"*\" GROUPBY 1 @category REDUCE AVG 1 @price AS avg_price SORTBY 2 @avg_price DESC\n1) (integer) 6\n2) 1) \"category\"\n   2) \"Computers\"\n   3) \"avg_price\"\n   4) \"864.495\"\n3) 1) \"category\"\n   2) \"Cameras\"\n   3) \"avg_price\"\n   4) \"299\"\n4) 1) \"category\"\n   2) \"Wearables\"\n   3) \"avg_price\"\n   4) \"164.495\"\n5) 1) \"category\"\n   2) \"Audio\"\n   3) \"avg_price\"\n   4) \"139.826666667\"\n6) 1) \"category\"\n   2) \"Accessories\"\n   3) \"avg_price\"\n   4) \"89.495\"\n7) 1) \"category\"\n   2) \"Home\"\n   3) \"avg_price\"\n   4) \"86.995\"",
      "section_id": "compute-an-average-per-group"
    },
    {
      "id": "compute-an-average-per-group-ex1",
      "language": "python",
      "code": "req = (\n    aggregations.AggregateRequest(\"*\")\n    .group_by(\"@category\", reducers.avg(\"@price\").alias(\"avg_price\"))\n    .sort_by(aggregations.Desc(\"@avg_price\"))\n)\nres = index.aggregate(req).rows\nprint(res)\n# >>> [['category', 'Computers', 'avg_price', '864.495'], ...]",
      "section_id": "compute-an-average-per-group"
    },
    {
      "id": "calculate-new-values-with-apply-ex0",
      "language": "plaintext",
      "code": "> FT.AGGREGATE idx:catalog \"@category:{Audio}\" LOAD 2 name price APPLY \"@price - (@price * 0.1)\" AS sale_price\n1) (integer) 3\n2) 1) \"name\"\n   2) \"Aurora AcousticPro Headphones\"\n   3) \"price\"\n   4) \"199.99\"\n   5) \"sale_price\"\n   6) \"179.991\"\n3) 1) \"name\"\n   2) \"Sonus Boom Portable Speaker\"\n   3) \"price\"\n   4) \"129.5\"\n   5) \"sale_price\"\n   6) \"116.55\"\n4) 1) \"name\"\n   2) \"Aurora BudsMini Earbuds\"\n   3) \"price\"\n   4) \"89.99\"\n   5) \"sale_price\"\n   6) \"80.991\"",
      "section_id": "calculate-new-values-with-apply"
    },
    {
      "id": "calculate-new-values-with-apply-ex1",
      "language": "python",
      "code": "req = (\n    aggregations.AggregateRequest(\"@category:{Audio}\")\n    .load(\"name\", \"price\")\n    .apply(sale_price=\"@price - (@price * 0.1)\")\n)\nres = index.aggregate(req).rows\nprint(res)\n# >>> [['name', 'Aurora AcousticPro Headphones', 'price', '199.99', 'sale_price', '179.991'], ...]",
      "section_id": "calculate-new-values-with-apply"
    },
    {
      "id": "build-a-pipeline-ex0",
      "language": "plaintext",
      "code": "> FT.AGGREGATE idx:catalog \"*\" GROUPBY 1 @brand REDUCE AVG 1 @rating AS avg_rating SORTBY 2 @avg_rating DESC\n1) (integer) 8\n2) 1) \"brand\"\n   2) \"Clackr\"\n   3) \"avg_rating\"\n   4) \"4.8\"\n3) 1) \"brand\"\n   2) \"Pixma\"\n   3) \"avg_rating\"\n   4) \"4.55\"\n4) 1) \"brand\"\n   2) \"Sonus\"\n   3) \"avg_rating\"\n   4) \"4.5\"\n5) 1) \"brand\"\n   2) \"Aurora\"\n   3) \"avg_rating\"\n   4) \"4.45\"",
      "section_id": "build-a-pipeline"
    },
    {
      "id": "build-a-pipeline-ex1",
      "language": "python",
      "code": "req = (\n    aggregations.AggregateRequest(\"*\")\n    .group_by(\"@brand\", reducers.avg(\"@rating\").alias(\"avg_rating\"))\n    .sort_by(aggregations.Desc(\"@avg_rating\"))\n)\nres = index.aggregate(req).rows\nprint(res)\n# >>> [['brand', 'Clackr', 'avg_rating', '4.8'], ['brand', 'Pixma', 'avg_rating', '4.55'], ...]",
      "section_id": "build-a-pipeline"
    }
  ]
}
