Архитектура

Elasticsearch Percolator: умные уведомления и обратный поиск в микросервисной архитектуре

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

Введение: что такое Percolator и в чём отличие обратного поиска

В классической поисковой модели вы храните документы и выполняете по ним запросы. Elasticsearch Percolator переворачивает эту логику с ног на голову: вы храните запросы в индексе, а затем проверяете, какие из них соответствуют входящему документу. Это и называется обратным поиском (reverse search).

Практический пример: пользователь настраивает алерт — «уведоми меня, когда iPhone 16 Pro появится на складе дешевле 80 000 рублей». Этот фильтр сохраняется как percolate-запрос. Когда в систему поступает новый товарный документ, Elasticsearch проверяет его против всех сохранённых запросов и возвращает список сработавших — то есть тех пользователей, которых нужно уведомить.

Ключевые сценарии применения Percolator в 2026 году:

  • Системы ценовых алертов в e-commerce
  • Мониторинг новостных потоков и RSS-агрегаторы
  • Алерты по метрикам в observability-платформах
  • Фильтрация событий безопасности (SIEM-системы)
  • Умные подписки на контент в медиаплатформах

Главное отличие от классического подхода: число сохранённых запросов может составлять миллионы, а документов — единицы в секунду. Percolator оптимизирован именно под такую инверсию соотношения запросов и данных.

Архитектурный обзор: Percolator в микросервисной системе

В микросервисной архитектуре Percolator обычно выступает ядром сервиса уведомлений или alert-engine. Типичная схема взаимодействия включает несколько слоёв:

  1. Сервис управления подписками — принимает пользовательские фильтры через REST API и сохраняет их как percolate-запросы в Elasticsearch.
  2. Брокер событий (Kafka, RabbitMQ, Pulsar) — получает входящие события из продуктового каталога, новостного потока или системы мониторинга.
  3. Percolate-worker — микросервис-консьюмер, который забирает события из брокера, выполняет percolate-запрос к Elasticsearch и передаёт список совпавших подписок в notification-сервис.
  4. Notification-сервис — рассылает уведомления по email, push, Telegram, Slack и другим каналам.

Асинхронная обработка через брокер событий критически важна: она позволяет перколировать документы независимо от основного продуктового потока и не блокировать запись. Percolate-worker может горизонтально масштабироваться — каждый экземпляр обрабатывает свою партицию топика.

Важный архитектурный принцип: индекс подписок и индекс данных должны быть разделены. Percolator работает поверх маппинга целевого индекса, но хранит запросы в отдельном поле типа percolator.

Настройка индекса Percolator: маппинг и хранение запросов

Рассмотрим настройку на примере индекса ценовых алертов для маркетплейса. Сначала создаём индекс с правильным маппингом:

PUT /price-alerts
{
  "mappings": {
    "properties": {
      "query": {
        "type": "percolator"
      },
      "user_id": {
        "type": "keyword"
      },
      "notification_channel": {
        "type": "keyword"
      },
      "created_at": {
        "type": "date"
      },
      "category": {
        "type": "keyword"
      },
      "max_price": {
        "type": "double"
      },
      "product_name": {
        "type": "text",
        "analyzer": "standard"
      },
      "in_stock": {
        "type": "boolean"
      },
      "sku": {
        "type": "keyword"
      }
    }
  },
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "index.percolator.map_unmapped_fields_as_text": true
  }
}

Поле query с типом percolator — ключевое. Остальные поля описывают структуру документов, которые будут перколироваться. Это важно: маппинг должен соответствовать структуре входящих данных, а не самих подписок.

Сохраняем пользовательский алерт — запрос на iPhone дешевле 80 000 рублей в наличии:

PUT /price-alerts/_doc/alert-user-42-iphone
{
  "user_id": "42",
  "notification_channel": "email",
  "created_at": "2026-01-15T10:00:00Z",
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "product_name": "iPhone 16 Pro"
          }
        },
        {
          "term": {
            "in_stock": true
          }
        }
      ],
      "filter": [
        {
          "range": {
            "max_price": {
              "lte": 80000
            }
          }
        }
      ]
    }
  }
}

Обратите внимание: внутри поля query можно использовать любые стандартные запросы Elasticsearch — bool, term, match, range, geo_distance, nested и другие. Это даёт огромную гибкость при создании сложных пользовательских фильтров.

Реализация сервиса уведомлений: обработка документов

Когда в каталог поступает новый или обновлённый товар, percolate-worker выполняет запрос на сопоставление:

POST /price-alerts/_search
{
  "query": {
    "percolate": {
      "field": "query",
      "document": {
        "product_name": "Apple iPhone 16 Pro 256GB",
        "sku": "APPL-IP16P-256",
        "max_price": 74990,
        "in_stock": true,
        "category": "smartphones"
      }
    }
  },
  "_source": ["user_id", "notification_channel"]
}

