Поиск это первое место, где большой сайт показывает свою истинную скорость: каталог на сотни тысяч товаров, база на миллионы записей и поток запросов в пиковые часы не прощают наивных решений. 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 обновление индекса и жизненный цикл

Пишут в поисковый индекс пакетами: одиночные документы это вчерашний день, пакетный формат заливает тысячи записей одним обращением. Рабочий размер пакета измеряется мегабайтами, а не миллионами строк, и подбирается наблюдением за отказами и временем ответа, потому что слишком крупный пакет кладёт и клиента, и кластер.

Свежесть поиска настраивается интервалом обновления индекса: по умолчанию изменения видны уже через секунду, что удобно в разработке и дорого при тяжёлой заливке. Массовые заливки поднимают интервал до десятков секунд или минуты, заливают всё, затем вызывают обновление вручную и возвращают настройку на место, и скорость записи вырастает в разы без потери итоговой свежести.

Индексы, которые больше не меняются, жёстко объединяются: количество сегментов внутри шарда сводится к одному, размер сжимается, кеш используется плотнее, и читающий архив перестаёт платить за историю изменений. Это финальная глава жизненного цикла, и автоматизация жизненного цикла делает её рутиной: свежие индексы на горячих узлах, тёплые на медленных дисках, закрытые объединены и забыты.

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

Мониторинг и медленный лог как найти что тормозит до жалоб пользователей

Медленный журнал это первый инструмент: порог в сотни миллисекунд на запросы и на запись собирает точный список проблемных операций, и по этому списку работают. Профилировщик запроса дополняет картину, раскладывая время по фазам, и два-три вечера с этими инструментами обычно удваивают скорость каталога.

Статистика кластера отвечает на вопросы планирования: размеры шардов, память, нагрузка по узлам, размеры очередей. Равномерность важнее суммы: один перегретый узел ломает всё, и своевременное перераспределение решает проблемы, которые не лечатся никаким тюнингом запросов.

Тренды важнее точечных чисел: время ответа, отложенное от пикового, и доля медленных запросов по неделям показывают, деградирует ли поиск или просто переживает распродажу. Пара графиков на стену команды экономит часы разбирательств по каждому всплеску.

Порядок оптимизации большого поиска свёрнут в короткую лестницу:

  1. составить маппинг с явными типами и анализатором под язык сайта;
  2. выбрать число шардов по объёму и горизонту роста индекса;
  3. перевести условия категорий и цен в фильтры с кэшированием;
  4. заменить глубокие смещения на продолжение поиска по курсору;
  5. настроить пакетную запись и интервал обновления под свой поток;
  6. включить медленный журнал и установить пороги по страницам;
  7. автоматизировать жизненный цикл и жёсткое объединение старых индексов.

Оптимизация поиска это не разовый подвиг, а дисциплина измерений: каждая правка проверяется цифрами, каждая догадка сравнивается с журналом, и кластер, за которым так ухаживают, отвечает страницами за десятки миллисекунд даже в пятничный вечер. Ради этого ощущения, когда каталог летает, всё и затевалось.