Метрические и бакетные агрегации

До сих пор Elasticsearch работал у нас как поисковик: принимал текст или вектор, возвращал документы. Но те же данные можно использовать совсем иначе — считать статистику, строить распределения, группировать по категориям. Всё это делают агрегации (aggregations).

По-простому: агрегации в ES — это то, что GROUP BY плюс агрегатные функции делают в SQL, только прямо поверх тех же индексов, по которым вы ищете.

Все агрегации делятся на два базовых класса:

  • Metric-агрегации — вычисляют одно число (или несколько) по набору документов: среднее, сумму, максимум и т. д.
  • Bucket-агрегации — делят документы на группы («бакеты»). Каждый бакет — подмножество документов. Внутрь бакетов можно вложить метрики или другие бакеты.
flowchart TD A["Aggregations"] --> B["Metric\nВозвращают числовое значение"] A --> C["Bucket\nДелят документы на группы"] B --> B1["avg · sum · min · max"] B --> B2["stats\n(все пять метрик сразу)"] B --> B3["cardinality\n(≈ уникальных значений)"] C --> C1["terms\nпо значению keyword-поля"] C --> C2["range\nпо числовым диапазонам"] C --> C3["date_histogram\nпо временным интервалам"] C -.->|"+ sub-aggregations"| B
flowchart TD
    A["Aggregations"] --> B["Metric\nВозвращают числовое значение"]
    A --> C["Bucket\nДелят документы на группы"]
    B --> B1["avg · sum · min · max"]
    B --> B2["stats\n(все пять метрик сразу)"]
    B --> B3["cardinality\n(≈ уникальных значений)"]
    C --> C1["terms\nпо значению keyword-поля"]
    C --> C2["range\nпо числовым диапазонам"]
    C --> C3["date_histogram\nпо временным интервалам"]
    C -.->|"+ sub-aggregations"| B
Два класса агрегаций: metric возвращают число, bucket делят документы на группы. Бакеты могут содержать вложенные metric-агрегации.

Запрос только за агрегациями: "size": 0

Когда вам нужна аналитика, а не сами документы, добавьте в запрос "size": 0. Тогда ES вернёт только блок "aggregations" и не потратит время на сериализацию документов:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "имя_агрегации": { ... }
  }
}

Без "size": 0 вы получите и 10 документов (по умолчанию), и агрегации — двойная работа без пользы.

> Ключ для объявления агрегаций — "aggs" (или полная форма "aggregations"). Оба варианта работают одинаково.

Check yourself
Зачем нужен `"size": 0` в агрегационном запросе? Что именно изменится в ответе ES, если его не добавить?

Metric-агрегации: числа по документам

#### avg, sum, min, max

Стандартные математические функции. Пример: считаем сразу четыре метрики по полю price в одном запросе:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "средняя_цена":   { "avg": { "field": "price" } },
    "суммарная_цена": { "sum": { "field": "price" } },
    "минимальная":    { "min": { "field": "price" } },
    "максимальная":   { "max": { "field": "price" } }
  }
}

Имена агрегаций ("средняя_цена" и т. д.) — произвольные строки. ES использует их как ключи в ответе:

{
  "aggregations": {
    "средняя_цена":   { "value": 1240.5 },
    "суммарная_цена": { "value": 248100.0 },
    "минимальная":    { "value": 199.0 },
    "максимальная":   { "value": 9999.0 }
  }
}

#### stats — всё сразу

Если нужны $\min$, $\max$, $\text{avg}$, $\text{sum}$ и $\text{count}$ одним запросом — используйте stats:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "статистика_цены": { "stats": { "field": "price" } }
  }
}

Ответ:

{
  "aggregations": {
    "статистика_цены": {
      "count": 200,
      "min":   199.0,
      "max":   9999.0,
      "avg":   1240.5,
      "sum":   248100.0
    }
  }
}

