CRUD-операции над документами

В статье Kibana Dev Tools и работа через REST API мы добавили первые документы и убедились, что кластер отвечает. Здесь разберём все четыре базовые операции полностью: создание, чтение, обновление и удаление — с нюансами, которые на практике важны.

Эндпоинт _doc и структура адреса

Каждый документ в Elasticsearch доступен по URL:

/<index>/_doc/<id>

Здесь _doc — фиксированный суффикс (начиная с ES 7.0 поддерживается только один тип документа на индекс). HTTP-метод определяет, что именно произойдёт: чтение, запись или удаление.

Создание документа

Явный ID. Когда идентификатор задаёт бизнес-логика — артикул, slug, UUID из вашей БД — используйте PUT:

PUT /products/_doc/42
{
  "name": "Клавиатура механическая",
  "price": 7500,
  "in_stock": true
}

Если документ с ID 42 уже есть — он будет полностью заменён новым. Не дополнен, а именно целиком перезаписан.

Автоматический ID. Если идентификатор не важен — используйте POST без ID:

POST /products/_doc
{
  "name": "Коврик для мыши",
  "price": 800,
  "in_stock": true
}

ES сам сгенерирует уникальный Base64-идентификатор и вернёт его в поле _id ответа — что-то вроде aB3cDe-xYz1234.

Защита от перезаписи. Хотите убедиться, что документ создаётся только один раз? Замените _doc на _create:

PUT /products/_create/42
{
  "name": "Клавиатура механическая",
  "price": 7500,
  "in_stock": true
}

Если документ с ID 42 уже существует, ES вернёт 409 Conflict — операция не выполнится.

Check yourself
Вы хотите добавить документ в индекс products, не назначая ID вручную. Какой HTTP-метод и URL использовать?

Чтение документа

GET /products/_doc/42

Ответ:

{
  "_index": "products",
  "_id": "42",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "name": "Клавиатура механическая",
    "price": 7500,
    "in_stock": true
  }
}

Поле _source — это дословно тот JSON, что вы положили. Всё остальное — служебные метаданные ES. Если документ не найден, "found" будет false и статус ответа — 404.

Версионирование: _version, _seq_no и _primary_term

ES отслеживает каждое изменение документа тремя числами:

  • _version — монотонно растущий счётчик. Начинается с 1 при создании, увеличивается при каждой записи. После удаления и повторного создания счётчик не сбрасывается.
  • _seq_no — порядковый номер операции на шарде. Уникален в пределах шарда.
  • _primary_term — номер «эпохи» primary-шарда; увеличивается, когда шард переизбирает лидера.

Пара _seq_no + _primary_term используется для оптимистичной блокировки: вы читаете документ, запоминаете эти значения, а при обновлении передаёте их как параметры запроса. ES сверит их с текущими. Если за это время кто-то изменил документ — значения не совпадут, и вы получите 409 Conflict:

PUT /products/_doc/42?if_seq_no=0&if_primary_term=1
{
  "name": "Клавиатура механическая",
  "price": 8000,
  "in_stock": true
}

Это стандартный способ избежать «гонки» при конкурентных записях без пессимистичных локов.

Check yourself
Вы прочитали документ: _version: 3, _seq_no: 10, _primary_term: 1. Пока вы готовили обновление, другой процесс тоже изменил этот документ. Что вернёт ES, если отправить PUT /products/_doc/42?if_seq_no=10&if_primary_term=1?
flowchart LR Client([Клиент]) Client -->|"PUT /_doc/id"| n1["Создать / Заменить"] Client -->|"POST /_doc"| n2["Создать с авто-ID"] Client -->|"PUT /_create/id"| n3["Только создать"] Client -->|"GET /_doc/id"| n4["Прочитать"] Client -->|"POST /_update/id"| n5["Частично обновить / Upsert"] Client -->|"DELETE /_doc/id"| n6["Удалить"] n1 --> Store[(_source в индексе)] n2 --> Store n3 --> Store n4 --> Store n5 --> Store n6 -->|"помечает удалённым"| Store
flowchart LR
    Client([Клиент])
    Client -->|"PUT /_doc/id"| n1["Создать / Заменить"]
    Client -->|"POST /_doc"| n2["Создать с авто-ID"]
    Client -->|"PUT /_create/id"| n3["Только создать"]
    Client -->|"GET /_doc/id"| n4["Прочитать"]
    Client -->|"POST /_update/id"| n5["Частично обновить / Upsert"]
    Client -->|"DELETE /_doc/id"| n6["Удалить"]
    n1 --> Store[(_source в индексе)]
    n2 --> Store
    n3 --> Store
    n4 --> Store
    n5 --> Store
    n6 -->|"помечает удалённым"| Store
Все CRUD-операции над документом: HTTP-метод, URL и результат

Частичное обновление через _update

PUT на _doc полностью заменяет документ. Но чаще нужно изменить одно-два поля, не трогая остальные. Для этого есть _update:

POST /products/_update/42
{
  "doc": {
    "price": 8500
  }
}

ES достанет существующий документ, применит изменения из doc и сохранит обратно. Поля name и in_stock останутся нетронутыми. Под капотом — атомарная операция «прочитать → смержить → записать» на primary-шарде.

Если в doc указать поле, которого раньше не было — оно добавится. Ошибок не будет.

Check yourself
Вы отправляете PUT /products/_doc/42 с телом {"price": 8500}. Что случится с полями name и in_stock, которые были в документе раньше?

Upsert: обновить или создать

Классический сценарий: нужно обновить документ, а если его нет — создать с нуля. _update поддерживает ключ upsert:

POST /products/_update/99
{
  "doc": {
    "price": 3000
  },
  "upsert": {
    "name": "USB-хаб",
    "price": 3000,
    "in_stock": true
  }
}

Логика проста:

  • Документ 99 существует → применяется doc (частичное обновление).
  • Документа 99 нет → создаётся документ из upsert.

Это удобно для счётчиков, кешей или логики «инициализировать при первом обращении».

Удаление документа

DELETE /products/_doc/42

В ответе придёт "result": "deleted". Если документа не было — "result": "not_found" и статус 404.

Удалённый документ не исчезает с диска мгновенно: Lucene помечает его как удалённый, а физически удаляет при следующем слиянии сегментов (merge). Место освобождается постепенно — это нормально.

Near real-time: документ появляется в поиске с задержкой

Прямой запрос по ID (GET /_doc/id) работает без задержки — ES читает данные напрямую из шарда. Но поисковый запрос (_search) увидит новый или изменённый документ только после refresh — по умолчанию раз в секунду. Это и называется near real-time (NRT).

Принудительный refresh для экспериментов:

POST /products/_refresh

В продакшне не злоупотребляйте: частые принудительные refresh-ы заметно снижают скорость индексации.

Quick recall
Зачем Elasticsearch отслеживает _version документа?
Quick recall
В ответе GET /products/_doc/42 есть поля _version, _seq_no, found, _source. Какое из них — ваши реальные данные?
Quick recall
Когда создаёте документ в Elasticsearch, выбираете между PUT с явным ID и POST без ID. В чём главное отличие?

См. также