Поле semantic_text и inference-эндпоинты

В статье ELSER и разреженные векторы для семантики мы прошли весь путь вручную: создали inference-эндпоинт, написали ingest pipeline, передавали ?pipeline=elser-pipeline при каждой индексации. Три отдельных конфигурационных шага, которые нужно поддерживать согласованными. Поле semantic_text сокращает их до одного.

С semantic_text вы объявляете тип поля — и Elasticsearch берёт на себя всё остальное: вызов inference-эндпоинта при индексации, разбивку длинного текста на фрагменты и хранение векторов. Никакого _ingest/pipeline, никакого ?pipeline=....


Быстрое повторение
Чем подход `semantic_text` отличается от ручного индексирования через ELSER с ingest pipeline?

Inference-эндпоинт: единственное, что нужно создать

Перед созданием индекса нужен inference-эндпоинт — точно так же, как при ручной работе с ELSER. Если вы читали предыдущую статью последовательно, эндпоинт my-elser-endpoint у вас уже есть.

Xорошая новость: ES 8 поставляется с двумя встроенными эндпоинтами, которые не нужно создавать вручную:

  • .elser-2-elasticsearch — ELSER v2, разреженные эмбеддинги. Оптимизирован для английского, с другими языками тоже работает.
  • .multilingual-e5-small-elasticsearch — плотные эмбеддинги, явная поддержка нескольких языков, включая русский.

Для быстрого старта можно указать .elser-2-elasticsearch прямо в маппинге, не создавая ничего заранее. Если нужны кастомные настройки аллокации — создайте эндпоинт явно, как описано в статье про ELSER.


Маппинг: одна строчка вместо трёх шагов

PUT /articles
{
  "mappings": {
    "properties": {
      "title":   { "type": "text" },
      "content": {
        "type":         "semantic_text",
        "inference_id": ".elser-2-elasticsearch"
      }
    }
  }
}

Поле content теперь «знает», через какой эндпоинт генерировать эмбеддинги. ES сохраняет эту связь в маппинге и использует её при каждой индексации.

> Если inference_id не указан — по умолчанию используется .elser-2-elasticsearch. Удобно для прототипов, где хочется как можно меньше конфигурации.


Быстрое повторение
Как объявить поле типа `semantic_text` с явным указанием ELSER-эндпоинта?

Индексирование: просто отправьте документ

POST /articles/_doc
{
  "title":   "Шумоподавляющие наушники для путешественников",
  "content": "Если вы часто летаете, наушники с активным шумоподавлением значительно снизят усталость в долгих перелётах. Технология ANC глушит монотонный гул двигателей, позволяя слушать музыку на меньшей громкости. При выборе обращайте внимание на автономность и складную конструкцию — это важно для перевозки в ручной клади."
}

Никакого ?pipeline=.... При получении этого запроса Elasticsearch:

1. Берёт текст поля content.

2. Разбивает его на чанки (об этом — следующий раздел).

3. Передаёт каждый чанк в ELSER-эндпоинт.

4. Сохраняет полученные векторы внутри документа.

Вставка происходит синхронно — ответ придёт после того, как эмбеддинги будут сгенерированы. На больших объёмах используйте Массовая загрузка данных через Bulk API параллельными батчами — иначе inference-эндпоинт станет узким местом.

flowchart LR subgraph manual["Ручной подход (ELSER)"] direction TB M1["① PUT /_inference"] --> M2["② PUT /_ingest/pipeline"] --> M3["③ PUT /index sparse_vector"] --> M4["④ POST /_doc?pipeline=..."] --> M5["Векторы в поле sparse_vector"] end subgraph smart["semantic_text"] direction TB S1["① PUT /index semantic_text + inference_id"] --> S2["② POST /_doc обычный"] --> S3["Авточанкинг + векторы сохранены"] end
flowchart LR
  subgraph manual["Ручной подход (ELSER)"]
    direction TB
    M1["① PUT /_inference"] --> M2["② PUT /_ingest/pipeline"] --> M3["③ PUT /index sparse_vector"] --> M4["④ POST /_doc?pipeline=..."] --> M5["Векторы в поле sparse_vector"]
  end
  subgraph smart["semantic_text"]
    direction TB
    S1["① PUT /index semantic_text + inference_id"] --> S2["② POST /_doc обычный"] --> S3["Авточанкинг + векторы сохранены"]
  end
