# SEARCH.AGGREGATE

> Compute analytics over matching documents.

Use `SEARCH.AGGREGATE` to compute metrics and buckets over matching documents.

The command takes two JSON arguments. The first is a filter, in the same language as [`SEARCH.QUERY`](/redis/commands/search/search-query), which selects the documents to aggregate; pass `'{}'` to cover the whole index. The second describes the aggregations to compute, as named entries such as `{"avg_price": {"$avg": {"field": "price"}}}`.

Metric operators like `$avg`, `$sum`, `$min`, `$max`, `$stats`, and `$cardinality` reduce the selected documents to a single number, while bucket operators like `$terms`, `$range`, `$histogram`, and `$dateHistogram` group them and report a count per bucket. Bucket operators accept nested `$aggs`, so you can compute a metric inside each bucket, for example the average price per category, and several aggregations can be requested in one call since they all run over the same selected document set.

`$composite` buckets by several sources at once, producing one bucket per combination of their values, and pages through the result instead of returning it whole. Each entry in `sources` names a source and gives it a `$terms`, `$histogram`, or `$dateHistogram` definition over a `FAST` field. `size` sets the page size, defaulting to `10`, and the reply carries an `afterKey`; passing that value back as `after` returns the next page. This is how you enumerate a large cross-product of buckets, which `$terms` alone cannot do because it returns only its top terms.

By default `$sum` reports `0` for a bucket that matched no documents, matching Elasticsearch. Setting `"nullIfNoMatch": true` makes it report `null` instead, which separates "nothing matched" from a total that genuinely came to zero. It only affects empty buckets; a sum over matching documents is unchanged.

See [Aggregations](/redis/search/aggregations) for the full operator reference and examples.

## Syntax

```redis
SEARCH.AGGREGATE <name> '<json_filter>' '<json_aggregations>'
```

`<json_filter>` is an [Upstash JSON filter](/redis/search/querying). The command accepts an index name or alias. See [Aggregations](/redis/search/aggregations) for the aggregation object and supported operators.

## Response

In `redis-cli --json`, the response is an object keyed by aggregation alias:

```json
{
  "avg_price": { "value": 49.99 },
  "by_category": { "buckets": [{ "key": "electronics", "docCount": 42 }] }
}
```

Raw RESP output may be rendered differently by client or protocol settings, but each top-level alias maps to its aggregation result. Returns `null` if the index does not exist.

A `$composite` aggregation replies with its buckets and the cursor to resume from:

```json
{
  "by_cat_and_price": {
    "buckets": [
      { "key": { "by_cat": "books", "by_price": 20 }, "docCount": 1 }
    ],
    "afterKey": { "by_cat": "str:books", "by_price": "f64:20" }
  }
}
```

Pass the `afterKey` object back verbatim as the aggregation's `after` option to get the next page.

<Note>
  A boolean `false` in an aggregation result is encoded as `0` under RESP2 and over the REST API, and as a boolean under RESP3. Earlier versions encoded it as null under RESP2.
</Note>

## Examples

<AccordionGroup>

<Accordion title="Redis CLI" icon="terminal">

```bash
SEARCH.AGGREGATE products '{}' '{"avg_price": {"$avg": {"field": "price"}}}'

# Page through every category and price bucket combination
SEARCH.AGGREGATE products '{}' '{"by_cat_and_price": {"$composite": {"size": 100, "sources": [{"by_cat": {"$terms": {"field": "category"}}}, {"by_price": {"$histogram": {"field": "price", "interval": 10}}}]}}}'
```

</Accordion>

<Accordion title="@upstash/redis" icon="node-js" iconType="brands">

```ts
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();
const products = redis.search.index({ name: "products" });

const result = await products.aggregate({
  aggregations: {
    avg_price: { $avg: { field: "price" } },
  },
});
```

</Accordion>

<Accordion title="upstash_redis" icon="python" iconType="brands">

```python
from upstash_redis import Redis

redis = Redis.from_env()
products = redis.search.index(name="products")

result = products.aggregate(
    aggregations={"avg_price": {"$avg": {"field": "price"}}},
)
```

</Accordion>

<Accordion title="ioredis" icon="node-js" iconType="brands">

```ts
import IORedis from "ioredis";
import { createSearch } from "@upstash/search-ioredis";

const redis = new IORedis(process.env.REDIS_URL!);
const search = createSearch(redis);
const products = search.index({ name: "products" });

const result = await products.aggregate({
  aggregations: {
    avg_price: { $avg: { field: "price" } },
  },
});
```

</Accordion>

<Accordion title="node-redis" icon="node-js" iconType="brands">

```ts
import { createClient } from "redis";
import { createSearch } from "@upstash/search-redis";

const client = await createClient({ url: process.env.REDIS_URL })
  .on("error", console.error)
  .connect();
const search = createSearch(client);
const products = search.index({ name: "products" });

const result = await products.aggregate({
  aggregations: {
    avg_price: { $avg: { field: "price" } },
  },
});
```

</Accordion>

<Accordion title="redis-py" icon="python" iconType="brands">

```python
import os
import redis

client = redis.from_url(os.environ["REDIS_URL"])
result = client.execute_command(
    "SEARCH.AGGREGATE",
    "products",
    "{}",
    "{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}",
)
print(result)
```

</Accordion>

<Accordion title="go-redis" icon="golang" iconType="brands">

```go
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/redis/go-redis/v9"
)

func main() {
    opts, err := redis.ParseURL(os.Getenv("REDIS_URL"))
    if err != nil {
        panic(err)
    }
    client := redis.NewClient(opts)
    result, err := client.Do(
        context.Background(),
        "SEARCH.AGGREGATE",
        "products",
        "{}",
        "{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}",
    ).Result()
    if err != nil {
        panic(err)
    }
    fmt.Println(result)
}
```

</Accordion>

<Accordion title="jedis" icon="java" iconType="brands">

```java
import java.net.URI;

import redis.clients.jedis.Jedis;

try (Jedis jedis = new Jedis(new URI(System.getenv("REDIS_URL")))) {
  Object result = jedis.sendCommand(
      () -> "SEARCH.AGGREGATE".getBytes(),
      "products",
      "{}",
      "{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}"
  );
  System.out.println(result);
}
```

</Accordion>

<Accordion title="redis-rs" icon="rust" iconType="brands">

```rust
fn main() -> redis::RedisResult<()> {
    let url = std::env::var("REDIS_URL").expect("REDIS_URL is not set");
    let client = redis::Client::open(url)?;
    let mut connection = client.get_connection()?;

    let mut command = redis::cmd("SEARCH.AGGREGATE");
    command.arg("products");
    command.arg("{}");
    command.arg("{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}");
    let result: redis::Value = command.query(&mut connection)?;
    println!("{result:?}");
    Ok(())
}
```

</Accordion>

<Accordion title="curl">

```bash
curl -X POST https://YOUR_ENDPOINT.upstash.io \
  -H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN" \
  -d '["SEARCH.AGGREGATE", "products", "{}", "{\"avg_price\": {\"$avg\": {\"field\": \"price\"}}}"]'
```

</Accordion>

</AccordionGroup>
