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["Результаты"]Шаг 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-ноды запрос вернёт ошибку.
Шаг 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 нельзя использовать для сортировки или агрегаций — только для семантического поиска.
Шаг 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 документа вы увидите это поле только если специально не отключили его хранение.
Шаг 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": "как не уставать в дальнем перелёте"
}
}Ограничения
- Только первые 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: оно автоматически оборачивает весь этот процесс в один тип поля.
См. также
- Векторный поиск: поле dense_vector и запрос kNN — как работают плотные векторы и kNN
- Поле semantic_text и inference-эндпоинты — более простой способ включить семантику с ELSER
- Гибридный поиск: retrievers и RRF — объединение BM25 и ELSER через RRF
- Составные запросы: bool и комбинирование условий — как строить bool-запросы с фильтрами
- Массовая загрузка данных через Bulk API — индексирование с пайплайном в bulk-режиме