Ручной подход требует трёх шагов настройки; semantic_text сводит всё к объявлению типа поля

Быстрое повторение
Какие операции выполняет Elasticsearch при индексировании документа с полем типа `semantic_text`?

Автоматический чанкинг — главное отличие от ручного ELSER

В статье про ELSER упоминалось критичное ограничение: модель обрабатывает только первые 512 токенов текста. Длинные документы нужно нарезать вручную — иначе всё, что дальше этой границы, просто теряется и не будет найдено при поиске.

semantic_text решает эту проблему автоматически. По умолчанию (начиная с ES 8.16) текст делится на предложения, которые группируются в секции по 250 слов с перекрытием в одно предложение. Каждая секция обрабатывается независимо и получает свой вектор.

Для документа из примера выше ES создаст примерно такие чанки:

  • Чанк 1: «Если вы часто летаете, наушники с активным шумоподавлением значительно снизят усталость в долгих перелётах.»
  • Чанк 2: «Технология ANC глушит монотонный гул двигателей, позволяя слушать музыку на меньшей громкости.»
  • Чанк 3: «При выборе обращайте внимание на автономность и складную конструкцию…»

При поиске ES сравнивает вектор запроса со всеми чанками всех документов и возвращает лучшие совпадения — даже если нужный фрагмент находится в середине длинного текста.

Проверь себя
Документ с пятью длинными абзацами индексируется двумя способами: вручную через ELSER без чанкинга и через поле `semantic_text`. Что произойдёт с последними абзацами, если суммарный текст превышает 512 токенов?

Параметры чанкинга можно переопределить прямо в маппинге:

"content": {
  "type":         "semantic_text",
  "inference_id": ".elser-2-elasticsearch",
  "chunking_settings": {
    "type":             "sentence",
    "max_chunk_size":   150,
    "sentence_overlap": 1
  }
}

Доступные значения type: "sentence" — по предложениям (по умолчанию), "word" — по словам, "none" — без разбивки (для коротких текстов или если вы нарезаете сами, передавая массив строк вместо одной строки).


Запрос semantic

GET /articles/_search
{
  "query": {
    "semantic": {
      "field": "content",
      "query": "как не уставать в дальнем перелёте"
    }
  }
}

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

Проверь себя
В запросе `semantic` вы указываете только `field` и `query` — ни модели, ни inference_id. Откуда Elasticsearch знает, каким эндпоинтом векторизовать строку запроса?

semantic-запрос хорошо комбинируется с фильтрами через bool:

GET /articles/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "semantic": {
            "field": "content",
            "query": "как не уставать в дальнем перелёте"
          }
        }
      ],
      "filter": [
        { "term": { "category": "travel" } }
      ]
    }
  }
}

А в гибридном поиске semantic объединяется с match через Гибридный поиск: retrievers и RRF — тогда один запрос охватывает и точные совпадения по ключевым словам, и семантику.


search_inference_id: разные модели для индексации и поиска

Опциональный параметр маппинга — отдельный эндпоинт только для векторизации поискового запроса:

"content": {
  "type":                "semantic_text",
  "inference_id":        ".elser-2-elasticsearch",
  "search_inference_id": "my-fast-elser-endpoint"
}

Это позволяет, например, индексировать через качественную модель, а запросы обрабатывать через более лёгкую и быструю — что снижает latency поиска. Единственное условие: оба эндпоинта должны генерировать совместимые векторы (sparse к sparse, dense к dense), иначе сравнение не даст смысла.


Ограничения

  • Поле нельзя использовать внутри nested-полей и в Cross-Cluster Search (поиск по нескольким кластерам).
  • После создания индекса изменить inference_id поля нельзя — только reindex в новый индекс с обновлённым маппингом.
  • Синхронная генерация эмбеддингов замедляет вставку: на высоких нагрузках нужно планировать дополнительные ML-ноды.
  • Требует индексов, созданных на ES 8.11+; Generally Available с версии 8.18.

semantic_text против ручного подхода: когда что выбирать

Простое правило: semantic_text — это ELSER в автоматическом режиме. Берите его по умолчанию. Переходите к ручному подходу через sparse_vector + pipeline только тогда, когда нужен нестандартный контроль: например, дополнительная нормализация текста прямо в пайплайне или хранение эмбеддингов в отдельном индексе по специфической схеме.


См. также