Поиск это первое место, где большой сайт показывает свою истинную скорость: каталог на сотни тысяч товаров, база на миллионы записей и поток запросов в пиковые часы не прощают наивных решений. Elasticsearch собирается именно под такие нагрузки, но из коробки он даёт лишь потенциал. Реальную скорость получают те, кто разобрался в маппинге, шардинге, типах запросов и привычках листания, и сделал сознательный выбор на каждом слое.
Задача статьи пройти по этим слоям с конкретикой: как устроен индекс, почему анализаторы решают судьбу русской морфологии, как резать шарды без потерь, какие запросы кэшируются, почему десятое тысячное смещение ломает кластер и что с этим делать, и как поток записи настроить без постоянной перегрузки.
Как устроен поисковый индекс и почему поиск находит документы быстро
Основой служит обратный индекс: для каждого термина хранится список документов, в которых он встретился. Поиск по слову это не проход по всей базе, а чтение коротких списков, и потому миллионы документов находятся за миллисекунды. Всё остальное, ранжирование, фильтры, агрегации, надстраивается над этой структурой.
Документы попадают в индекс через анализ: текст режется на термины, приводится к нижнему регистру, стоп-слова отбрасываются, основы слов нормализуются. Анализ происходит один раз при записи, поэтому поиск не тратит время на разбор: он просто сравнивает готовые термины с теми, что пришли в запросе, и эта асимметрия объясняет, почему индекс сначала строится медленно, а потом отвечает мгновенно.
Индекс в кластере режется на шарды, каждый шард это самостоятельная поисковая единица на конкретном узле, а реплики шардов дают две вещи сразу: отказоустойчивость и параллельность чтения. Запрос уходит на все шарды одновременно, результаты схлопываются на координирующем узле, и горизонтальное масштабирование для чтения работает из коробки.
Понимание этой тройки, анализ, шарды, реплики, экономит недели: почти каждая проблема больших поисковых решений раскладывается на вопрос о том, что происходит с текстом на входе, как он разложен по машинам и сколько копий читается одновременно.
Маппинг и анализаторы типы полей и русская морфология без сюрпризов
Маппинг это схема индекса, и небрежность на этом слое оплачивается годами. Главный выбор звучит так: поле для полного текста или поле для точных значений. Первое анализируется и ищется по корням слов, второе хранится как есть и сравнивается целиком, служит фильтрам, сортировке и агрегациям. Смешение этих ролей порождает вечные жалобы: фильтр по категории, объявленной текстом, не находит точное совпадение, потому что значение прошло через анализатор.
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "russian" },
"brand": { "type": "keyword" },
"price": { "type": "scaled_float", "scaling_factor": 100 }
}
}
}
Русская морфология решается готовым анализатором: он понимает падежи и приводит формы к основе, поэтому запрос про наушники находит и наушников, и наушники в тексте. Готовый анализатор покрывает большинство сценариев, а точность доводят пользовательскими: свой список стоп-слов, свой токенизатор для артикулов и сокращений, синонимы в отдельном фильтре.
Динамическое определение типов удобно на старте и опасно в росте: первый случайно записанный документ задаёт тип поля на весь индекс, и лишнее поле вида текст сдирает производительность у фильтров. Зрелая схема явно описывает каждое поле, отключает появление новых типов и ведёт журнал исключений, чтобы сюрпризы разбирал человек, а не кластер.
Числа заслуживают отдельной аккуратности: цены и рейтинги описываются числовыми типами с масштабом, сортировка по ним ничего не стоит, а хранение чисел в текстовых полях отравляет и порядок, и память.
Шарды и реплики как резать индекс без потерь и без излишеств
Число первичных шардов задаётся при создании и не меняется потом без пересборки, поэтому это главное архитектурное решение. Рабочая логика опирается на объём: шард комфортно живёт в десятках гигабайт, и индекс на гигабиты режется на один-два шарда, а на терабайты пересматривается всерьёз. Овершардинг, когда маленький индекс нарезан на тридцать кусков, является классической болезнью: каждая частичка ест память и заставляет кластер бегать по пустым спискам.
Реплики выбирают по нагрузке: одна копия даёт спокойный сон и удвоение чтения, вторая нужна тем, у кого читают с несколькими девятками готовности. Записывают обычно в первичные шарды и читают с любых копий, поэтому рост чтения лечится добавлением реплик и узлов, а рост записи только увеличением первичных шардов и пересборкой.
Файловый кеш это тайный герой скорости: горячие списки индекса живут в памяти страницы, и узел с памятью под размер рабочего множества отвечает в разы быстрее узла, полагающегося на диск. Отсюда практическое правило: лучше меньше индексов, но помещающихся в память, чем много средних, и запрет на подкачку без разговоров.
Для растущих индексов применяется нарезка по времени: свежие сутки или неделя живут в активном индексе, старые уезжают в закрытые, а запрос читает псевдоним, покрывающий нужный горизонт.
Место на диске это отдельный лимит: кластер бережёт себя и перестаёт выдавать реплики при заполнении дисков, сначала на девяносто процентов, затем жёстче. Заполненные узлы не тормозят, они отказывают, поэтому за свободным местом следят как за памятью, и жёсткие объединения старых индексов экономят и кеш, и гигабайты. Такая схема упрощает ротацию, экономит кеш и позволяет жёстко объединять старые куски для экономии места.
Запросы фильтры и полнотекстовый поиск что кэшируется и что считается дорого
Запросы делятся на два класса по стоимости. Полный текст считается с ранжированием: каждое совпадение получает оценку, и это неизбежная плата за уместность. Фильтры не считают релевантность, они отвечают да либо нет, поэтому кластер кэширует их биты и повторные фильтры по категории или цене почти ничего не стоят.
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "wireless headphones" } }
],
"filter": [
{ "term": { "brand": "example" } },
{ "range": { "price": { "lte": 5000 } } }
]
}
}
}
Правило сборки запроса простое: текст внутри, условия снаружи, и категория с ценой всегда живут в фильтрах. Обратный порядок не сломает поиск, но выбросит кэш, будет считать релевантность цен и наградит каталог необъяснимой медлительностью в часы пик.
Дорогими остаются запросы с неопределённым началом: поиск по подстроке в начале которой звёздочка обязан обойти все термины индекса, и на больших словарях это минуты. Потребность в таком поиске закрывают нграммами, префиксными полями или отдельным полем подсказок, но никогда не звёздочкой на живом кластере.
Медленный запрос почти всегда опознаётся по структуре: слишком широкий полный текст, агрегация без ограничений, листание в глубину. Журнал медленных запросов включается настройкой порогов, и его чтение по вечерам даёт больше оптимизаций, чем любая теория.
Листание результатов от классического окна до search_after и композитных агрегаций
Классическое листание с указанием смещения работает быстро у первых страниц: сместиться на сотню строк дёшево. Но смещение на десятитысячную позицию заставляет координатора собрать и выбросить десять тысяч документов, и глубина жёстко ограничена окном по умолчанию в десять тысяч строк.
GET /products/_search
{
"size": 20,
"sort": [{ "price": "asc" }, { "_id": "asc" }],
"search_after": [4990, "prod-1024"]
}
Рабочая альтернатива для глубокой прокрутки это продолжение поиска от последней пары значений сортировки: каждая страница несёт курсор из своих крайних значений, следующая просит всё, что после него, и кластер не выбрасывает гигабайты ради двухсотой страницы. Бесконечные ленты, экспорт и обход каталога строятся именно на этом механизме.
Агрегации листаются собственной конструкцией: композитные агрегации выдают фрагменты по курсору и позволяют обойти все уникальные значения фасетов без взрыва памяти. Классические агрегации по популярности работают отлично на топ-десятках, а на длинных хвостах терминологических сгруппированных значений курсор обязателен.
Пользовательская сортировка добавляет требование к уникальности: курсор строится по значениям, а значения повторяются, поэтому в пару к цене всегда добавляют идентификатор документа, и перемещение становится однозначным на любой глубине.
Фасеты каталога считаются агрегациями, и их стоимость зависит от числа уникальных значений: десяток категорий живёт хорошо, сотни тысяч значений в одной агрегации уже требуют курсоров и ограничений. Рабочая практика ставит размер выборки агрегации в разумный предел, потому что пользователь просит первые пять категорий, а не все пятьдесят тысяч.
Поток записи bulk обновление индекса и жизненный цикл
Пишут в поисковый индекс пакетами: одиночные документы это вчерашний день, пакетный формат заливает тысячи записей одним обращением. Рабочий размер пакета измеряется мегабайтами, а не миллионами строк, и подбирается наблюдением за отказами и временем ответа, потому что слишком крупный пакет кладёт и клиента, и кластер.
Свежесть поиска настраивается интервалом обновления индекса: по умолчанию изменения видны уже через секунду, что удобно в разработке и дорого при тяжёлой заливке. Массовые заливки поднимают интервал до десятков секунд или минуты, заливают всё, затем вызывают обновление вручную и возвращают настройку на место, и скорость записи вырастает в разы без потери итоговой свежести.
Индексы, которые больше не меняются, жёстко объединяются: количество сегментов внутри шарда сводится к одному, размер сжимается, кеш используется плотнее, и читающий архив перестаёт платить за историю изменений. Это финальная глава жизненного цикла, и автоматизация жизненного цикла делает её рутиной: свежие индексы на горячих узлах, тёплые на медленных дисках, закрытые объединены и забыты.
Пересборка больших индексов всегда идёт через псевдоним: новый индекс строится рядом, наполняется, проверяется, и псевдоним переключается одной командой на новый. Пользователи не видят ни секунды простоя, а откат занимает одну строку, и этот приём снимает страх перед любым изменением схемы.
Мониторинг и медленный лог как найти что тормозит до жалоб пользователей
Медленный журнал это первый инструмент: порог в сотни миллисекунд на запросы и на запись собирает точный список проблемных операций, и по этому списку работают. Профилировщик запроса дополняет картину, раскладывая время по фазам, и два-три вечера с этими инструментами обычно удваивают скорость каталога.
Статистика кластера отвечает на вопросы планирования: размеры шардов, память, нагрузка по узлам, размеры очередей. Равномерность важнее суммы: один перегретый узел ломает всё, и своевременное перераспределение решает проблемы, которые не лечатся никаким тюнингом запросов.
Тренды важнее точечных чисел: время ответа, отложенное от пикового, и доля медленных запросов по неделям показывают, деградирует ли поиск или просто переживает распродажу. Пара графиков на стену команды экономит часы разбирательств по каждому всплеску.
Порядок оптимизации большого поиска свёрнут в короткую лестницу:
- составить маппинг с явными типами и анализатором под язык сайта;
- выбрать число шардов по объёму и горизонту роста индекса;
- перевести условия категорий и цен в фильтры с кэшированием;
- заменить глубокие смещения на продолжение поиска по курсору;
- настроить пакетную запись и интервал обновления под свой поток;
- включить медленный журнал и установить пороги по страницам;
- автоматизировать жизненный цикл и жёсткое объединение старых индексов.
Оптимизация поиска это не разовый подвиг, а дисциплина измерений: каждая правка проверяется цифрами, каждая догадка сравнивается с журналом, и кластер, за которым так ухаживают, отвечает страницами за десятки миллисекунд даже в пятничный вечер. Ради этого ощущения, когда каталог летает, всё и затевалось.