Elasticsearch возвращает все документы-запросы, которые соответствуют переданному документу. Ответ содержит _id алертов и метаданные пользователей. Далее worker передаёт список в notification-сервис:

# Псевдокод Python-воркера
def process_product_event(product: dict):
    response = es_client.search(
        index="price-alerts",
        body={
            "query": {
                "percolate": {
                    "field": "query",
                    "document": product
                }
            },
            "_source": ["user_id", "notification_channel"],
            "size": 1000  # максимум алертов за один запрос
        }
    )

    matched_alerts = response["hits"]["hits"]

    for alert in matched_alerts:
        user_id = alert["_source"]["user_id"]
        channel = alert["_source"]["notification_channel"]
        notification_queue.publish({
            "user_id": user_id,
            "channel": channel,
            "product": product,
            "alert_id": alert["_id"]
        })

    return len(matched_alerts)

Важный момент: при большом числе совпадений используйте параметр size и, при необходимости, pagination через search_after. По умолчанию Elasticsearch возвращает только 10 совпавших запросов.

Интеграция с микросервисами через REST API

Сервис управления подписками экспонирует REST API для фронтенда и других микросервисов. Типовой контракт:

# Создание алерта
POST /api/v1/alerts
Content-Type: application/json
Authorization: Bearer {token}

{
  "user_id": "42",
  "notification_channel": "push",
  "filters": {
    "product_name": "MacBook Pro",
    "max_price": 150000,
    "in_stock": true,
    "category": "laptops"
  }
}

# Ответ
{
  "alert_id": "alert-user-42-macbook-001",
  "status": "active",
  "created_at": "2026-03-10T12:00:00Z"
}

Сервис подписок транслирует пользовательские фильтры в Elasticsearch-запрос и сохраняет их в percolator-индексе. Ключевой момент — валидация запроса перед сохранением. Elasticsearch предоставляет Validate API:

POST /price-alerts/_validate/query
{
  "query": {
    "bool": {
      "must": [
        { "match": { "product_name": "MacBook Pro" } },
        { "term": { "in_stock": true } }
      ],
      "filter": [
        { "range": { "max_price": { "lte": 150000 } } }
      ]
    }
  }
}

Для асинхронной обработки входящих событий используйте Kafka с партиционированием по category товара. Это позволяет каждому экземпляру percolate-worker обрабатывать свою категорию параллельно, не конкурируя за одни и те же алерты.

При обновлении или удалении алерта достаточно стандартных операций Elasticsearch Update/Delete — percolator-индекс ведёт себя как обычный индекс с точки зрения CRUD.

Производительность: нагрузочные характеристики и оптимизация

Производительность Percolator определяется прежде всего числом сохранённых запросов и их сложностью. Ключевые метрики и рекомендации:

  • Число шардов: Elasticsearch параллельно выполняет percolate на всех шардах. При миллионе запросов оптимально 5–10 шардов. Избыточное шардирование создаёт overhead на координацию.
  • Кэширование: Percolator агрессивно использует query cache. Термины и фильтры (term, range) кешируются эффективно. match-запросы менее кэшируемы — отдавайте предпочтение term где возможно.
  • Сортировка по score: если вам не нужен relevance score, добавьте "sort": ["_doc"] — это ускоряет выборку на 20–40%.
  • Размер документа: чем меньше документ, который вы перколируете, тем быстрее обработка. Не передавайте лишние поля.
  • Named queries: используйте _name в запросах для дебага, но отключайте в production — они добавляют overhead.

Пример запроса с оптимизациями для production:

POST /price-alerts/_search
{
  "query": {
    "percolate": {
      "field": "query",
      "document": {
        "product_name": "Samsung Galaxy S25",
        "max_price": 65000,
        "in_stock": true,
        "category": "smartphones"
      }
    }
  },
  "sort": ["_doc"],
  "_source": ["user_id", "notification_channel"],
  "size": 500,
  "track_total_hits": false
}

Бенчмарки на кластере из 3 нод по 16 CPU / 64 GB RAM показывают: при 500 000 сохранённых percolate-запросов среднее время выполнения одного percolate — 15–50 мс в зависимости от сложности запросов. При 5 миллионах запросов — 100–300 мс, что требует тщательной оптимизации шардирования и hardware.

Реальный use case: система ценовых алертов end-to-end

Рассмотрим полный цикл работы системы мониторинга цен для маркетплейса с 2 миллионами пользователей:

  1. Пользователь создаёт алерт через мобильное приложение: «Уведоми меня о MacBook Pro дешевле 150 000 рублей».
  2. Subscription Service валидирует запрос, транслирует в DSL Elasticsearch и сохраняет в индексе price-alerts. Параллельно записывает мета-информацию (email, push-токен) в PostgreSQL.
  3. Поставщик обновляет цену через Catalog Service → событие публикуется в Kafka топик product.price.updated.
  4. Percolate Worker (Go-микросервис) консьюмит событие, выполняет percolate-запрос, получает список совпавших алертов.
  5. Deduplication: результаты проверяются через Redis — не уведомлять одного пользователя по одному алерту чаще раза в 24 часа. Ключ: notif:{alert_id}:{date}.
  6. Notification Worker отправляет push через Firebase, email через SendGrid. Статус записывается в PostgreSQL.
