Elasticsearch como motor analítico: agregaciones, dashboards y alternativa a ClickHouse para volúmenes medianos
Elasticsearch se percibe tradicionalmente como un motor de búsqueda. Sin embargo, en 2026 cada vez más equipos lo consideran una herramienta analítica para business intelligence y dashboards con volúmenes de datos de hasta 1 TB. En este artículo analizamos si este enfoque está justificado y ofrecemos ejemplos concretos de consultas, consejos de optimización y una comparativa honesta con los competidores.
1. Elasticsearch como herramienta analítica: mitos y realidad
Existe un mito extendido: Elasticsearch sirve únicamente para full-text search, y para analítica se necesita ClickHouse o Druid. La realidad es más compleja. Elasticsearch dispone de un potente motor de agregaciones que cubre la mayoría de los escenarios OLAP con volúmenes de hasta varios cientos de gigabytes. Además, obtienes una infraestructura unificada para búsqueda y analítica, lo que reduce la complejidad operativa.
Ventajas clave de Elasticsearch como motor analítico:
- Agregaciones integradas sin capa adicional de transformación
- Integración nativa con Kibana para la creación de dashboards
- Escalado horizontal mediante sharding
- Mapeo flexible para datos semiestructurados
- REST API sin necesidad de dialecto SQL (aunque también existe interfaz SQL)
Principales limitaciones: Elasticsearch no es un almacén columnar en el sentido clásico, y los JOIN entre índices no están soportados a nivel de consultas. Para cargas analíticas pesadas con miles de millones de filas y consultas complejas multitabla, ClickHouse ganará. Pero para volúmenes medianos y cargas mixtas, Elasticsearch es una opción perfectamente válida.
2. Agregaciones en Elasticsearch: terms, date_histogram, nested, pipeline aggregations
Las agregaciones son el corazón del motor analítico de Elasticsearch. Se dividen en tres categorías: métricas (metric), de cubos (bucket) y de canalización (pipeline). Veamos los tipos principales con ejemplos.
Terms aggregation — agrupación por valor
Equivalente al GROUP BY en SQL. Se utiliza para contar valores únicos de un campo:
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"
}
}
}
}
}
}
El parámetro size determina el número de cubos devueltos. Para cardinalidades altas, utiliza la agregación composite para paginación.
Date histogram — series temporales
Herramienta clave para la analítica temporal. date_histogram agrupa documentos por intervalos de tiempo:
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
}
}
}
}
}
}
El parámetro min_doc_count: 0 garantiza la presencia de valores cero para los días sin eventos, lo que es fundamental para los gráficos de series temporales.
Nested aggregations — trabajo con objetos anidados
Para analizar estructuras anidadas (por ejemplo, líneas de pedido) se utiliza la agregación 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 — analítica sobre agregaciones
Las pipeline aggregations trabajan con los resultados de otras agregaciones. Son una herramienta poderosa para medias móviles, métricas derivadas y detección de anomalías:
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"
}
}
}
}
}
}
Las pipeline aggregations de tipo derivative, cumulative_sum y bucket_script permiten construir expresiones analíticas complejas sin postprocesamiento en el lado de la aplicación.
3. Diseño de índices para analítica
El diseño correcto de índices es fundamental para el rendimiento del motor analítico de Elasticsearch. Principios clave:
Mapeo para analítica
Para campos de texto usados en agregaciones, utiliza el tipo keyword en lugar de text. Los campos numéricos deben tener el tipo numérico correcto:
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"
}
}
El tipo scaled_float ahorra espacio en comparación con double y es adecuado para datos financieros. Establecer dynamic: false en objetos libres evita el crecimiento explosivo del mapeo.
Plantillas dinámicas
Para logs y eventos con estructura impredecible, utiliza 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" }
}
}
]
}
}
Optimización del almacenamiento
- Utiliza ILM (Index Lifecycle Management) para la transición automática de índices calientes a fríos
- Aplica
force_mergeen índices de solo lectura: reduce el número de segmentos y acelera las agregaciones - Desactiva
_sourceen índices donde no se necesita acceso a los documentos originales (solo agregaciones) - Usa compresión
best_compressionpara índices analíticos
4. Construcción de consultas analíticas: análisis de sesiones, embudos y análisis de cohortes
Análisis de sesiones
El análisis de sesiones de usuario es un caso de uso típico para el motor analítico de Elasticsearch. Consulta para calcular la duración media de sesión y el número de eventos:
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"
}
}
}
}
Análisis de embudos
Los embudos se implementan mediante agregaciones de filtrado. Para cada paso del embudo se aplica un filter independiente:
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" } }
}
}
}
}
Análisis de cohortes
El análisis de cohortes requiere un enfoque en dos etapas: primero se define la cohorte por la fecha del primer evento y luego se analiza el comportamiento. En Elasticsearch esto se implementa mediante enrichment o el cálculo previo de la cohorte durante la indexación.
Si el campo cohort_month ya está calculado y almacenado en el documento, la consulta es sencilla:
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. Rendimiento de las agregaciones: doc_values, fielddata y optimización de memoria
El rendimiento de las agregaciones en Elasticsearch depende directamente de cómo se almacenan los datos en disco y en memoria.
Doc values vs Fielddata
doc_values es el almacén columnar en disco, activado por defecto para todos los tipos excepto text. Las agregaciones se basan precisamente en doc_values. Nunca desactives doc_values para los campos que participan en agregaciones.
fielddata es un mecanismo obsoleto de almacenamiento en memoria para campos text. Usarlo en agregaciones es un antipatrón que provoca OutOfMemoryError. Utiliza siempre el subtipo keyword para cadenas en agregaciones:
"event_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
Optimización de memoria
- Establece el heap de JVM en no más del 50% de la RAM del nodo y no más de 32 GB (límite de compressed oops)
- Usa
circuit breakerspara limitar el consumo de memoria de las agregaciones - Para agregaciones
termsde alta cardinalidad, utiliza la agregacióncompositecon paginación - El parámetro
execution_hint: mapen la agregacióntermsreduce el consumo de memoria a costa de velocidad - Aplica
filterpara reducir la selección antes de agregar: esto es crítico para el rendimiento
Request cache
Elasticsearch almacena en caché automáticamente los resultados de agregaciones para consultas con size: 0 y rangos fijos. Para dashboards, esto reduce significativamente la carga. Gestiona la caché mediante indices.requests.cache.size (por defecto, 1% del heap).
6. Integración con Kibana y herramientas externas de dashboards
Kibana sigue siendo la principal herramienta de visualización para los dashboards de Elasticsearch. En 2026, Kibana ofrece:
- Lens — interfaz drag-and-drop para crear visualizaciones sin conocer el DSL
- ESQL — nuevo lenguaje de consultas similar a SQL para analítica (introducido en Elasticsearch 8.x)
- Alerting — alertas por umbral basadas en agregaciones
- Canvas — dashboards personalizados con precisión a nivel de píxel
Para visualización externa, Elasticsearch se integra con Grafana mediante el plugin grafana-elasticsearch-datasource. Grafana permite construir dashboards sobre las agregaciones de Elasticsearch al mismo nivel que Prometheus y otras fuentes de datos.
Apache Superset soporta la conexión a Elasticsearch mediante la interfaz SQL (elasticsearch-dbapi). Esto permite utilizar la sintaxis SQL habitual para consultas analíticas:
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. Comparativa con ClickHouse y PostgreSQL para analítica con volúmenes de hasta 1 TB
La elección del motor analítico es una decisión estratégica. Analizamos las tres opciones de forma objetiva.
Elasticsearch
- Puntos fuertes: full-text search + analítica en un mismo sistema, mapeo flexible, integración nativa con Kibana, escalado horizontal
- Puntos débiles: sin JOIN, alto consumo de memoria, no óptimo para consultas OLAP pesadas, clúster costoso de mantener
- Óptimo para: cargas mixtas de búsqueda y analítica, análisis de logs, datos de eventos hasta ~500 GB
ClickHouse
- Puntos fuertes: almacén columnar, rendimiento excepcional en consultas analíticas, compatibilidad SQL, bajo consumo de memoria en agregaciones
- Puntos débiles: full-text search limitado, UPDATE/DELETE complejos, menor flexibilidad de esquema
- Óptimo para: cargas puramente analíticas, series temporales, volúmenes de 100 GB a petabytes
PostgreSQL
- Puntos fuertes: ACID, SQL completo con JOIN, extensiones (TimescaleDB, pg_analytics), herramientas familiares
- Puntos débiles: almacenamiento por filas poco adecuado para OLAP, escalado vertical
- Óptimo para: volúmenes de hasta 50-100 GB, cargas mixtas OLTP+OLAP
Conclusión: para analítica pura con volúmenes de 100 GB a 1 TB, ClickHouse supera a Elasticsearch entre 3 y 10 veces en consultas OLAP típicas. Elasticsearch gana cuando se necesita un sistema unificado para búsqueda y analítica, o cuando los datos son semiestructurados.
8. Carga de datos mediante pipelines en Go y PHP
La carga eficiente de datos es fundamental para el motor analítico de Elasticsearch. Utiliza siempre la Bulk API.
Pipeline en Go con el cliente oficial
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
}
Pipeline en PHP con 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";
Para PHP se recomienda usar colas (Laravel Queue, RabbitMQ) para almacenar en buffer los eventos antes de escribirlos en Elasticsearch, evitando sobrecargar el clúster con solicitudes individuales.
9. Monitorización y gestión del clúster Elasticsearch en Kubernetes
El despliegue de Elasticsearch en Kubernetes se ha convertido en una práctica estándar en 2026. Usa el operador oficial 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
Métricas clave para monitorizar el clúster Elasticsearch mediante Prometheus + Grafana:
elasticsearch_cluster_health_status— estado del clúster (green/yellow/red)elasticsearch_jvm_memory_used_bytes— uso del heap de memoriaelasticsearch_indices_search_query_time_seconds— tiempo de ejecución de consultaselasticsearch_indices_segments_count— número de segmentos (muchos segmentos = agregaciones lentas)elasticsearch_thread_pool_search_rejected_count— solicitudes rechazadas
Para la exportación de métricas, utiliza elasticsearch-exporter (prometheus-community/elasticsearch-exporter). En Kubernetes se despliega como un Deployment independiente con ServiceMonitor para el Prometheus Operator.
Prácticas operativas importantes para Elasticsearch en Kubernetes:
- Usa
PodDisruptionBudgetpara evitar el drain simultáneo de varios nodos de datos - Configura
vm.max_map_count=262144mediante initContainer o DaemonSet - Separa los nodos master, data y coordinating en clústeres de producción
- Usa una storageClass dedicada con SSD para los nodos de datos del nivel caliente
- Configura ILM para la transición automática de índices antiguos a nodos cold/frozen
La imagen Docker de Elasticsearch requiere una configuración específica de recursos. Nunca ejecutes Elasticsearch en Docker o Kubernetes sin limitar explícitamente el heap mediante ES_JAVA_OPTS y los resource limits: esta es la principal causa de OOM kills en producción.
10. Conclusión
Elasticsearch como motor analítico es una elección razonada para los equipos que necesitan un sistema unificado de búsqueda de texto completo y business intelligence con volúmenes de datos de hasta 500 GB - 1 TB. Las potentes agregaciones, incluidas date_histogram, terms, nested y las pipeline aggregations, permiten construir consultas analíticas complejas sin capas adicionales de procesamiento.
Conclusiones clave sobre el uso de Elasticsearch para analítica:
- Elige Elasticsearch si tienes una carga mixta de búsqueda y analítica, datos semiestructurados o una infraestructura Elastic ya existente
- Elige ClickHouse si se trata de analítica pura con grandes volúmenes, GROUP BY complejos y un esquema predecible
- Elige PostgreSQL con extensiones si los volúmenes son pequeños y necesitas compatibilidad SQL completa con garantías ACID
Para maximizar el rendimiento de las agregaciones en Elasticsearch: diseña el mapeo con keyword para agrupaciones, usa doc_values, aplica filtrado antes de agregar, gestiona el heap de JVM y utiliza ILM para gestionar el ciclo de vida de los índices. Los pipelines en Go proporcionarán una carga de datos de alto rendimiento mediante la Bulk API, mientras que las soluciones PHP son adecuadas para la integración con aplicaciones web existentes.
El despliegue en Kubernetes mediante el operador ECK, con recursos correctamente configurados y monitorización a través de Prometheus y Grafana, convierte a Elasticsearch en una plataforma analítica production-ready para medianas empresas en 2026.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →