Перколятор: обратный поиск по сохранённым запросам

Во всех статьях этого раздела поиск работал одинаково: документы лежат в индексе, запрос приходит снаружи, ES возвращает совпавшие документы. Перколятор переворачивает эту схему.

Обычный поиск: вы отправляете запрос → ES ищет по документам → возвращает подходящие.

Перколятор: вы подаёте документ → ES прогоняет его через сохранённые запросы → возвращает совпавшие запросы.

Это не просто другая точка зрения — это другой класс задач. Перколятор нужен там, где нужно «подписаться» на появление нужного контента, а не искать по уже накопленному. Три типичных сценария:

  • Алерты: пользователь описывает, что его интересует; при появлении нового документа он получает уведомление.
  • Авторазметка: правила-запросы описывают категории; новый документ автоматически получает теги.
  • Маршрутизация событий: входящие данные (логи, транзакции) проверяются на совпадение с набором фильтров.
flowchart LR subgraph NORMAL[Обычный поиск] Q1[Запрос из приложения] --> IDX1[(Индекс документов)] IDX1 --> R1[Список документов] end subgraph PERC[Перколятор] D1[Входящий документ] --> IDX2[(Индекс запросов)] IDX2 --> R2[Список запросов] end
flowchart LR
  subgraph NORMAL[Обычный поиск]
    Q1[Запрос из приложения] --> IDX1[(Индекс документов)]
    IDX1 --> R1[Список документов]
  end
  subgraph PERC[Перколятор]
    D1[Входящий документ] --> IDX2[(Индекс запросов)]
    IDX2 --> R2[Список запросов]
  end
Обычный поиск против перколятора: что находится в индексе и что возвращается в ответе

Тип поля percolator

Для хранения запросов в ES предусмотрен специальный тип поля — percolator. Когда вы индексируете документ с таким полем, ES компилирует Query DSL-запрос в структуры Lucene и сохраняет их. Именно поэтому маппинг перколятор-индекса должен заранее описывать все поля, на которые ссылаются сохранённые запросы — без схемы ES не сможет скомпилировать запрос.

Создадим индекс для алертов на статьи:

PUT /article-alerts
{
  "mappings": {
    "properties": {
      "alert_query": { "type": "percolator" },
      "description": { "type": "text" },
      "title":       { "type": "text" },
      "body":        { "type": "text" },
      "tags":        { "type": "keyword" }
    }
  }
}

description — поле самого алерта (текст для отображения пользователю). title, body, tags — схема документов, которые будут перколироваться. Именно на эти поля будут ссылаться сохранённые запросы — поэтому они обязаны присутствовать в маппинге.

Проверь себя
Зачем маппинг перколятор-индекса `article-alerts` содержит поля `title`, `body`, `tags` — если в самих алертах хранятся запросы, а не статьи?

Сохранение запросов в индексе

Каждый алерт — обычный документ. В поле alert_query кладём Query DSL-запрос. Подходит любой запрос, который поддерживает ES: bool, match, term, range и т. д.

PUT /article-alerts/_doc/alert-es
{
  "description": "Всё об Elasticsearch",
  "alert_query": {
    "match": { "title": "elasticsearch" }
  }
}

PUT /article-alerts/_doc/alert-vector
{
  "description": "Векторный и семантический поиск",
  "alert_query": {
    "bool": {
      "should": [
        { "match": { "body": "kNN" } },
        { "match": { "body": "эмбеддинги" } },
        { "match": { "body": "семантический поиск" } }
      ],
      "minimum_should_match": 1
    }
  }
}

Обратите внимание: запросы в alert_query ссылаются на поля title и body — именно они описаны в маппинге выше. Если попытаться сохранить запрос, ссылающийся на неизвестное поле, ES вернёт ошибку при индексации.

Percolate-запрос: прогоняем документ через индекс

Используем запрос типа percolate. Передаём имя поля-перколятора и сам документ:

GET /article-alerts/_search
{
  "query": {
    "percolate": {
      "field":    "alert_query",
      "document": {
        "title": "Введение в Elasticsearch: векторный поиск и kNN",
        "body":  "В статье разбираются kNN и эмбеддинги для семантического поиска."
      }
    }
  }
}

ES вернёт документы из article-alerts, чьи сохранённые запросы совпали с переданным документом. Здесь сработают оба: alert-es (слово «elasticsearch» в title) и alert-vector (слова «kNN» и «семантический поиск» в body).

Каждый совпавший результат — обычный ES-документ с _id, _score и _source. Вы получаете сразу и ID алерта, и его description. Поле _score отражает, насколько плотно документ совпал с условиями сохранённого запроса.

Проверь себя
Percolate-запрос отправлен в индекс `article-alerts`. Что именно вернёт ES в `hits` — статьи из какого-то другого индекса или сами алерты из `article-alerts`?

Подсветка совпавших фрагментов

Стандартный highlight работает с перколятором. Добавьте секцию highlight к запросу:

GET /article-alerts/_search
{
  "query": {
    "percolate": {
      "field":    "alert_query",
      "document": {
        "title": "Введение в Elasticsearch: векторный поиск и kNN",
        "body":  "В статье разбираются kNN и эмбеддинги для семантического поиска."
      }
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "body":  {}
    }
  }
}

Подсветка показывает, какие именно фрагменты перколируемого документа совпали с условиями каждого алерта. В ответе рядом с alert-vector в секции highlight появятся фрагменты с выделенными <em>kNN</em> и <em>эмбеддинги</em>. В уведомлении пользователю можно написать: «Ваша подписка сработала, потому что статья содержит kNN и семантический поиск».

Перколяция уже проиндексированного документа

Если документ уже есть в каком-то индексе — не передавайте его inline, укажите ссылку:

GET /article-alerts/_search
{
  "query": {
    "percolate": {
      "field": "alert_query",
      "index": "articles",
      "id":    "article-42"
    }
  }
}

ES сам вытащит документ из индекса articles по ID article-42 и прогонит его через перколятор.

Паттерн: система подписок

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

1. Пользователь создаёт подписку → вы сохраняете его Query DSL-запрос в /article-alerts.

2. Публикуется новая статья → вы отправляете percolate-запрос с её содержимым.

3. Получаете список совпавших алертов → рассылаете уведомления владельцам подписок.

Перколятор эффективно работает с сотнями тысяч сохранённых запросов — это реальный рабочий объём для современного кластера. Когда пользователь меняет подписку, достаточно сделать PUT с новым телом документа: ES перекомпилирует запрос при переиндексации.

Проверь себя
Пользователь хочет уточнить подписку — добавить к ней ещё одно условие. Как это сделать в ES, если запрос уже сохранён в `article-alerts`?

Несколько важных деталей

  • Обновление запроса: сделайте PUT /article-alerts/_doc/<id> с новым телом — ES перекомпилирует запрос. Работает и POST _update с частичным патчем поля alert_query.
  • Несколько документов за раз: используйте параметр documents (массив объектов) вместо document. В ответе появится поле _percolator_document_slot, указывающее, какой именно из переданных документов совпал.
  • Percolate в filter context: если скор не нужен, оберните в bool.filter — условие кешируется и не тратит ресурсы на подсчёт _score.
  • Маппинг — это контракт: добавляете новые поля в перколируемые документы — обновляйте маппинг перколятор-индекса через API _mapping.
Быстрое повторение
Какой тип запроса используется для прогонки документа через сохранённые запросы в перколяторе?
Быстрое повторение
Зачем в маппинге перколятор-индекса требуется описывать поля, на которые ссылаются сохранённые запросы?
Быстрое повторение
Как перколятор отличается от обычного поиска в Elasticsearch?

См. также