// Go: Percolate Worker — упрощённый пример
func (w *PercolateWorker) HandleProductEvent(ctx context.Context, product Product) error {
    res, err := w.esClient.Search(
        w.esClient.Search.WithIndex("price-alerts"),
        w.esClient.Search.WithBody(strings.NewReader(fmt.Sprintf(`{
            "query": {
                "percolate": {
                    "field": "query",
                    "document": {
                        "product_name": %q,
                        "max_price": %f,
                        "in_stock": %v,
                        "category": %q
                    }
                }
            },
            "sort": ["_doc"],
            "_source": ["user_id", "notification_channel"],
            "size": 1000,
            "track_total_hits": false
        }`, product.Name, product.Price, product.InStock, product.Category))),
    )
    if err != nil {
        return fmt.Errorf("percolate query failed: %w", err)
    }
    defer res.Body.Close()

    var result PercolateResult
    if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
        return fmt.Errorf("decode response failed: %w", err)
    }

    for _, hit := range result.Hits.Hits {
        dedupKey := fmt.Sprintf("notif:%s:%s", hit.ID, time.Now().Format("2006-01-02"))
        if w.redis.SetNX(ctx, dedupKey, 1, 24*time.Hour).Val() {
            w.notifQueue.Publish(ctx, NotificationTask{
                AlertID:  hit.ID,
                UserID:   hit.Source.UserID,
                Channel:  hit.Source.NotificationChannel,
                Product:  product,
            })
        }
    }
    return nil
}

Эта архитектура обрабатывает до 10 000 ценовых событий в минуту при 2 миллионах активных алертов, укладываясь в SLA 500 мс end-to-end от события до отправки уведомления.

Подводные камни и ограничения Percolator

Percolator — мощный инструмент, но у него есть серьёзные ограничения, которые нужно учитывать при проектировании:

  • Нет поддержки join-запросов между документами. Percolator проверяет один документ за раз. Если ваш алерт должен учитывать связанные данные из другого индекса — придётся денормализовывать данные перед перколяцией.
  • Сложность запросов влияет нелинейно. Вложенные bool-запросы с wildcard или script-фильтрами могут на порядок замедлить обработку. Профилируйте с помощью "profile": true.
  • Обновление маппинга требует переиндексации. Если структура документов изменилась, нужно пересоздавать индекс алертов и перенаправлять сохранённые запросы.
  • Нет нативной дедупликации. Elasticsearch не отслеживает, какие алерты уже срабатывали — это ваша ответственность (Redis, PostgreSQL).
  • Запросы с агрегациями не поддерживаются. Percolate-запрос не может содержать aggregations внутри сохранённого фильтра.
  • Не подходит для высокочастотных событий с простыми правилами. Если у вас 100 000 событий в секунду с простыми условиями — рассмотрите решения на уровне Kafka Streams или Flink, которые дадут меньшую задержку.
  • Размер индекса. Миллионы сложных запросов требуют значительного объёма RAM для сегментов Lucene. Планируйте heap Elasticsearch из расчёта ~1–5 KB на запрос.

Percolator идеален, когда число уникальных пользовательских фильтров на порядки превышает частоту входящих событий, а сами фильтры умеренно сложны. Для простых правил с высокой частотой событий рассмотрите stream processing; для очень сложных join-условий — rule engine на уровне приложения.

Заключение

Elasticsearch Percolator остаётся одним из наиболее элегантных инструментов для построения систем умных уведомлений и обратного поиска в микросервисной архитектуре. В 2026 году он актуален как никогда: рост числа персонализированных пользовательских фильтров в e-commerce, медиа и observability создаёт именно тот паттерн нагрузки, под который Percolator оптимизирован.

Ключевые выводы для практического применения:

  • Разделяйте индексы данных и percolator-запросов, используйте правильный маппинг с самого начала.
  • Выстраивайте асинхронную обработку через брокер событий — это обязательное условие для production-готовой системы.
  • Реализуйте дедупликацию уведомлений на уровне Redis вне Elasticsearch.
  • Профилируйте сложные запросы, отдавайте предпочтение term/range-фильтрам перед match и script.
  • Тестируйте производительность при реальном объёме сохранённых запросов — деградация нелинейна.

Правильно спроектированная система с Elasticsearch Percolator способна обслуживать миллионы пользовательских алертов в реальном времени с приемлемыми задержками и горизонтальным масштабированием — без написания собственного rule engine с нуля.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →