Elasticsearch vs Redis: comparación de enfoques para caché inteligente y búsqueda en arquitectura de microservicios
Introducción: ¿por qué se comparan Elasticsearch y Redis?
A primera vista, comparar Elasticsearch y Redis puede parecer extraño: uno es un potente motor de búsqueda sobre Lucene, el otro es un almacén de datos en memoria con ricas estructuras de datos. Sin embargo, en sistemas de microservicios reales, estas herramientas compiten frecuentemente por el mismo nicho: acceso rápido a datos y búsqueda relevante.
El motivo de la comparación es simple: ambas tecnologías operan en memoria RAM (o parcialmente), ambas soportan escalado horizontal y ambas pueden responder consultas en milisegundos. Cuando un equipo diseña un nuevo microservicio con funciones de búsqueda o caché, surge la pregunta: ¿necesitamos realmente Elasticsearch completo, o Redis Search es suficiente?
En este artículo analizaremos ambas herramientas sin etiquetas de marketing, con diagramas arquitectónicos, ejemplos de código en Go y recomendaciones prácticas aplicables en 2026.
Redis como caché: patrones, TTL y políticas de desalojo
Redis es la herramienta de referencia para caché en sistemas de microservicios. Su arquitectura de event loop monohilo garantiza una latencia predecible: un GET típico tarda menos de 1 ms incluso con miles de conexiones simultáneas.
Patrón Cache-Aside
El patrón más común. La aplicación consulta primero el caché; si hay un fallo, accede al almacén principal y escribe el resultado en Redis.
// Go: Cache-Aside con Redis
func GetProduct(ctx context.Context, rdb *redis.Client, db *sql.DB, id string) (*Product, error) {
cacheKey := "product:" + id
// 1. Verificamos el caché
cached, err := rdb.Get(ctx, cacheKey).Bytes()
if err == nil {
var p Product
if json.Unmarshal(cached, &p) == nil {
return &p, nil
}
}
// 2. Cache miss — vamos a la BD
var p Product
err = db.QueryRowContext(ctx,
"SELECT id, name, price FROM products WHERE id = $1", id,
).Scan(&p.ID, &p.Name, &p.Price)
if err != nil {
return nil, err
}
// 3. Escribimos en caché con TTL de 10 minutos
data, _ := json.Marshal(p)
rdb.Set(ctx, cacheKey, data, 10*time.Minute)
return &p, nil
}
Patrón Write-Through
Con cada escritura en la BD, el caché se actualiza simultáneamente. Garantiza consistencia, pero aumenta la latencia de escritura. Se utiliza donde la frescura de los datos es crítica, por ejemplo en servicios de inventario.
TTL y políticas de desalojo
Redis soporta varias políticas de desalojo configurables mediante maxmemory-policy en la configuración:
- allkeys-lru — desaloja las claves usadas menos recientemente. Buena opción predeterminada para caché.
- volatile-lru — desaloja solo claves con TTL. Más seguro si parte de los datos debe permanecer persistente.
- allkeys-lfu — desaloja las claves usadas con menor frecuencia. Mejor para cargas de trabajo con un hot-set definido.
- noeviction — devuelve un error al alcanzar el límite de memoria. Peligroso para escenarios de caché.
# redis.conf para nodo de caché
maxmemory 4gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
# Liberación perezosa de memoria (recomendado para highload)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
Elasticsearch como motor de búsqueda: búsqueda de texto completo y agregaciones
Elasticsearch resuelve una tarea fundamentalmente diferente. Su punto fuerte es la búsqueda de texto completo relevante basada en índice invertido, agregaciones sobre millones de documentos en tiempo real y un flexible lenguaje de consultas DSL.
Cómo funciona el relevance scoring
Elasticsearch utiliza el algoritmo BM25 (por defecto desde la versión 5.0) para clasificar resultados. La puntuación de un documento depende de la frecuencia del término en el documento, la rareza del término en la colección (IDF) y la longitud del campo. Esto lo diferencia fundamentalmente de la búsqueda simple por índice en Redis.
Ejemplo de consulta con agregación
// Go: búsqueda de texto completo con agregación mediante Elasticsearch
func SearchProducts(ctx context.Context, es *elasticsearch.Client, query string) error {
body := map[string]interface{}{
"query": map[string]interface{}{
"multi_match": map[string]interface{}{
"query": query,
"fields": []string{"name^3", "description", "tags"},
"type": "best_fields",
"fuzziness": "AUTO",
},
},
"aggs": map[string]interface{}{
"by_category": map[string]interface{}{
"terms": map[string]interface{}{
"field": "category.keyword",
"size": 10,
},
},
"price_stats": map[string]interface{}{
"stats": map[string]interface{}{
"field": "price",
},
},
},
"highlight": map[string]interface{}{
"fields": map[string]interface{}{
"name": map[string]interface{}{},
"description": map[string]interface{}{},
},
},
}
data, _ := json.Marshal(body)
res, err := es.Search(
es.Search.WithContext(ctx),
es.Search.WithIndex("products"),
es.Search.WithBody(bytes.NewReader(data)),
es.Search.WithTrackTotalHits(true),
)
if err != nil {
return err
}
defer res.Body.Close()
// procesamiento de resultados...
return nil
}
Cuándo Elasticsearch es insustituible
- Búsqueda de texto completo con relevancia y resaltado.
- Filtrado facetado con agregaciones (e-commerce, catálogos).
- Análisis de logs y métricas (stack ELK).
- Búsqueda sobre datos multilingües con análisis morfológico.
- Índices desde varios millones de documentos en adelante.
Escenarios híbridos: cuando Redis y Elasticsearch trabajan juntos
En la práctica, Redis y Elasticsearch no compiten: se complementan. La arquitectura típica de un microservicio de búsqueda tiene este aspecto:
Cliente → API Gateway → Search Service → [Redis Cache] → Elasticsearch → PostgreSQL (fuente de datos)
Diagrama arquitectónico (descripción en texto):
- El cliente envía la consulta de búsqueda al Search Service.
- El Search Service genera una cache key basada en el hash de los parámetros de la consulta.
- Se verifica Redis: si la clave existe, se devuelve el JSON cacheado con resultados y agregaciones.
- En caso de cache miss, la consulta se envía al clúster de Elasticsearch.
- El resultado se serializa y se guarda en Redis con TTL (30–300 segundos según el tipo de consulta).
- De forma asíncrona: el indexador escucha eventos de cambios en PostgreSQL mediante Debezium/CDC y actualiza el índice de Elasticsearch, invalidando las claves correspondientes en Redis.
Este enfoque reduce la carga sobre Elasticsearch entre un 60–80% en escenarios con consultas de búsqueda repetitivas (catálogos, páginas principales, categorías populares).
Caso 1: caché de resultados de búsqueda de Elasticsearch en Redis
Arquitectura de la solución
La pregunta clave es cómo generar la cache key para las consultas de búsqueda. El enfoque naive (simplemente la cadena de consulta) no funciona: los parámetros pueden llegar en orden diferente y los filtros en distintas combinaciones.
// Go: cache key determinista para una consulta de búsqueda
type SearchRequest struct {
Query string `json:"query"`
Filters map[string]string `json:"filters"`
Page int `json:"page"`
PageSize int `json:"page_size"`
SortField string `json:"sort_field"`
SortOrder string `json:"sort_order"`
}
func (r *SearchRequest) CacheKey() string {
// Ordenamos las claves de los filtros para determinismo
keys := make([]string, 0, len(r.Filters))
for k := range r.Filters {
keys = append(keys, k)
}
sort.Strings(keys)
parts := []string{
"search",
url.QueryEscape(strings.ToLower(r.Query)),
fmt.Sprintf("p%d", r.Page),
fmt.Sprintf("ps%d", r.PageSize),
r.SortField + ":" + r.SortOrder,
}
for _, k := range keys {
parts = append(parts, k+"="+r.Filters[k])
}
raw := strings.Join(parts, "|")
hash := sha256.Sum256([]byte(raw))
return "es_cache:" + hex.EncodeToString(hash[:16])
}
// Servicio con caché
type SearchService struct {
es *elasticsearch.Client
rdb *redis.Client
}
func (s *SearchService) Search(ctx context.Context, req *SearchRequest) (*SearchResult, error) {
key := req.CacheKey()
// Intento de obtener desde caché
if cached, err := s.rdb.Get(ctx, key).Bytes(); err == nil {
var result SearchResult
if json.Unmarshal(cached, &result) == nil {
result.FromCache = true
return &result, nil
}
}
// Consulta a Elasticsearch
result, err := s.queryElasticsearch(ctx, req)
if err != nil {
return nil, err
}
// Cacheamos con TTL adaptativo
ttl := s.adaptiveTTL(req)
if data, err := json.Marshal(result); err == nil {
s.rdb.Set(ctx, key, data, ttl)
}
return result, nil
}
// TTL adaptativo: las consultas populares se cachean por más tiempo
func (s *SearchService) adaptiveTTL(req *SearchRequest) time.Duration {
if req.Page == 1 && req.Query == "" {
return 5 * time.Minute // página principal del catálogo
}
if req.Page > 5 {
return 30 * time.Second // paginación profunda — TTL corto
}
return 2 * time.Minute
}
Escollos a tener en cuenta
- Invalidación del caché al actualizar el índice. Cuando se modifican documentos en Elasticsearch, es necesario invalidar las claves relacionadas en Redis. El problema es que un solo documento puede afectar a miles de consultas de búsqueda. Solución: invalidación por etiquetas (Redis Tags mediante scripts Lua o mapeo externo) o TTL corto con tolerancia a datos ligeramente desactualizados.
- Cache stampede. Cuando muchas claves expiran simultáneamente, todas las solicitudes recaen sobre Elasticsearch. Solución: probabilistic early expiration o distributed lock mediante
SET NX. - Serialización de agregaciones. La respuesta de Elasticsearch con agregaciones puede ocupar varios MB. Comprimir con gzip antes de escribir en Redis reduce el consumo de memoria entre un 70–80%.
Caso 2: Redis Search como alternativa a Elasticsearch
Redis Search (módulo RediSearch, incluido en Redis Stack) es una alternativa seria para escenarios con volúmenes de datos de hasta varios millones de documentos. Soporta búsqueda de texto completo, filtros numéricos, geobúsqueda y agregaciones, todo en memoria.
Cuándo Redis Search está justificado
- Datos de hasta 1–5 millones de documentos con estructura compacta.
- El equipo no quiere mantener un clúster de Elasticsearch separado.
- Se requiere actualización atómica de datos e índice (Redis almacena ambos).
- La latencia de búsqueda debe ser < 5 ms.
// Go: Redis Search usando go-redis v9
func CreateIndex(ctx context.Context, rdb *redis.Client) error {
_, err := rdb.Do(ctx, "FT.CREATE", "products_idx",
"ON", "HASH",
"PREFIX", "1", "product:",
"SCHEMA",
"name", "TEXT", "WEIGHT", "5.0",
"description", "TEXT",
"category", "TAG",
"price", "NUMERIC", "SORTABLE",
"stock", "NUMERIC",
).Result()
return err
}
func SearchInRedis(ctx context.Context, rdb *redis.Client, query, category string, maxPrice float64) (interface{}, error) {
// Búsqueda de texto completo + filtrado por etiqueta y campo numérico
ftQuery := fmt.Sprintf("@name|description:(%s) @category:{%s} @price:[0 %v]",
query, category, maxPrice,
)
result, err := rdb.Do(ctx, "FT.SEARCH", "products_idx",
ftQuery,
"LIMIT", "0", "20",
"SORTBY", "price", "ASC",
"HIGHLIGHT", "FIELDS", "2", "name", "description",
).Result()
return result, err
}
Limitaciones de Redis Search
- Todos los datos se almacenan en RAM: el coste de almacenamiento es significativamente mayor que el de Elasticsearch con índices en disco.
- No soporta nested objects al nivel de Elasticsearch.
- Las agregaciones son menos expresivas: no hay pipeline aggregations ni percentile aggregations.
- El scoring BM25 es menos configurable que en Elasticsearch.
- El escalado horizontal mediante Redis Cluster tiene limitaciones en operaciones cross-slot.
Comparación de herramientas: rendimiento, escalabilidad y coste
Rendimiento
- Redis (caché): < 1 ms en GET/SET, más de 100k ops/seg en un solo nodo. Ideal para hot data.
- Redis Search: 1–5 ms por consulta de búsqueda con índices de hasta 1 millón de documentos. Degrada al aumentar el volumen.
- Elasticsearch: 10–100 ms en consultas complejas con agregaciones, pero escala linealmente hasta miles de millones de documentos. El warm cache (caché de archivos del SO) lo acelera a 5–20 ms.
Escalabilidad
- Redis Cluster: escalado horizontal mediante sharding. Limitación: las operaciones multi-clave solo funcionan dentro de un mismo slot. Redis Sentinel proporciona HA sin sharding.
- Elasticsearch: escalado horizontal nativo mediante shards y réplicas. Soporta replicación entre centros de datos (CCR). Las operaciones sobre el índice completo no tienen las restricciones de Redis Cluster.
Complejidad operacional
- Redis: instalación sencilla, configuración mínima, comportamiento predecible. Monitorización mediante
redis-cli INFOy métricas integradas. Copias de seguridad mediante RDB/AOF. - Elasticsearch: requiere ajuste del heap de JVM (se recomienda 50% de RAM, pero no más de 31 GB para compressed oops), gestión de shards y monitorización del cluster health. Operar un clúster de 3+ nodos supone un esfuerzo operacional considerable para el equipo.
Coste total de propiedad (TCO)
- Redis OSS: gratuito, pero Redis Stack (con Search) requiere licencia para uso en producción en algunas funciones. Redis gestionado (AWS ElastiCache, GCP Memorystore): desde $0.02/hora por nodo.
- Elasticsearch OSS / OpenSearch: gratuitos. Elastic Cloud o AWS OpenSearch Service: desde $0.10/hora por nodo. Una configuración mínima de producción (3 dedicated master + 2 data nodes) cuesta entre $300–600/mes en la nube.
Recomendaciones prácticas: qué elegir y cuándo
Elige Redis (sin Search) si:
- La tarea es caché de resultados, sesiones, rate limiting o pub/sub entre microservicios.
- La búsqueda se reduce a coincidencia exacta por clave o filtrado simple.
- El equipo es pequeño (< 5 desarrolladores backend) y la carga operacional es crítica.
Elige Redis Search si:
- El volumen de datos es < 2 millones de documentos y la estructura de los documentos es compacta (< 1 KB).
- Ya usas Redis y quieres evitar añadir otro componente de infraestructura.
- Se requiere atomicidad en las operaciones de actualización de datos e índice.
- El presupuesto de infraestructura es limitado y el SLA de búsqueda es > 50 ms (no en tiempo real).
Elige Elasticsearch si:
- El volumen de datos es > 5 millones de documentos o se espera un crecimiento rápido.
- Se necesita búsqueda de texto completo relevante con morfología, sinónimos y resaltado.
- Se requieren agregaciones complejas: nested, pipeline, percentile, geo.
- Ya existe un stack ELK para logging y se puede reutilizar la infraestructura.
- El SLA de búsqueda es < 100 ms con una carga > 1000 QPS.
Enfoque híbrido: arquitectura recomendada para highload
En sistemas con alta carga y requisitos de búsqueda, utiliza ambas herramientas según su propósito:
- Elasticsearch — fuente de verdad para consultas de búsqueda, agregaciones y facetas.
- Redis — caché L1 sobre Elasticsearch con TTL y caché adaptativo de consultas populares.
- Redis también — caché para tokens de autenticación, rate limiting y colas de tareas (mediante Redis Streams).
- Invalidación — mediante CDC (Debezium) o eventos internos de los microservicios.
Conclusión: no hay una respuesta universal, sino el contexto adecuado
Elasticsearch y Redis no son competidores, sino herramientas con perfiles fundamentalmente distintos. Redis brilla como caché rápido con ricas estructuras de datos y un modelo operacional simple. Elasticsearch es insustituible para búsqueda de texto completo relevante sobre grandes volúmenes de datos con agregaciones flexibles.
Redis Search cubre el nicho intermedio y frecuentemente es la elección pragmática para startups y servicios pequeños donde la simplicidad operacional importa más que las capacidades funcionales de Elasticsearch.
La respuesta correcta a «qué elegir» no la determina la popularidad de la herramienta, sino las características concretas de tu sistema: volumen de datos, requisitos de relevancia, SLA, tamaño del equipo y presupuesto de infraestructura. Comienza con Redis y migra a Elasticsearch cuando Redis Search deje de ser suficiente. Eso no es un compromiso: es pragmatismo ingenieril.
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í →