stats — удобный инструмент для быстрой диагностики: один вызов вместо четырёх отдельных агрегаций.

#### cardinality — количество уникальных значений

Аналог COUNT(DISTINCT field) в SQL. Реализован через алгоритм HyperLogLog++, поэтому результат приближённый — погрешность порядка $1$$5\%$ при настройках по умолчанию:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "уникальные_клиенты": { "cardinality": { "field": "customer_id" } }
  }
}

Для повышения точности используйте "precision_threshold" (максимум $40\,000$), но это дороже по памяти. На миллионах уникальных значений приближённый результат за миллисекунды — обычно именно то, что нужно.

Check yourself
Какая metric-агрегация возвращает сразу $\min$, $\max$, $\text{avg}$, $\text{sum}$ и $\text{count}$ в одном ответе? И почему `cardinality` даёт приближённый, а не точный результат?

Bucket-агрегации: документы по группам

Bucket-агрегации не возвращают одно число — они делят документы на корзины. В каждом бакете есть поле doc_count — количество документов, попавших в него.

#### terms — группировка по значению поля

Самая частая агрегация. Разбивает документы по уникальным значениям поля:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "по_категории": {
      "terms": { "field": "category", "size": 10 }
    }
  }
}

> Важно: поле category должно быть типа keyword (или text с sub-field .keyword). По text-полям terms-агрегация не работает корректно — там хранятся отдельные токены, а не исходные строки. Подробнее — в статье Типы полей: text, keyword, числа и даты.

Пример ответа:

{
  "aggregations": {
    "по_категории": {
      "buckets": [
        { "key": "электроника", "doc_count": 520 },
        { "key": "одежда",      "doc_count": 310 },
        { "key": "книги",       "doc_count": 180 }
      ]
    }
  }
}

Параметр "size": 10 задаёт, сколько топ-бакетов вернуть. Это не ограничение на документы — это количество уникальных значений в ответе.

#### range — числовые диапазоны

Разбивает по произвольным числовым границам. Каждый диапазон — полуоткрытый интервал: from включительно, to не включительно:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "ценовые_диапазоны": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "бюджетный",   "to": 500 },
          { "key": "средний",     "from": 500, "to": 2000 },
          { "key": "премиальный", "from": 2000 }
        ]
      }
    }
  }
}

Параметр "key" даёт удобное имя бакету вместо автоматически генерируемого "*-500.0".

#### date_histogram — временные срезы

Группирует по временным интервалам — незаменима для построения графиков динамики:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "заказы_по_месяцам": {
      "date_histogram": {
        "field":             "created_at",
        "calendar_interval": "month",
        "format":            "yyyy-MM"
      }
    }
  }
}

calendar_interval принимает: minute, hour, day, week, month, quarter, year. Для фиксированных промежутков — например, каждые $12$ часов — используйте "fixed_interval": "12h".

Пример ответа:

{
  "aggregations": {
    "заказы_по_месяцам": {
      "buckets": [
        { "key_as_string": "2024-01", "doc_count": 341 },
        { "key_as_string": "2024-02", "doc_count": 289 },
        { "key_as_string": "2024-03", "doc_count": 412 }
      ]
    }
  }
}
Check yourself
Вам нужно посмотреть, сколько заказов было каждую неделю за последний год. Какую агрегацию использовать? Как будет выглядеть ключевой параметр?

Первый шаг к вложенности

Bucket-агрегации становятся по-настоящему мощными, когда внутрь бакетов вкладываются другие агрегации. Например, terms по категориям $+$ avg по цене даёт среднюю цену в каждой категории за один запрос. Это тема статьи Вложенные агрегации и аналитические сценарии.



Quick recall
Сколько операций заменяет одна `stats`-агрегация?
Quick recall
Какой SQL-конструкции соответствует агрегация в Elasticsearch?

См. также

Quick recall
Зачем добавлять `"size": 0` в запрос с агрегациями?