Elasticsearch vs Redis: сравнение подходов к реализации умного кэша и поиска в микросервисной архитектуре
Введение: почему Elasticsearch и Redis вообще сравнивают?
На первый взгляд сравнение Elasticsearch и Redis выглядит странно: один — мощный поисковый движок поверх Lucene, другой — in-memory хранилище данных с богатыми структурами. Однако в реальных микросервисных системах эти инструменты нередко конкурируют за одну и ту же нишу — быстрого доступа к данным и релевантного поиска.
Причина сравнения проста: обе технологии работают в оперативной памяти (или частично в ней), обе поддерживают горизонтальное масштабирование, и обе способны отвечать на запросы за миллисекунды. Когда команда проектирует новый микросервис с функцией поиска или кэширования, возникает вопрос: нужен ли нам полноценный Elasticsearch, или Redis Search справится?
В этой статье мы разберём оба инструмента без маркетинговых ярлыков — с архитектурными диаграммами, примерами кода на Go и практическими рекомендациями, применимыми в 2026 году.
Redis как кэш: паттерны, TTL и eviction policies
Redis — эталонный инструмент кэширования в микросервисных системах. Его архитектура однопоточного event loop обеспечивает предсказуемую латентность: типичный GET занимает менее 1 мс даже при тысячах одновременных соединений.
Паттерн Cache-Aside
Наиболее распространённый паттерн. Приложение сначала проверяет кэш, при промахе идёт в основное хранилище и записывает результат в Redis.
// Go: Cache-Aside с Redis
func GetProduct(ctx context.Context, rdb *redis.Client, db *sql.DB, id string) (*Product, error) {
cacheKey := "product:" + id
// 1. Проверяем кэш
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 — идём в БД
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. Записываем в кэш с TTL 10 минут
data, _ := json.Marshal(p)
rdb.Set(ctx, cacheKey, data, 10*time.Minute)
return &p, nil
}
Паттерн Write-Through
При каждой записи в БД одновременно обновляется кэш. Гарантирует консистентность, но увеличивает латентность записи. Используется там, где свежесть данных критична — например, в инвентаризационных сервисах.
TTL и политики вытеснения
Redis поддерживает несколько eviction policy, задаваемых через maxmemory-policy в конфигурации:
- allkeys-lru — вытесняет наименее недавно использованные ключи. Хороший выбор по умолчанию для кэша.
- volatile-lru — вытесняет только ключи с TTL. Безопаснее, если часть данных должна оставаться постоянной.
- allkeys-lfu — вытесняет наименее часто используемые. Лучше для рабочих нагрузок с явным hot-set.
- noeviction — возвращает ошибку при достижении лимита памяти. Опасно для кэширующих сценариев.
# redis.conf для кэширующего узла
maxmemory 4gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
# Ленивое освобождение памяти (рекомендуется для highload)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
Elasticsearch как поисковый движок: полнотекстовый поиск и агрегации
Elasticsearch решает принципиально другую задачу. Его сильная сторона — релевантный полнотекстовый поиск на основе инвертированного индекса, агрегации по миллионам документов в реальном времени и гибкий язык запросов DSL.
Как работает relevance scoring
Elasticsearch использует алгоритм BM25 (по умолчанию с версии 5.0) для ранжирования результатов. Оценка документа зависит от частоты термина в документе, редкости термина в коллекции (IDF) и длины поля. Это принципиально отличает его от простого поиска по индексу в Redis.
Пример запроса с агрегацией
// Go: полнотекстовый поиск с агрегацией через 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()
// обработка результатов...
return nil
}
Когда Elasticsearch незаменим
- Полнотекстовый поиск с релевантностью и подсветкой.
- Фасетная фильтрация с агрегациями (e-commerce, каталоги).
- Анализ логов и метрик (ELK-стек).
- Поиск по многоязычным данным с морфологическим анализом.
- Индексы от нескольких миллионов документов и выше.
Гибридные сценарии: когда Redis и Elasticsearch работают вместе
На практике Redis и Elasticsearch не конкурируют — они дополняют друг друга. Типичная архитектура микросервиса поиска выглядит так:
Клиент → API Gateway → Search Service → [Redis Cache] → Elasticsearch → PostgreSQL (источник данных)
Архитектурная диаграмма (текстовое описание):
- Клиент отправляет поисковый запрос в Search Service.
- Search Service формирует cache key на основе хэша параметров запроса.
- Проверяется Redis: если ключ есть — возвращается кэшированный JSON с результатами и агрегациями.
- При cache miss запрос идёт в Elasticsearch кластер.
- Результат сериализуется и сохраняется в Redis с TTL (30–300 секунд в зависимости от типа запроса).
- Асинхронно: индексатор слушает события изменений в PostgreSQL через Debezium/CDC и обновляет индекс Elasticsearch, инвалидируя соответствующие ключи в Redis.
Такой подход снижает нагрузку на Elasticsearch на 60–80% в сценариях с повторяющимися поисковыми запросами (каталоги, главные страницы, популярные категории).
Кейс 1: кэширование результатов поиска Elasticsearch в Redis
Архитектура решения
Ключевой вопрос — как формировать cache key для поисковых запросов. Naive-подход (просто строка запроса) не работает: параметры могут приходить в разном порядке, фильтры — в разных комбинациях.
// Go: детерминированный cache key для поискового запроса
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 {
// Сортируем ключи фильтров для детерминизма
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])
}
// Сервис с кэшированием
type SearchService struct {
es *elasticsearch.Client
rdb *redis.Client
}
func (s *SearchService) Search(ctx context.Context, req *SearchRequest) (*SearchResult, error) {
key := req.CacheKey()
// Попытка получить из кэша
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
}
}
// Запрос к Elasticsearch
result, err := s.queryElasticsearch(ctx, req)
if err != nil {
return nil, err
}
// Кэшируем с адаптивным TTL
ttl := s.adaptiveTTL(req)
if data, err := json.Marshal(result); err == nil {
s.rdb.Set(ctx, key, data, ttl)
}
return result, nil
}
// Адаптивный TTL: популярные запросы кэшируются дольше
func (s *SearchService) adaptiveTTL(req *SearchRequest) time.Duration {
if req.Page == 1 && req.Query == "" {
return 5 * time.Minute // главная страница каталога
}
if req.Page > 5 {
return 30 * time.Second // глубокая пагинация — короткий TTL
}
return 2 * time.Minute
}
Подводные камни
- Инвалидация кэша при обновлении индекса. При изменении документов в Elasticsearch нужно инвалидировать связанные ключи Redis. Проблема в том, что один документ может влиять на тысячи поисковых запросов. Решение: инвалидация по тегам (Redis Tags через Lua-скрипты или внешний маппинг) или короткий TTL с tolerable staleness.
- Cache stampede. При одновременном истечении TTL у многих ключей все запросы падают на Elasticsearch. Решение: probabilistic early expiration или distributed lock через
SET NX. - Сериализация агрегаций. Ответ Elasticsearch с агрегациями может занимать несколько МБ. Сжатие через gzip перед записью в Redis снижает потребление памяти на 70–80%.
Кейс 2: Redis Search как альтернатива Elasticsearch
Redis Search (модуль RediSearch, входящий в Redis Stack) — серьёзная альтернатива для сценариев с объёмом данных до нескольких миллионов документов. Он поддерживает полнотекстовый поиск, числовые фильтры, геопоиск и агрегации — всё в памяти.
Когда Redis Search оправдан
- Данных до 1–5 млн документов с компактной структурой.
- Команда не хочет поддерживать отдельный Elasticsearch кластер.
- Требуется атомарное обновление данных и индекса (Redis хранит оба).
- Латентность поиска должна быть < 5 мс.
// Go: Redis Search с использованием 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) {
// Полнотекстовый поиск + фильтрация по тегу и числовому полю
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
}
Ограничения Redis Search
- Все данные хранятся в RAM — стоимость хранения значительно выше, чем у Elasticsearch с disk-based индексами.
- Нет поддержки nested objects уровня Elasticsearch.
- Агрегации менее выразительны: нет pipeline aggregations, нет percentile aggregations.
- BM25 scoring менее настраиваем, чем в Elasticsearch.
- Горизонтальное масштабирование через Redis Cluster имеет ограничения по cross-slot операциям.
Сравнение инструментов: производительность, масштабируемость и стоимость
Производительность
- Redis (кэш): < 1 мс на GET/SET, 100k+ ops/sec на одном узле. Идеален для hot data.
- Redis Search: 1–5 мс на поисковый запрос при индексе до 1 млн документов. Деградирует при росте объёма.
- Elasticsearch: 10–100 мс на сложный поисковый запрос с агрегациями, но масштабируется линейно до миллиардов документов. Warm cache (OS file cache) ускоряет до 5–20 мс.
Масштабируемость
- Redis Cluster: горизонтальное масштабирование через шардирование. Ограничение: multi-key операции работают только в пределах одного слота. Redis Sentinel — для HA без шардирования.
- Elasticsearch: нативное горизонтальное масштабирование через шарды и реплики. Поддерживает cross-datacenter репликацию (CCR). Операции над всем индексом не имеют ограничений Redis Cluster.
Сложность операций
- Redis: простая установка, минимальная конфигурация, предсказуемое поведение. Мониторинг через
redis-cli INFOи встроенные метрики. Резервное копирование через RDB/AOF. - Elasticsearch: требует тюнинга JVM heap (рекомендуется 50% RAM, но не более 31 ГБ для compressed oops), управления шардами, мониторинга cluster health. Оперируя кластером из 3+ нод, команда тратит заметные усилия на операции.
Стоимость владения (TCO)
- Redis OSS: бесплатен, но Redis Stack (с Search) требует лицензии для производственного использования в части функций. Managed Redis (AWS ElastiCache, GCP Memorystore) — от $0.02/час за узел.
- Elasticsearch OSS / OpenSearch: бесплатны. Elastic Cloud или AWS OpenSearch Service — от $0.10/час за узел. Минимальная production-конфигурация (3 dedicated master + 2 data nodes) обходится в $300–600/месяц на облаке.
Практические рекомендации: что выбрать и когда
Выбирайте Redis (без Search) если:
- Задача — кэширование результатов, сессий, rate limiting, pub/sub между микросервисами.
- Поиск сводится к точному совпадению по ключу или простой фильтрации.
- Команда небольшая (< 5 backend-разработчиков), и операционная нагрузка критична.
Выбирайте Redis Search если:
- Объём данных < 2 млн документов, структура документов компактная (< 1 КБ).
- Уже используете Redis и хотите избежать ещё одного инфраструктурного компонента.
- Требуется атомарность операций обновления данных и индекса.
- Бюджет на инфраструктуру ограничен, а SLA по поиску — > 50 мс (не realtime).
Выбирайте Elasticsearch если:
- Объём данных > 5 млн документов или ожидается быстрый рост.
- Нужен релевантный полнотекстовый поиск с морфологией, синонимами, подсветкой.
- Требуются сложные агрегации: nested, pipeline, percentile, geo.
- Уже есть ELK-стек для логирования — можно переиспользовать инфраструктуру.
- SLA по поиску < 100 мс при нагрузке > 1000 QPS.
Гибридный подход: рекомендуемая архитектура для highload
В системах с высокой нагрузкой и требованиями к поиску используйте оба инструмента по их назначению:
- Elasticsearch — источник истины для поисковых запросов, агрегаций, фасетов.
- Redis — L1-кэш поверх Elasticsearch с TTL, адаптивным кэшированием популярных запросов.
- Redis также — кэш для auth-токенов, rate limiting, очереди задач (через Redis Streams).
- Инвалидация — через CDC (Debezium) или внутренние события микросервисов.
Заключение: нет универсального ответа — есть правильный контекст
Elasticsearch и Redis — не конкуренты, а инструменты с принципиально разными профилями. Redis блестит как быстрый кэш с богатыми структурами данных и простой операционной моделью. Elasticsearch незаменим для релевантного полнотекстового поиска на больших объёмах данных с гибкими агрегациями.
Redis Search закрывает промежуточную нишу и часто является прагматичным выбором для стартапов и небольших сервисов, где операционная простота важнее функциональных возможностей Elasticsearch.
Правильный ответ на вопрос «что выбрать» определяется не популярностью инструмента, а конкретными характеристиками вашей системы: объёмом данных, требованиями к релевантности, SLA, размером команды и бюджетом на инфраструктуру. Начните с Redis — и переходите к Elasticsearch, когда Redis Search перестаёт справляться. Это не компромисс, это инженерная прагматика.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →