Elasticsearch как аналитический движок: агрегации, дашборды и замена ClickHouse для средних объёмов
Elasticsearch традиционно воспринимается как поисковый движок. Однако в 2026 году всё больше команд рассматривают его как аналитический инструмент для бизнес-аналитики и дашбордов при объёмах данных до 1 ТБ. В этой статье разберём, насколько оправдан такой подход, и дадим конкретные примеры запросов, советы по оптимизации и честное сравнение с конкурентами.
1. Elasticsearch как аналитический инструмент: мифы и реальность
Существует распространённый миф: Elasticsearch — это только full-text search, а для аналитики нужен ClickHouse или Druid. Реальность сложнее. Elasticsearch обладает мощным движком агрегаций, который покрывает большинство сценариев OLAP при объёмах до нескольких сотен гигабайт. При этом вы получаете единую инфраструктуру для поиска и аналитики, что снижает операционную сложность.
Ключевые преимущества Elasticsearch как аналитического движка:
- Встроенные агрегации без дополнительного слоя трансформации
- Нативная интеграция с Kibana для построения дашбордов
- Горизонтальное масштабирование через шардирование
- Гибкий маппинг для полуструктурированных данных
- REST API без необходимости в SQL-диалекте (хотя SQL-интерфейс тоже есть)
Главные ограничения: Elasticsearch не является колоночным хранилищем в классическом смысле, а JOIN между индексами не поддерживается на уровне запросов. Для тяжёлых аналитических нагрузок с миллиардами строк и сложными многотабличными запросами ClickHouse выиграет. Но для средних объёмов и смешанных нагрузок Elasticsearch — вполне легитимный выбор.
2. Агрегации в Elasticsearch: terms, date_histogram, nested, pipeline aggregations
Агрегации — сердце аналитического движка Elasticsearch. Они делятся на три категории: метрические (metric), бакетные (bucket) и конвейерные (pipeline). Рассмотрим ключевые типы с примерами.
Terms aggregation — группировка по значению
Аналог GROUP BY в SQL. Используется для подсчёта уникальных значений поля:
POST /orders/_search
{
"size": 0,
"aggs": {
"by_country": {
"terms": {
"field": "country.keyword",
"size": 20
},
"aggs": {
"total_revenue": {
"sum": {
"field": "amount"
}
},
"avg_order": {
"avg": {
"field": "amount"
}
}
}
}
}
}
Параметр size определяет количество возвращаемых бакетов. При больших кардинальностях используйте composite агрегацию для пагинации.
Date histogram — временные ряды
Ключевой инструмент для временной аналитики. date_histogram группирует документы по временным интервалам:
POST /events/_search
{
"size": 0,
"query": {
"range": {
"timestamp": {
"gte": "2025-01-01",
"lte": "2025-12-31"
}
}
},
"aggs": {
"events_over_time": {
"date_histogram": {
"field": "timestamp",
"calendar_interval": "day",
"format": "yyyy-MM-dd",
"min_doc_count": 0
},
"aggs": {
"unique_users": {
"cardinality": {
"field": "user_id",
"precision_threshold": 1000
}
}
}
}
}
}
Параметр min_doc_count: 0 гарантирует наличие нулевых значений для дней без событий — критично для графиков временных рядов.
Nested aggregations — работа со вложенными объектами
Для анализа вложенных структур (например, позиций в заказе) используется nested агрегация:
POST /orders/_search
{
"size": 0,
"aggs": {
"items_analysis": {
"nested": {
"path": "items"
},
"aggs": {
"by_category": {
"terms": {
"field": "items.category.keyword"
},
"aggs": {
"item_revenue": {
"sum": {
"field": "items.price"
}
}
}
}
}
}
}
}
Pipeline aggregations — аналитика поверх агрегаций
Pipeline aggregations работают с результатами других агрегаций. Это мощный инструмент для скользящих средних, производных метрик и аномалий:
POST /sales/_search
{
"size": 0,
"aggs": {
"monthly_sales": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
},
"aggs": {
"total": {
"sum": { "field": "amount" }
},
"moving_avg": {
"moving_avg": {
"buckets_path": "total",
"window": 3,
"model": "simple"
}
},
"month_over_month": {
"derivative": {
"buckets_path": "total"
}
}
}
}
}
}
Pipeline aggregations типа derivative, cumulative_sum и bucket_script позволяют строить сложные аналитические выражения без постобработки на стороне приложения.
3. Проектирование индексов для аналитики
Правильное проектирование индексов критично для производительности аналитического движка Elasticsearch. Ключевые принципы:
Маппинг для аналитики
Для строковых полей, используемых в агрегациях, используйте тип keyword, а не text. Числовые поля должны иметь правильный числовой тип:
PUT /analytics_events
{
"mappings": {
"properties": {
"timestamp": { "type": "date", "format": "strict_date_time" },
"user_id": { "type": "keyword" },
"session_id": { "type": "keyword" },
"event_type": { "type": "keyword" },
"page": { "type": "keyword" },
"duration_ms": { "type": "integer" },
"revenue": { "type": "scaled_float", "scaling_factor": 100 },
"properties": {
"type": "object",
"dynamic": false
}
}
},
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.codec": "best_compression",
"index.refresh_interval": "30s"
}
}
Тип scaled_float экономит место по сравнению с double и подходит для финансовых данных. Установка dynamic: false для свободных объектов предотвращает взрывной рост маппинга.
Динамические шаблоны
Для логов и событий с непредсказуемой структурой используйте dynamic_templates:
PUT /logs
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword",
"ignore_above": 256
}
}
},
{
"longs_as_integers": {
"match_mapping_type": "long",
"mapping": { "type": "integer" }
}
}
]
}
}
Оптимизация хранения
- Используйте ILM (Index Lifecycle Management) для автоматического перехода от горячих к холодным индексам
- Применяйте
force_mergeдля read-only индексов: снижает количество сегментов и ускоряет агрегации - Отключайте
_sourceдля индексов, где не нужен доступ к исходным документам (только агрегации) - Используйте сжатие
best_compressionдля аналитических индексов
4. Построение аналитических запросов: сессионный анализ, воронки, когортный анализ
Сессионный анализ
Анализ пользовательских сессий — типичный кейс для аналитического движка Elasticsearch. Запрос для подсчёта средней длины сессии и количества событий:
POST /events/_search
{
"size": 0,
"aggs": {
"by_session": {
"terms": {
"field": "session_id",
"size": 10000
},
"aggs": {
"session_duration": {
"max": { "field": "timestamp" }
},
"session_start": {
"min": { "field": "timestamp" }
},
"event_count": {
"value_count": { "field": "event_type" }
}
}
},
"avg_events_per_session": {
"avg_bucket": {
"buckets_path": "by_session>event_count"
}
}
}
}
Анализ воронок
Воронки реализуются через фильтрующие агрегации. Для каждого шага воронки применяем отдельный filter:
POST /events/_search
{
"size": 0,
"aggs": {
"funnel_step_1": {
"filter": { "term": { "event_type": "page_view" } },
"aggs": {
"unique_users": { "cardinality": { "field": "user_id" } }
}
},
"funnel_step_2": {
"filter": { "term": { "event_type": "add_to_cart" } },
"aggs": {
"unique_users": { "cardinality": { "field": "user_id" } }
}
},
"funnel_step_3": {
"filter": { "term": { "event_type": "purchase" } },
"aggs": {
"unique_users": { "cardinality": { "field": "user_id" } },
"total_revenue": { "sum": { "field": "revenue" } }
}
}
}
}
Когортный анализ
Когортный анализ требует двухэтапного подхода: сначала определяем когорту по дате первого события, затем анализируем поведение. В Elasticsearch это реализуется через enrichment или предварительное вычисление когорты при индексации.
Если поле cohort_month вычислено заранее и записано в документ, запрос становится простым:
POST /user_events/_search
{
"size": 0,
"aggs": {
"by_cohort": {
"terms": { "field": "cohort_month" },
"aggs": {
"by_month_offset": {
"terms": { "field": "months_since_registration" },
"aggs": {
"retained_users": { "cardinality": { "field": "user_id" } }
}
}
}
}
}
}
5. Производительность агрегаций: doc_values, fielddata, оптимизация памяти
Производительность агрегаций в Elasticsearch напрямую зависит от того, как хранятся данные на диске и в памяти.
Doc values vs Fielddata
doc_values — это колоночное хранилище на диске, включённое по умолчанию для всех типов, кроме text. Именно на doc_values опираются агрегации. Никогда не отключайте doc_values для полей, участвующих в агрегациях.
fielddata — устаревший механизм in-memory хранения для text-полей. Его использование в агрегациях — антипаттерн: приводит к OutOfMemoryError. Всегда используйте keyword-подтип для строк в агрегациях:
"event_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
Оптимизация памяти
- Устанавливайте heap JVM не более 50% от RAM узла и не более 32 ГБ (граница compressed oops)
- Используйте
circuit breakersдля ограничения потребления памяти агрегациями - Для высококардинальных
termsагрегаций применяйтеcompositeагрегацию с пагинацией - Параметр
execution_hint: mapвtermsагрегации снижает потребление памяти за счёт скорости - Применяйте
filterдля сужения выборки перед агрегацией — это критически важно для производительности
Request cache
Elasticsearch автоматически кэширует результаты агрегаций для запросов с size: 0 и фиксированными диапазонами. Для дашбордов это существенно снижает нагрузку. Управляйте кэшем через indices.requests.cache.size (по умолчанию 1% heap).
6. Интеграция с Kibana и внешними дашборд-инструментами
Kibana остаётся основным инструментом визуализации для Elasticsearch дашбордов. В 2026 году Kibana предлагает:
- Lens — drag-and-drop интерфейс для создания визуализаций без знания DSL
- ESQL — новый SQL-подобный язык запросов для аналитики (появился в Elasticsearch 8.x)
- Alerting — пороговые алерты на основе агрегаций
- Canvas — кастомные дашборды с пиксельной точностью
Для внешней визуализации Elasticsearch интегрируется с Grafana через плагин grafana-elasticsearch-datasource. Grafana позволяет строить дашборды поверх агрегаций Elasticsearch наравне с Prometheus и другими источниками данных.
Apache Superset поддерживает подключение к Elasticsearch через SQL-интерфейс (elasticsearch-dbapi). Это даёт возможность использовать привычный SQL-синтаксис для аналитических запросов:
SELECT
DATE_TRUNC('day', timestamp) AS day,
event_type,
COUNT(*) AS event_count,
COUNT(DISTINCT user_id) AS unique_users
FROM events
WHERE timestamp >= '2025-01-01'
GROUP BY 1, 2
ORDER BY 1
7. Сравнение с ClickHouse и PostgreSQL для аналитики при объёмах до 1 ТБ
Выбор аналитического движка — стратегическое решение. Рассмотрим три варианта честно.
Elasticsearch
- Сильные стороны: full-text search + аналитика в одной системе, гибкий маппинг, нативная интеграция с Kibana, горизонтальное масштабирование
- Слабые стороны: нет JOIN, высокое потребление памяти, не оптимален для тяжёлых OLAP-запросов, дорогой в поддержке кластер
- Оптимально для: смешанных поисково-аналитических нагрузок, логаналитики, событийных данных до ~500 ГБ
ClickHouse
- Сильные стороны: колоночное хранилище, исключительная производительность на аналитических запросах, SQL-совместимость, низкое потребление памяти при агрегациях
- Слабые стороны: слабый full-text search, сложные UPDATE/DELETE, меньшая гибкость схемы
- Оптимально для: чистой аналитической нагрузки, временных рядов, объёмов от 100 ГБ до петабайт
PostgreSQL
- Сильные стороны: ACID, полноценный SQL с JOIN, расширения (TimescaleDB, pg_analytics), знакомый инструментарий
- Слабые стороны: строчное хранилище плохо подходит для OLAP, вертикальное масштабирование
- Оптимально для: объёмов до 50-100 ГБ, смешанных OLTP+OLAP нагрузок
Вывод: для чистой аналитики при объёмах 100 ГБ — 1 ТБ ClickHouse производительнее Elasticsearch в 3-10 раз на типичных OLAP-запросах. Elasticsearch выигрывает, когда нужна единая система для поиска и аналитики или когда данные полуструктурированы.
8. Заполнение данными через Go и PHP пайплайны
Эффективная загрузка данных критична для аналитического движка Elasticsearch. Всегда используйте Bulk API.
Go пайплайн с официальным клиентом
package main
import (
"bytes"
"context"
"encoding/json"
"log"
"strings"
"time"
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esutil"
)
type AnalyticsEvent struct {
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id"`
EventType string `json:"event_type"`
Revenue float64 `json:"revenue,omitempty"`
}
func main() {
es, err := elasticsearch.NewDefaultClient()
if err != nil {
log.Fatalf("Error creating client: %s", err)
}
bi, err := esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
Index: "analytics_events",
Client: es,
NumWorkers: 4,
FlushBytes: 5e6, // 5MB
FlushInterval: 10 * time.Second,
})
if err != nil {
log.Fatalf("Error creating indexer: %s", err)
}
events := generateEvents(10000)
for _, event := range events {
data, _ := json.Marshal(event)
bi.Add(context.Background(), esutil.BulkIndexerItem{
Action: "index",
Body: bytes.NewReader(data),
OnFailure: func(ctx context.Context, item esutil.BulkIndexerItem,
res esutil.BulkIndexerResponseItem, err error) {
log.Printf("Indexing failed: %s", err)
},
})
}
bi.Close(context.Background())
stats := bi.Stats()
log.Printf("Indexed %d documents, failed: %d",
stats.NumIndexed, stats.NumFailed)
}
func generateEvents(n int) []AnalyticsEvent {
events := make([]AnalyticsEvent, n)
for i := range events {
events[i] = AnalyticsEvent{
Timestamp: time.Now().Add(-time.Duration(i) * time.Minute),
UserID: "user_" + strings.Repeat("x", i%10),
EventType: []string{"page_view", "click", "purchase"}[i%3],
}
}
return events
}
PHP пайплайн с использованием elasticsearch-php
setHosts(['localhost:9200'])
->build();
function indexEventsBatch(array $events, $client, string $index): void
{
$params = ['body' => []];
foreach ($events as $event) {
$params['body'][] = [
'index' => [
'_index' => $index,
]
];
$params['body'][] = [
'timestamp' => $event['timestamp'],
'user_id' => $event['user_id'],
'event_type' => $event['event_type'],
'revenue' => $event['revenue'] ?? null,
];
if (count($params['body']) >= 2000) {
$response = $client->bulk($params);
if ($response['errors']) {
error_log('Bulk indexing errors detected');
}
$params['body'] = [];
}
}
if (!empty($params['body'])) {
$client->bulk($params);
}
}
$events = array_map(fn($i) => [
'timestamp' => date('c', strtotime("-{$i} minutes")),
'user_id' => 'user_' . ($i % 100),
'event_type' => ['page_view', 'click', 'purchase'][$i % 3],
'revenue' => $i % 3 === 2 ? rand(10, 500) : null,
], range(0, 9999));
indexEventsBatch($events, $client, 'analytics_events');
echo "Indexing complete\n";
Для PHP рекомендуется использовать очереди (Laravel Queue, RabbitMQ) для буферизации событий перед записью в Elasticsearch, чтобы не нагружать кластер единичными запросами.
9. Мониторинг и управление кластером Elasticsearch в Kubernetes
Развёртывание Elasticsearch в Kubernetes в 2026 году стало стандартной практикой. Используйте официальный Elastic Cloud on Kubernetes (ECK) оператор:
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: analytics-cluster
spec:
version: 8.12.0
nodeSets:
- name: masters
count: 3
config:
node.roles: [master]
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests:
memory: 4Gi
cpu: 1
limits:
memory: 4Gi
- name: data-hot
count: 3
config:
node.roles: [data_hot, data_content]
node.attr.data: hot
podTemplate:
spec:
containers:
- name: elasticsearch
env:
- name: ES_JAVA_OPTS
value: "-Xms16g -Xmx16g"
resources:
requests:
memory: 32Gi
cpu: 4
limits:
memory: 32Gi
initContainers:
- name: sysctl
securityContext:
privileged: true
command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast-ssd
resources:
requests:
storage: 500Gi
Ключевые метрики для мониторинга кластера Elasticsearch через Prometheus + Grafana:
elasticsearch_cluster_health_status— статус кластера (green/yellow/red)elasticsearch_jvm_memory_used_bytes— использование heap памятиelasticsearch_indices_search_query_time_seconds— время выполнения запросовelasticsearch_indices_segments_count— количество сегментов (много сегментов = медленные агрегации)elasticsearch_thread_pool_search_rejected_count— отклонённые запросы
Для экспорта метрик используйте elasticsearch-exporter (prometheus-community/elasticsearch-exporter). В Kubernetes он разворачивается как отдельный Deployment с ServiceMonitor для Prometheus Operator.
Важные операционные практики для Elasticsearch в Kubernetes:
- Используйте
PodDisruptionBudgetдля предотвращения одновременного drain нескольких data-нод - Настройте
vm.max_map_count=262144через initContainer или DaemonSet - Разделяйте master, data и coordinating ноды для production-кластеров
- Используйте dedicated storageClass с SSD для data-нод горячего уровня
- Настройте ILM для автоматического перевода старых индексов на cold/frozen ноды
Docker-образ Elasticsearch требует специфической конфигурации ресурсов. Никогда не запускайте Elasticsearch в Docker или Kubernetes без явного ограничения heap через ES_JAVA_OPTS и resource limits — это ключевая причина OOM kills в production.
10. Заключение
Elasticsearch как аналитический движок — это обоснованный выбор для команд, которым нужна единая система для полнотекстового поиска и бизнес-аналитики при объёмах данных до 500 ГБ — 1 ТБ. Мощные агрегации, включая date_histogram, terms, nested и pipeline aggregations, позволяют строить сложные аналитические запросы без дополнительного слоя обработки.
Ключевые выводы о целесообразности использования Elasticsearch для аналитики:
- Выбирайте Elasticsearch, если у вас смешанная поисково-аналитическая нагрузка, полуструктурированные данные или существующая Elastic-инфраструктура
- Выбирайте ClickHouse, если это чистая аналитика с большими объёмами, сложными GROUP BY и предсказуемой схемой
- Выбирайте PostgreSQL с расширениями, если объёмы невелики и нужна полная SQL-совместимость с ACID-гарантиями
Для максимальной производительности Elasticsearch агрегаций: проектируйте маппинг с keyword для группировок, используйте doc_values, применяйте фильтрацию перед агрегацией, управляйте heap JVM и используйте ILM для управления жизненным циклом индексов. Пайплайны на Go обеспечат высокопроизводительную загрузку данных через Bulk API, а PHP-решения подойдут для интеграции с существующими веб-приложениями.
Развёртывание в Kubernetes через ECK оператор с правильно настроенными ресурсами, мониторингом через Prometheus и Grafana делает Elasticsearch production-ready аналитической платформой для среднего бизнеса в 2026 году.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →