ELSER: семантический поиск без своих эмбеддингов

В статье Векторный поиск: поле dense_vector и запрос kNN бэкенд сам генерировал вектор — отправлял текст во внешнюю модель, получал массив из сотен чисел, клал его в Elasticsearch. Это рабочая схема, но она требует инфраструктуры: модель нужно где-то хостить, поддерживать, платить за вызовы.

ELSER (Elastic Learned Sparse EncodeR) решает эту проблему иначе. Это встроенная NLP-модель Elastic — она живёт прямо внутри кластера и генерирует эмбеддинги самостоятельно, без вызова внешних сервисов. Вы отправляете документ, ELSER внутри ES создаёт его векторное представление и сохраняет. Внешний код пишет тот же REST, что и всегда — никаких изменений в бэкенде.


Sparse vs Dense: принципиально разные векторы

У dense_vector каждое из, скажем, 384 измерений имеет ненулевое значение — вектор «плотный», все числа значимы. ELSER работает иначе: он создаёт разреженный вектор (sparse vector), где большинство весов равны нулю, а ненулевые получают только термины, действительно значимые для смысла текста.

Конкретный пример. Текст «наушники с шумоподавлением для авиаперелётов» после обработки ELSER превращается примерно в такой объект:

{
  "headphones": 2.31,
  "noise_canceling": 1.87,
  "aviation": 1.43,
  "travel": 1.12,
  "audio": 0.84
  // остальные ~10 000 токенов = 0
}

Это не обычные токены из анализатора — ELSER обучен расширять смысл: слово «авиаперелёт» порождает вес у «aviation», «travel», «flight», которых в исходном тексте нет совсем. Именно это и называется семантическим поиском: запрос «шумоподавление в самолёте» находит документ «наушники для авиаперелётов», потому что их разреженные векторы пересекаются по значимым токенам.

flowchart LR A["📄 Документ\n(текст)"] --> B["Ingest Pipeline\n(inference-процессор)"] B --> C[".elser_model_2\n(ML-нода)"] C --> D["sparse_vector\n{\"travel\": 1.12,\n \"noise\": 1.87, ...}"] D --> E[("Индекс ES")] Q["🔍 Запрос\n(текст)"] --> F["inference_id:\nmy-elser-endpoint"] F --> C C --> G["sparse вектор\nзапроса"] G --> H["sparse_vector\nquery"] H --> E E --> I["Результаты"]
flowchart LR
    A["📄 Документ\n(текст)"] --> B["Ingest Pipeline\n(inference-процессор)"]
    B --> C[".elser_model_2\n(ML-нода)"]
    C --> D["sparse_vector\n{\"travel\": 1.12,\n \"noise\": 1.87, ...}"]
    D --> E[("Индекс ES")]

    Q["🔍 Запрос\n(текст)"] --> F["inference_id:\nmy-elser-endpoint"]
    F --> C
    C --> G["sparse вектор\nзапроса"]
    G --> H["sparse_vector\nquery"]
    H --> E
    E --> I["Результаты"]
Полный цикл ELSER: документ и запрос проходят через одну и ту же модель внутри ES
Check yourself
Пользователь вводит запрос «тихий перелёт». В индексе есть документ со словом «шумоподавление». При обычном match-поиске по полю text этот документ не найдётся. Почему ELSER его найдёт?

Quick recall
Чем разреженный вектор отличается от плотного в контексте ELSER?

Шаг 1: создать inference-эндпоинт

Прежде всего нужно зарегистрировать ELSER как inference-эндпоинт — это одновременно скачивает модель .elser_model_2 и разворачивает её на ML-ноде кластера:

PUT /_inference/sparse_embedding/my-elser-endpoint
{
  "service": "elser",
  "service_settings": {
    "model_id": ".elser_model_2"
  }
}

Здесь:

  • sparse_embedding — тип задачи (ELSER генерирует именно разреженные эмбеддинги).
  • my-elser-endpoint — произвольное имя эндпоинта; будет использоваться в запросах.
  • model_id: ".elser_model_2" — вторая версия модели, более точная и экономная по ресурсам, чем .elser_model_1. Точка в начале имени — это не опечатка: так Elastic маркирует системные модели.

Запрос вернётся быстро, но саму модель кластер продолжает скачивать в фоне. Проверить статус:

GET /_inference/sparse_embedding/my-elser-endpoint

Поле "status": "ready" — модель задеплоена, можно работать.

> Ресурсы. ELSER требует ML-ноды. В Elastic Cloud минимальный размер ML-зоны для ELSER — 4 ГБ RAM. При self-managed-установке нужна нода с ролью ml. Без ML-ноды запрос вернёт ошибку.


Quick recall
Какой REST-запрос создает inference-эндпоинт для ELSER?

Шаг 2: маппинг индекса

Для хранения sparse-вектора используется тип поля sparse_vector:

PUT /articles
{
  "mappings": {
    "properties": {
      "title":   { "type": "text" },
      "content": { "type": "text" },
      "content_embedding": { "type": "sparse_vector" }
    }
  }
}

Поле content_embedding будет хранить словарь «токен → вес», который ELSER создаёт из поля content. Поле sparse_vector нельзя использовать для сортировки или агрегаций — только для семантического поиска.


Quick recall
Почему нужно создать отдельное поле типа sparse_vector в маппинге для ELSER?

Шаг 3: индексирование через ingest pipeline

Чтобы ELSER автоматически генерировал эмбеддинги при каждом индексировании, создадим ingest pipeline с процессором inference:

PUT /_ingest/pipeline/elser-pipeline
{
  "description": "Генерация sparse-эмбеддингов через ELSER",
  "processors": [
    {
      "inference": {
        "model_id": ".elser_model_2",
        "input_output": [
          {
            "input_field":  "content",
            "output_field": "content_embedding"
          }
        ]
      }
    }
  ]
}

Теперь при индексировании передаём название пайплайна:

POST /articles/_doc?pipeline=elser-pipeline
{
  "title":   "Советы по выбору наушников для путешествий",
  "content": "Шумоподавляющие наушники незаменимы в авиаперелётах — они
               снижают усталость и помогают сосредоточиться."
}

ES передаст поле content в ELSER, получит словарь весов и сохранит его в content_embedding. В _source документа вы увидите это поле только если специально не отключили его хранение.

Check yourself
Вы создали inference-эндпоинт и ingest pipeline. Теперь индексируете документ командой POST /articles/_doc (без параметра pipeline). Что окажется в поле content_embedding?

Шаг 4: запрос sparse_vector

Семантический поиск выполняется запросом sparse_vector — он передаёт текст запроса в ELSER и ищет по разреженным весам:

GET /articles/_search
{
  "query": {
    "sparse_vector": {
      "field":        "content_embedding",
      "inference_id": "my-elser-endpoint",
      "query":        "как не уставать в дальнем перелёте"
    }
  }
}

ES берёт строку "query", прогоняет её через ELSER-эндпоинт, получает разреженный вектор запроса, потом ищет документы с максимальным пересечением весов. Документ из примера выше окажется в выдаче — несмотря на то что в запросе нет слов «наушники» или «авиаперелёт».

Можно комбинировать с фильтрами через bool:

GET /articles/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "sparse_vector": {
            "field":        "content_embedding",
            "inference_id": "my-elser-endpoint",
            "query":        "как не уставать в дальнем перелёте"
          }
        }
      ],
      "filter": [
        { "term": { "category": "travel" } }
      ]
    }
  }
}

text_expansion — устаревший запрос

В старых статьях и туториалах можно встретить запрос text_expansion — это предшественник sparse_vector-запроса. Он всё ещё работает в ES 8.x, но помечен как legacy в официальной документации. Используйте sparse_vector — у него более гибкий синтаксис и лучшая интеграция с inference API.

Старый вариант (не используйте в новых проектах):

// ⚠️ устаревший синтаксис — только для справки
"text_expansion": {
  "content_embedding": {
    "model_id": ".elser_model_2",
    "model_text": "как не уставать в дальнем перелёте"
  }
}
Check yourself
В чём разница между запросом sparse_vector и устаревшим text_expansion? Можно ли использовать text_expansion в новом проекте на ES 8.x?

Ограничения

  • Только первые 512 токенов каждого поля обрабатываются ELSER. Длинные документы нужно нарезать на чанки — это автоматически делает поле semantic_text.
  • Нужны ML-ноды в кластере. В Docker-single-node для разработки можно попробовать с флагом xpack.ml.enabled: true, но продакшн требует отдельных ML-нод.
  • Инкрементальная индексация. Если добавить ELSER-поле к существующему индексу, старые документы не получат эмбеддинги автоматически — нужен reindex с пайплайном.

ELSER vs dense_vector: когда что выбирать

СитуацияЧто выбрать
Нет своей ML-инфраструктуры, нужен семантический поиск «из коробки»ELSER
Уже используете конкретную модель (OpenAI, sentence-transformers)dense_vector
Документы и запросы на нескольких языкахdense_vector (модель E5) — ELSER работает только с английским
Нужно искать по изображениям или аудиоdense_vector (мультимодальные модели)
Хотите объяснить, почему документ подошёлELSER — веса токенов читаемы человеком

Оба подхода можно комбинировать в гибридном поиске через RRF — об этом в статье Гибридный поиск: retrievers и RRF.

Если хотите ещё проще — вообще без ручного управления пайплайнами — посмотрите на поле semantic_text: оно автоматически оборачивает весь этот процесс в один тип поля.


См. также

Sources

  1. Semantic search with ELSER | Elastic Docs
  2. Semantic search with ELSER (Reference) | Elasticsearch
  3. Sparse vector field type | Elasticsearch Reference
  4. ELSER inference service | Elasticsearch Reference
  5. Elasticsearch sparse vector query | Elasticsearch Labs