Документ, индекс и JSON: модель данных

Документ — базовая единица данных

В Elasticsearch нет таблиц, строк и столбцов. Вместо этого всё строится вокруг документов. Документ — это JSON-объект: набор пар «ключ-значение», вложенных на любую глубину. Если вы уже работали с REST API, JSON вам знаком — это тот же формат.

Вот пример документа о товаре:

{
  "title": "Ноутбук Lenovo IdeaPad 3",
  "category": "laptops",
  "price": 79990,
  "in_stock": true,
  "tags": ["ноутбук", "lenovo", "14 дюймов"],
  "specs": {
    "cpu": "Intel Core i5",
    "ram_gb": 16,
    "storage_gb": 512
  }
}

Каждая пара — это поле (field): title, category, price и т. д. Значение поля может быть строкой, числом, булевым, массивом или вложенным объектом (как specs выше). ES принимает всё это без предварительного объявления схемы — хотя явный маппинг рекомендуется: подробнее в статье Маппинг: явный, динамический и шаблоны индексов.

Индекс — коллекция документов

Документы группируются в индексы. Индекс — это именованная коллекция документов с общим маппингом (схемой полей). Грубая аналогия с SQL: документ — строка, индекс — таблица. Аналогия неточная: ES не требует, чтобы все документы одного индекса имели одинаковые поля, — но стремиться к однородности стоит.

Имя индекса пишется строчными буквами, допускаются цифры, дефисы и подчёркивания: products, articles, logs-2024-06.

flowchart TD C["Кластер"] I1["Индекс: products"] I2["Индекс: articles"] D1["Документ _id: 1"] D2["Документ _id: 2"] D3["Документ _id: 42"] F1["title: string\nprice: number\nin_stock: boolean"] C --> I1 C --> I2 I1 --> D1 I1 --> D2 I2 --> D3 D1 --> F1
flowchart TD
    C["Кластер"]
    I1["Индекс: products"]
    I2["Индекс: articles"]
    D1["Документ _id: 1"]
    D2["Документ _id: 2"]
    D3["Документ _id: 42"]
    F1["title: string\nprice: number\nin_stock: boolean"]
    C --> I1
    C --> I2
    I1 --> D1
    I1 --> D2
    I2 --> D3
    D1 --> F1
Иерархия: кластер → индексы → документы → поля

Когда вы кладёте первый документ в несуществующий индекс, ES создаёт его автоматически. Это называется динамическим маппингом — удобно для экспериментов, но в продакшне лучше объявлять схему заранее.

Историческая деталь: раньше внутри одного индекса можно было заводить «типы» (types). С версии 7.0 типы убраны; теперь у каждого индекса единственный тип _doc. Правило хорошего тона: разные сущности — в разных индексах. Товары в products, статьи в articles, логи в logs-2024-06, а не всё вместе.

Проверь себя
ES создаёт индекс автоматически при первой записи документа. Почему это может быть проблемой в продакшне? И почему товары и статьи лучше держать в разных индексах, а не в одном?

Метаполя: что ES добавляет к вашему документу

Когда вы читаете или ищете документ, он возвращается не голым JSON-ом, а с набором метаполей — служебных полей ES. Они всегда начинаются с подчёркивания.

_id — уникальный идентификатор документа внутри индекса. Аналог первичного ключа в SQL. Можно задать вручную при индексации — или позволить ES сгенерировать автоматически (тогда это будет строка вроде "ZP4y5YIBNcTI_4Oo0Gzz"). Важно: _id уникален в рамках одного индекса, но не всего кластера.

_source — оригинальный JSON вашего документа в том виде, в котором вы его отправили. Именно _source возвращается в результатах поиска. ES хранит его отдельно от инвертированного индекса — чтобы можно было вернуть читаемый текст, а не просто факт «документ с таким-то _id содержит это слово».

_index — имя индекса, которому принадлежит документ. Полезно при запросах сразу по нескольким индексам.

Есть ещё технические метаполя: _version (номер версии), _seq_no и _primary_term — они нужны для оптимистичной блокировки при конкурентных обновлениях. На старте достаточно знать, что они существуют.

Полный цикл: создаём и читаем документ

Смотрим на реальный пример — прямо в Kibana Dev Tools или через curl.

Создаём документ с явным _id = 1:

PUT /products/_doc/1
{
  "title": "Ноутбук Lenovo IdeaPad 3",
  "category": "laptops",
  "price": 79990,
  "in_stock": true
}

ES отвечает:

{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": { "total": 2, "successful": 1, "failed": 0 }
}

Читаем документ по _id:

GET /products/_doc/1

Ответ:

{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "title": "Ноутбук Lenovo IdeaPad 3",
    "category": "laptops",
    "price": 79990,
    "in_stock": true
  }
}

Структура ответа всегда одна и та же: верхний уровень — метаполя (_index, _id, _version, found), внутри _source — ваш оригинальный JSON. При поиске через _search каждый элемент массива hits.hits выглядит точно так же.

Проверь себя
Посмотрите на ответ GET /products/_doc/1. Вы отправляли поле `price: 79990` — в каком ключе ответа оно окажется? Что находится на самом верхнем уровне JSON-ответа?

PUT с явным _id или POST с автогенерацией

Если у ваших данных уже есть естественный ключ (ID из основной БД, ISBN книги, артикул товара) — используйте его как _id. Повторный PUT на тот же _id просто перезапишет документ, не создав дубликат:

PUT /products/_doc/1
{
  "title": "Ноутбук Lenovo IdeaPad 3 Gen 2",
  "price": 84990
}

Если естественного ключа нет — POST без _id в URL, и ES сгенерирует его сам:

POST /products/_doc
{
  "title": "Мышь Logitech MX Master 3",
  "category": "accessories",
  "price": 8990
}

Автогенерируемый _id — случайная URL-safe строка Base64, достаточно длинная, чтобы коллизии были практически исключены.

_source под контролем: выбираем нужные поля

По умолчанию ES возвращает весь _source. Если документ большой, а нужна лишь пара полей, ограничьте это через параметр _source в теле запроса:

POST /products/_search
{
  "_source": ["title", "price"],
  "query": {
    "match_all": {}
  }
}

Тогда в каждом hit._source появятся только title и price. ES отфильтрует остальное перед отправкой ответа — экономит трафик при больших документах.

Проверь себя
Документы в индексе весят по 50 КБ каждый, но вам нужны только поля `title` и `price`. Как изменить запрос _search, чтобы получить только их — без остальных полей?

Иерархия сущностей: поле → документ → индекс → кластер

Соберём всё вместе:

  • Поле (field) — пара «имя: значение» внутри документа.
  • Документ (document) — JSON-объект, основная единица хранения и поиска. Имеет метаполя: _id, _source, _index.
  • Индекс (index) — именованная коллекция документов с единым маппингом. Физически хранится в шардах.
  • Кластер (cluster) — совокупность нод, между которыми распределены шарды всех индексов. Подробно — в Кластер, ноды, шарды и реплики.

Быстрое повторение
Что такое _id и почему он нужен?
Быстрое повторение
Как организованы документы в Elasticsearch?
Быстрое повторение
Какая базовая единица данных в Elasticsearch вместо таблиц и строк?

См. также