Индексация документов: PUT, POST и bulk

Учимся загружать данные в Elasticsearch по одному документу и массово, тысячами, через bulk.

Индексация — это процесс добавления документа в индекс, при котором ES разбирает его поля и обновляет инвертированный индекс.

Одиночная индексация

Самый прямой способ — положить один документ. С известным id используем PUT:

curl -X PUT http://localhost:9200/products/_doc/1 \
  -H 'Content-Type: application/json' \
  -d '{"title": "Чайник", "price": 1990}'

Без id — POST, и ES вернёт сгенерированный _id:

curl -X POST http://localhost:9200/products/_doc \
  -H 'Content-Type: application/json' \
  -d '{"title": "Тостер", "price": 3490}'

Обновление и версии

Повторный PUT по тому же id заменяет документ целиком. Чтобы поменять только одно поле, есть частичное обновление через _update:

curl -X POST http://localhost:9200/products/_update/1 \
  -H 'Content-Type: application/json' \
  -d '{"doc": {"price": 1790}}'

Каждое изменение увеличивает поле _version документа — ES так отслеживает актуальность и помогает избегать конфликтов одновременной записи.

Массовая загрузка: _bulk

Грузить миллион документов по одному HTTP-запросу — это миллион round-trip'ов, очень медленно. Эндпоинт _bulk принимает много операций в одном запросе. Формат особенный: две строки на операцию — строка-команда и строка-данные, разделённые переводом строки (это NDJSON, а не обычный JSON-массив).

curl -X POST http://localhost:9200/_bulk \
  -H 'Content-Type: application/x-ndjson' \
  --data-binary '
{ "index": { "_index": "products", "_id": "10" } }
{ "title": "Блендер", "price": 4990 }
{ "index": { "_index": "products", "_id": "11" } }
{ "title": "Миксер", "price": 2990 }
'

Важно: каждая строка — отдельный компактный JSON без переносов внутри, и файл должен заканчиваться переводом строки. Поддерживаются действия index, create, update, delete.

Как работает под капотом

Когда документ приходит, ES определяет, в какой шард он попадёт (по хешу от _id), и пишет его в этот шард. Сначала запись попадает в буфер в памяти и в журнал (translog), а потом периодически (по умолчанию раз в секунду) происходит refresh — данные становятся видимы для поиска. Поэтому только что вставленный документ может появиться в результатах поиска с задержкой до секунды. Bulk эффективнее, потому что амортизирует накладные расходы: один сетевой запрос, один разбор, пакетная запись в шарды.

Частые ошибки

  • Слать обычный JSON-массив в _bulk. Bulk ждёт NDJSON: пары строк команда/данные, разделённые \n. Массив [...] не примет.
  • Ждать мгновенной видимости. Из-за refresh документ виден в поиске не сразу. Для тестов можно форсировать: ?refresh=true, но в проде так делать не стоит — это бьёт по производительности.
  • Слишком большие bulk-пачки. Пачка на сотни мегабайт перегрузит узел. Оптимум обычно — несколько тысяч документов или 5–15 МБ на запрос.

Итоги

  • Одиночно: PUT с id (замена целиком) или POST без id; частичное изменение — через _update.
  • Массово — через _bulk в формате NDJSON (две строки на операцию).
  • Из-за refresh свежий документ виден в поиске с небольшой задержкой (по умолчанию ~1 секунда).
Проверьте себя
1. В каком формате нужно отправлять данные в эндпоинт _bulk?
AОбычный JSON-массив объектов
BNDJSON: пары строк команда/данные, разделённые переводом строки
CCSV
DXML
2. Почему только что проиндексированный документ может не сразу найтись в поиске?
AИз-за шифрования
BИз-за периодического refresh: данные становятся видимы для поиска с задержкой (по умолчанию ~1 секунда)
CПотому что PUT не индексирует документ
DПотому что нужно перезапустить кластер