Что происходит, когда вы индексируете документ
Вы отправляете PUT /products/_doc/1 с JSON-объектом, и Elasticsearch отвечает "result": "created". Но что реально произошло внутри? Не «документ сохранился», а что именно? За ответом нужно заглянуть в Apache Lucene — библиотеку, на которой построен весь ES.
Инвертированный индекс: термин → документы
Реляционная БД при поиске перебирает строки таблицы (или читает отдельный индекс по столбцу). Elasticsearch делает принципиально иначе: уже при индексации он строит структуру «термин → список документов, в которых этот термин встречается». Это и есть инвертированный индекс — центральная структура данных в Lucene.
Конкретный пример. Добавим три документа с полем title:
| ID | title |
|---|---|
| 1 | «быстрый поиск» |
| 2 | «точный поиск» |
| 3 | «быстрый движок» |
После анализа (приведение к нижнему регистру, разбивка на токены) инвертированный индекс для поля title выглядит так:
| Термин | Документы |
|---|---|
быстрый | [1, 3] |
поиск | [1, 2] |
точный | [2] |
движок | [3] |
Теперь запрос match: { "title": "быстрый поиск" } превращается в поиск токенов быстрый и поиск. ES смотрит в таблицу выше: быстрый даёт {1, 3}, поиск даёт {1, 2}. В режиме OR (по умолчанию) объединяем — документы 1, 2, 3. В режиме AND ("operator": "and") берём пересечение — только документ 1. Никакого перебора всего индекса.
flowchart LR
subgraph docs[" Документы "]
D1["Doc 1: быстрый поиск"]
D2["Doc 2: точный поиск"]
D3["Doc 3: быстрый движок"]
end
subgraph idx[" Инвертированный индекс "]
T1["быстрый → 1, 3"]
T2["поиск → 1, 2"]
T3["точный → 2"]
T4["движок → 3"]
end
D1 --> T1
D1 --> T2
D2 --> T2
D2 --> T3
D3 --> T1
D3 --> T4От текста к токенам: связь с анализатором
Прежде чем попасть в инвертированный индекс, строка проходит через анализатор (подробно — в статье Анатомия анализатора: char filters, токенизатор, token filters):
1. Char filters — предобработка символов: убрать HTML-теги, нормализовать апострофы.
2. Tokenizer — разбить строку на токены: "Быстрый поиск!" → ["быстрый", "поиск"].
3. Token filters — преобразовать токены: привести к нижнему регистру, удалить стоп-слова, применить стемминг.
Ключевое следствие: поле типа text хранит в инвертированном индексе токены, а не исходную строку. Именно поэтому term-запрос по text-полю не работает так, как кажется — об этом мы говорили в Типы полей: text, keyword, числа и даты. term ищет точное значение в индексе; если там лежат токены elasticsearch и 8, запрос с "Elasticsearch 8" ничего не найдёт.
Поле keyword хранит строку без анализа — и именно для него term-запрос работает предсказуемо.
Сегменты Lucene: почему индекс нельзя «отредактировать»
Lucene не хранит один монолитный файл. Каждый шард ES содержит набор сегментов — отдельных, неизменяемых (immutable) мини-индексов. Новые документы пишутся в новый сегмент; то, что уже попало в сегмент, не изменяется никогда.
Что происходит при удалении или обновлении?
- Удаление — документ помечается в служебном файле
.delкак удалённый, но физически остаётся в сегменте. - Обновление — старая версия помечается удалённой, новая пишется в новый сегмент.
Настоящая очистка — слияние сегментов (segment merge): периодически Lucene объединяет несколько мелких сегментов в один крупный, выбрасывая помеченные документы. Для вас этот процесс полностью прозрачен.
flowchart TD
A["Новые документы"] --> B["In-memory buffer"]
B -->|refresh 1с| S1["Сегмент 1 — неизменяемый"]
B -->|refresh 1с| S2["Сегмент 2 — неизменяемый"]
B -->|refresh 1с| S3["Сегмент 3 — неизменяемый"]
S1 --> M["Слияние — segment merge"]
S2 --> M
S3 --> M
M --> S4["Большой сегмент — помеченные удалены"]Near real-time: почему документ не виден сразу
Новый документ не попадает мгновенно в сегмент. Сначала он оказывается во временном буфере памяти (in-memory buffer). Раз в ~1 секунду (настраивается через index.refresh_interval) Elasticsearch выполняет refresh: переносит буфер в новый сегмент, и документ становится видимым для поиска.
Именно поэтому Elasticsearch называют near real-time поисковиком: между записью и видимостью есть окно около 1 секунды. Это осознанный компромисс — refresh при каждой записи убил бы пропускную способность.
Если нужна немедленная видимость (например, в интеграционных тестах):
POST /products/_refreshИли при записи документа:
PUT /products/_doc/1?refresh=true
{
"title": "Elasticsearch 8"
}Почему полнотекстовый поиск такой быстрый
Три механизма работают вместе:
1. Инвертированный индекс устраняет линейный перебор. Поиск термина — это lookup в специализированной структуре данных, а не сканирование $N$ документов. Для типичного запроса это разница между «за миллисекунды» и «за секунды» при миллионах документов.
2. Операции над отсортированными списками. Списки документов в инвертированном индексе хранятся в отсортированном виде. Пересечение (AND) и объединение (OR) работают алгоритмом слияния — один проход по двум спискам одновременно, без вложенных циклов.
3. Параллельность по сегментам. Запрос выполняется по каждому сегменту независимо и параллельно; результаты мержатся координирующей нодой.
Кроме инвертированного индекса, Lucene строит в каждом сегменте дополнительные структуры:
- BKD-дерево — для быстрых
range-запросов по числам и датам. - Doc values — колоночное хранилище значений полей; обеспечивает быструю сортировку и агрегации.
Все эти структуры живут бок о бок внутри каждого сегмента.
Шард = экземпляр Lucene
Из статьи Кластер, ноды, шарды и реплики вы знаете, что шард — единица распределения данных. Добавим внутренний уровень: каждый шард это один экземпляр Lucene со своим набором сегментов, своим инвертированным индексом и своими doc values.
Один индекс ES с тремя primary-шардами — три независимых Lucene-индекса. Поисковый запрос рассылается по всем шардам параллельно, каждый шард ищет в своих сегментах, а координирующая нода собирает и ранжирует итоговые результаты. Именно это устройство позволяет ES горизонтально масштабироваться на терабайты данных без потери скорости поиска.
См. также
- Кластер, ноды, шарды и реплики
- Документ, индекс и JSON: модель данных
- Типы полей: text, keyword, числа и даты
- Анатомия анализатора: char filters, токенизатор, token filters
- Маппинг: явный, динамический и шаблоны индексов
- Query DSL: структура запроса и контекст query vs filter
- Релевантность и BM25: как считается _score