Что такое шардинг (sharding) в Elasticsearch? Как работает Apache Lucene index?
Понимание того, что такое шардинг (sharding) в Elasticsearch играет главную роль в этой мощнейшей поисковой архитектуре. Поэтому для правильной её настройки очень важно понимать как этот процесс работает.
В основе поисковых возможностей Elasticsearch лежит работа Apache Lucene index. Наверняка, многие из вас видели, как в Google при наборе поисковой фразы появляются варианты продолжения. Или как в некоторых приложениях при вводе каждой последующей буквы в поисковом слове, мгновенно появляются куски текста с искомым фрагментом. Например, как в моей простейшей, но мощной поисковой системе поиска статей на канале Дзен.
Однако, специалистов по настройке Elasticsearch мало. Как мало и толковой документации, объясняющей её работу. В англоязычных источниках написано тоже не очень понятно.
Попытки прояснить, что такое шардинг вместе с заточенной под программирование нейросетью Claude (Sonnet 4.5 и Opus 4.1) помогли мало. Потом я понял почему: Claude пытался объяснить всё сверху вниз (от сложного к простому). Но если верхний уровень построен сложно, это – не лучшая отправная точка.
Тогда я переключился на Perplexity, с его PRO планом, поиском в интернете, особенно с мощными возможностями Deep Research. Perplexity тоже начал со сложного и сослался на Apache Lucene index. Стоп, говорю я ему, объясни сначала как работает этот самый индекс, на простых примерах. “Ты пытаешься объяснить мне как, работает Apache Lucene index через сегменты, которые сами являются частью этого самого индекса???”. Спрашивал на английском, на русском вообще было бы непонятно с терминологией.
Общий смысл объяснения был таков: что-то там появляется, создаются индексы, но применяются обратные (инвертированные) индексы, создающиеся при вводе нового документа; из этих индексов создаются списки для изменения, хитрым образом возникают сегменты, которые изменяться в дальнейшем не должны, но могут сливаться, образуя новые. В Elasticsearch всё это попадает в шарды. А слово index в Elasticsearch используется ещё и в совершенно другом смысле – базы данных поиска (коллекции документов).
Я уж было немного приуныл – ну неужели я такой тупой, что не могу понять всю эту механику? Но нет, отступать не привык, начал с самого простого.
Итак, идём от простого к сложному.
Как работает Apache Lucene index?
Объяснять буду на английских примерах для упрощения. Английский для понимания технических терминов – самый простой язык.
Итак, получаем первую фразу (документ doc1) : “I love cakes” (Я люблю торты). Фраза расщепляется на токены (смысловые единицы), в данном случае на слова “I”, “love” и “cake” (берём единственное число, без буквы “S”) . При это создаются индексы, показывающие в каком документе какие слова встречаются. Но применяются инвертированные индексы с указанием слова и в каком документе оно имеется.
Например, после первой фразы создались инвертированные индексы:
I : doc1
love : doc1
cake : doc1
Вводим ещё 2 документа: “Cake is delicious” (Торт восхитителен) – doc2 и “I love chocolate” (Я люблю шоколад). С тремя документами мы имеем следующий набор инвертированных индексов:
I : doc1, doc3
love : doc1, doc3
cake : doc1
is : doc2
delicious : doc2
chocolate : doc3
Видно, что первые 2 слова встречаются как в doc1, так и в doc3.
При вводе каждого нового документа создается сегмент. С тремя введенными документами мы имеем 3 сегмента:
segment1 : (I : Doc1), (love : Doc1), (cake : Doc1)
segment2 : (cake : Doc2), (is : Doc2), (delicious : Doc2)
segment3 : (I : Doc3), (love : Doc3), (chocolate : Doc3)
При поиске искомое слово ищется в каждом независимом сегменте. Понятно, что число сегментов изначально равно количеству документов.
При появлении нового документа doc4: “I love candies” (Я люблю конфеты), мы бы в segment1 нашли 2 из трёх слов документа doc4 в документах doc1 и doc3. При поиск похожих документов мы бы быстро их нашли в segment1.
А что было бы, если бы третий документ был просто “I love“? Тогда бы мы объединили последний полностью повторяющийся сегмент с тем, в который он входит. Т.е. объединили бы сегменты 1 и 3. В результате наши новые сегменты выглядели бы так:
segment4 : (I : (Doc1, Doc3), (love : (Doc1, Doc3), (cake : Doc1) # Объединение doc1 и doc3
segment2 : (cake : Doc2), (is : Doc2), (delicious : Doc2) # не меняется
Число сегментов уменьшилось. Значит, и поиск будет быстрее.
В поиске слова “I” или слова “love” сегмент 4 сразу выдаст 2 документа, doc1 doc3.
Так работает Apache Lucene index. Теперь, когда мы разобрали, как работает индексирование документов и создание сегментов, давайте ещё раз начнём с самого начала.
Что такое Apache Lucene index
Apache Lucene – движок полнотекстового поиска.
Apache Lucene – это open-source библиотека (написана на Java), которая обеспечивает полнотекстовый поиск. Это не самостоятельное приложение, а библиотека – строительный блок, который используют другие системы. Elasticsearch построен поверх Lucene – каждый шард в ES это обёртка над Lucene индексом.
Что такое индекс в Lucene
Lucene index – это структура данных на диске, оптимизированная для быстрого поиска текста. Технически это директория с набором файлов специального формата.
Основные компоненты Lucene индекса:
1. Documents (документы)
- Единица данных в Lucene
- Состоит из полей (fields)
- Каждое поле имеет имя и значение
Lucene Index
├── segment_1 (100 docs)
├── segment_2 (150 docs)
├── segment_3 (200 docs)
└── segment_4 (50 docs)
```
2. Inverted Index (инвертированный индекс)
Это сердце Lucene. Вместо хранения “документ → слова в нём”, Lucene строит обратную структуру: “слово → документы, где оно встречается”.
Пример:
Документы:
Doc1: "The quick brown fox"
Doc2: "The fox jumps high"
Doc3: "Brown dogs run fast"
Инвертированный индекс:
"the" → [Doc1, Doc2]
"quick" → [Doc1]
"brown" → [Doc1, Doc3]
"fox" → [Doc1, Doc2]
"jumps" → [Doc2]
"high" → [Doc2]
"dogs" → [Doc3]
"run" → [Doc3]
"fast" → [Doc3]
Поиск слова “fox” мгновенно возвращает [Doc1, Doc2] без сканирования всех документов.
3. Сегменты
- Каждый segment – это мини-индекс со своим инвертированным индексом
- Сегмент никогда не модифицируется после создания, его можно только пометить как удаленный и создать новый сегмент (аналог изменения)
- Новые документы → новые сегменты
- Старые сегменты периодически сливаются в более крупные
При поиске Lucene запрашивает все сегменты параллельно и объединяет результаты.
Почему сегменты неизменяемы?
Преимущества:
- Если сегмент не меняется, его можно читать быстро и безопасно – если бы менялись была бы конкуренция с другими запросами на право записи и временная блокировка сегмента, если кто-то его обновляет.
- Операционная система может агрессивно кешировать неизменяемые файлы.
- Простое восстановление при крэше – незавершённые сегменты просто удаляются.
- Эффективная компрессия – неизменяемые данные сжимается эффективнее.
Недостаток:
- Удалённые документы не удаляются физически – только помечаются в отдельном файле
.del - Update = delete + insert – старая версия остаётся до merge
- Периодический merge необходим для освобождения места
4. Физические файлы на диске
Lucene индекс – это директория со множеством файлов:
my_index/
├── segments_N # Список активных segments
├── _0.cfs # Compound file segment 0
├── _0.cfe # Compound file entries
├── _1.si # Segment info
├── _1.fdx # Field index
├── _1.fdt # Field data (stored fields)
├── _1.tim # Term dictionary
├── _1.tip # Term index
├── _1.doc # Document frequencies
├── _1.pos # Term positions
└── write.lock # Write lock file
Каждый тип файла отвечает за свою часть данных:
.tim/.tip– term dictionary (словарь терминов).doc– document frequencies (частотность в документах).pos– positions терминов (для phrase queries).fdt/.fdx– stored fields (исходные данные полей).dvd/.dvm– doc values (для сортировки/агрегаций)
Как Lucene выполняет поиск
1. Term lookup:
Query: “elasticsearch AND distributed”
→ Получает posting list для “elasticsearch”: [5, 12, 47, 103]
→ Получает posting list для “distributed”: [12, 47, 89]
→ Пересечение: [12, 47]
Query: "elasticsearch"
→ Читает term dictionary (.tim/.tip)
→ Находит posting list для "elasticsearch"
→ Получает список: [doc5, doc12, doc47, doc103, ...]
2. Boolean queries:
Query: “elasticsearch AND distributed”
→ Получает posting list для “elasticsearch”: [5, 12, 47, 103]
→ Получает posting list для “distributed”: [12, 47, 89]
3. Scoring:
Для каждого найденного документа вычисляет relevance score (BM25 в современных версиях):
- Term frequency (как часто термин встречается в документе)
- Inverse document frequency (насколько редок термин в коллекции)
- Field length normalization (длина поля)
- Boosts (фактор умножения веса)
Связь Lucene ↔ Elasticsearch
Elasticsearch Cluster
└── Indices (логические коллекции данных)
└── Shards (распределённые части индекса)
└── LUCENE INDEX (физический поисковый индекс)
└── Segments (неизменяемые файлы)
Ключевые моменты:
Каждый shard = один Lucene индекс
- Primary shard – это Lucene индекс
- Replica shard – это копия того же Lucene индекса на другом узле
Elasticsearch добавляет поверх Lucene:
- Распределённость (sharding, routing)
- Репликацию (primary/replica)
- REST API
- JSON documents
- Dynamic mapping
- Кластерную координацию
- Aggregations framework
- Security, monitoring, ML и т.д.
Но сам поиск выполняет Lucene.
Ограничения Lucene индекса
Жёсткий лимит: 2,147,483,519 документов (Integer.MAX_VALUE – 128)
Это внутреннее ограничение формата данных Lucene. Document IDs хранятся как 32-bit integers.
Отсюда необходимость шардирования в Elasticsearch:
- Один Elasticsearch индекс может содержать миллиарды документов
- Но он разбивается на шарды
- Каждый шард = отдельный Lucene индекс
- Каждый Lucene индекс < 2.1 млрд. документов
- Суммарно – практически неограниченно
Практический пример
Создадим индекс в Elasticsearch:
PUT /my-index
{
"settings": {
"number_of_shards": 3
}
}
Что происходит на уровне диска:
Node 1:
/data/nodes/0/indices/my-index/0/ ← Shard 0 (Lucene index)
├── segments_3
├── _0.cfs
├── _1.cfs
└── write.lock
Node 2:
/data/nodes/0/indices/my-index/1/ ← Shard 1 (Lucene index)
├── segments_5
├── _0.cfs
├── _1.cfs
├── _2.cfs
└── write.lock
Node 3:
/data/nodes/0/indices/my-index/2/ ← Shard 2 (Lucene index)
├── segments_2
├── _0.cfs
└── write.lock
Три отдельных Lucene индекса, каждый независимый, со своими segments.
Для чего это важно
1. Настройка производительности:
- Слияние сегментов влияет на производительность
- Force merge оптимизирует read-only индексы
- Refresh interval контролирует видимость новых документов
2. Планирование ёмкости:
- Размер сегментов влияет на использование памяти
- Много мелких сегментов = больше нагрузка
- Один огромный сегмент= медленное слияние
3. Troubleshooting (Поиск проблем):
- Медленный поиск может быть из-за слишком многих segments
- Большое количество удалённых документов требует слияния (merge)
- Испорченные сегменты – как восстановить?
4. Архитектурные решения:
- Почему количество шардов нельзя изменить без реиндексирования
- Почему updates дороже inserts (delete + insert в Lucene)
- Почему есть refresh interval (segment становится видимым)
Резюме
Apache Lucene индекс – это высокооптимизированная структура данных для полнотекстового поиска, состоящая из неизменяемых сегментов с инвертированными индексами. Elasticsearch шард – это обёртка над Lucene индексом, добавляющая распределённость, репликацию и удобный API.
Понимание Lucene критично для глубокого понимания Elasticsearch – это фундамент, на котором всё построено.