Elasticsearch Percolator: умные уведомления и обратный поиск в микросервисной архитектуре
Введение: что такое Percolator и в чём отличие обратного поиска
В классической поисковой модели вы храните документы и выполняете по ним запросы. Elasticsearch Percolator переворачивает эту логику с ног на голову: вы храните запросы в индексе, а затем проверяете, какие из них соответствуют входящему документу. Это и называется обратным поиском (reverse search).
Практический пример: пользователь настраивает алерт — «уведоми меня, когда iPhone 16 Pro появится на складе дешевле 80 000 рублей». Этот фильтр сохраняется как percolate-запрос. Когда в систему поступает новый товарный документ, Elasticsearch проверяет его против всех сохранённых запросов и возвращает список сработавших — то есть тех пользователей, которых нужно уведомить.
Ключевые сценарии применения Percolator в 2026 году:
- Системы ценовых алертов в e-commerce
- Мониторинг новостных потоков и RSS-агрегаторы
- Алерты по метрикам в observability-платформах
- Фильтрация событий безопасности (SIEM-системы)
- Умные подписки на контент в медиаплатформах
Главное отличие от классического подхода: число сохранённых запросов может составлять миллионы, а документов — единицы в секунду. Percolator оптимизирован именно под такую инверсию соотношения запросов и данных.
Архитектурный обзор: Percolator в микросервисной системе
В микросервисной архитектуре Percolator обычно выступает ядром сервиса уведомлений или alert-engine. Типичная схема взаимодействия включает несколько слоёв:
- Сервис управления подписками — принимает пользовательские фильтры через REST API и сохраняет их как percolate-запросы в Elasticsearch.
- Брокер событий (Kafka, RabbitMQ, Pulsar) — получает входящие события из продуктового каталога, новостного потока или системы мониторинга.
- Percolate-worker — микросервис-консьюмер, который забирает события из брокера, выполняет percolate-запрос к Elasticsearch и передаёт список совпавших подписок в notification-сервис.
- 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 миллионами пользователей:
- Пользователь создаёт алерт через мобильное приложение: «Уведоми меня о MacBook Pro дешевле 150 000 рублей».
- Subscription Service валидирует запрос, транслирует в DSL Elasticsearch и сохраняет в индексе
price-alerts. Параллельно записывает мета-информацию (email, push-токен) в PostgreSQL. - Поставщик обновляет цену через Catalog Service → событие публикуется в Kafka топик
product.price.updated. - Percolate Worker (Go-микросервис) консьюмит событие, выполняет percolate-запрос, получает список совпавших алертов.
- Deduplication: результаты проверяются через Redis — не уведомлять одного пользователя по одному алерту чаще раза в 24 часа. Ключ:
notif:{alert_id}:{date}. - 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. Подробнее обо мне →