Архитектура

Elasticsearch vs Redis: сравнение подходов к реализации умного кэша и поиска в микросервисной архитектуре

Ruslan Ismailov Опубликовано 14 мин чтения
E

Введение: почему 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

В системах с высокой нагрузкой и требованиями к поиску используйте оба инструмента по их назначению:

  1. Elasticsearch — источник истины для поисковых запросов, агрегаций, фасетов.
  2. Redis — L1-кэш поверх Elasticsearch с TTL, адаптивным кэшированием популярных запросов.
  3. Redis также — кэш для auth-токенов, rate limiting, очереди задач (через Redis Streams).
  4. Инвалидация — через 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. Подробнее обо мне →