Поиск, который врёт: почему Elasticsearch показывает «в наличии», когда товара нет
Покупатель ввёл запрос, нашёл нужный товар, кликнул — и увидел «нет в наличии». Ушёл. Написал в поддержку: «у вас сломан поиск». Я открыл логи — поиск работал правильно. Он показывал то, что было в индексе час назад. Проблема не в Elasticsearch. Проблема в том, что мы не подумали, как часто он должен узнавать о переменах.
На каталоге с 28 000 SKU цены меняются каждую ночь из 1С. Остатки — несколько раз в день, иногда раз в час в период акций. Мы три месяца настраивали морфологию, синонимы, фасеты. А потом выяснили, что 4% поисковых сессий заканчиваются разочарованием не из-за нерелевантности, а из-за устаревших данных.
Это не редкий случай. Это системная проблема, которую решают в последнюю очередь.
Почему индекс устаревает, даже когда всё «настроено»
В стандартной Bitrix-архитектуре данные каталога живут в инфоблоках. Цены, остатки, свойства товара — это разные сущности с разными lifecycle. Остатки обновляются через 1С-обмен: файл выгружается, Bitrix парсит его, пишет в БД. Цены могут приходить отдельным потоком. Описания и изображения редактирует менеджер вручную.
Elasticsearch ничего об этом не знает. Он знает только то, что вы ему отправили — и когда.
Если Elasticsearch переиндексируется раз в ночь, к полудню следующего дня он уже содержит вчерашние остатки. При этом в Bitrix уже прошло два 1С-обмена. Товар на поиске — «в наличии». На карточке товара — «нет».
Это разрыв между источником правды и тем, что видит покупатель.
Что считать «достаточно свежим» — по типу данных
Не все поля в индексе одинаково критичны. Это важное разграничение, потому что попытка обновлять всё в реальном времени — это сложность ради сложности.
Остаток (in_stock, quantity) — критично. Задержка больше 15–30 минут в период акций создаёт прямой ущерб: заказы на отсутствующий товар, звонки в поддержку, возвраты.
Цена — важно. Задержка в 1–2 часа приемлема для большинства каталогов, но не для магазинов с динамическим ценообразованием или акциями с таймером. Ошибочная цена в поиске — это или недополученная выручка, или судебный риск.
Описание, характеристики, изображения — некритично. Менеджер поправил текст карточки — поиск обновится в течение дня, ничего не сломается. Обновлять в плановом режиме.
Понимание этой градации позволяет проектировать стратегию осознанно, а не «обновлять всё как можно чаще».
Три стратегии обновления индекса
Полный переиндекс
Самый простой вариант. Cron раз в час (или чаще) запускает скрипт, который выгружает весь каталог и отправляет в Elasticsearch. На каталоге с 28 000 SKU полный переиндекс занимает 8–12 минут при нормальной нагрузке.
Проблема: во время переиндекса поиск либо работает на старых данных, либо не работает вообще, если вы удаляете и пересоздаёте индекс. Решается через alias-переключение: держите два индекса, переключайте alias только после успешной сборки нового. Но это уже усложнение.
Второй минус: нагрузка. Полный переиндекс каждый час — это постоянный фоновый процесс, который конкурирует с пользовательскими запросами и 1С-обменом.
Scheduled batch (delta по timestamp)
Умнее. Вместо полного каталога — только изменения за последние N минут. Bitrix хранит TIMESTAMP_X в таблице инфоблоков — дату последнего изменения элемента. Cron каждые 5 минут забирает товары с TIMESTAMP_X > last_run_time и отправляет их в Elasticsearch.
Это то, что мы используем на большинстве проектов. Работает стабильно, нагрузка предсказуема, задержка — 5–7 минут в обычном режиме.
Но есть нюанс с 1С-обменом: когда 1С обновляет остатки, Bitrix не всегда проставляет TIMESTAMP_X на элемент каталога. Обновление может писаться напрямую в таблицу цен или остатков, минуя основной элемент. Для таких случаев нужен отдельный трекинг изменений по таблицам b_catalog_price и b_catalog_store_product.
Event-driven delta
Самый свежий вариант. Bitrix-события (OnAfterIBlockElementUpdate, хуки 1С-обмена) кладут задачи в очередь — Redis или простую таблицу в MySQL — и воркер обрабатывает её с задержкой в секунды. Задержка от изменения в Bitrix до обновления в Elasticsearch: 5–30 секунд.
Сложность реализации выше. Нужно правильно обработать дублирование событий (один товар может получить 5 событий за секунду при пакетном обмене), race conditions (два события для одного товара в очереди — какое свежее?), и надёжность воркера (что если он упал в середине обработки?).
Оправдан для критичных данных — остатков и цен в магазинах с высокой конкуренцией.
Как мы реализовали delta-индексацию для Bitrix
На каталоге 28 000 SKU мы используем гибрид: scheduled batch каждые 5 минут для описаний и характеристик, event-driven для остатков и цен.
Для scheduled batch: cron-задача забирает из Bitrix товары с DATE_MODIFY > :last_run, формирует Elasticsearch-документы, отправляет batch-запросом через PHP Elasticsearch client. Время выполнения для пакета в 200 изменённых позиций — около 3–4 секунд.
Для event-driven: при каждом 1С-обмене перехватываем событие OnAfterIBlockElementSetPropertyValues (там обновляются количества) и OnIBlockElementAdd/OnIBlockElementUpdate. Пишем iblock_element_id в таблицу очереди с timestamp. Отдельный PHP-скрипт под PM2 (простой воркер без LangGraph и прочего) забирает из очереди пачками по 50 элементов, запрашивает актуальные данные из Bitrix API и пишет в Elasticsearch.
Почему PHP-скрипт под PM2, а не очередь на Redis? Потому что на этом проекте Redis не было в инфраструктуре, а добавлять его ради одного воркера было лишним.
Подводные камни
Гонка при пакетном обмене. 1С выгружает данные файлами, Bitrix обрабатывает их последовательно. Иногда за 10 секунд один товар обновляется трижды. В очереди три задачи. Воркер берёт первую, запрашивает данные из Bitrix — а там уже третье (финальное) состояние. Это нормально. Главное — при запросе в Bitrix брать актуальные данные, а не те, что были в момент события. Не кешировать payload события.
Потеря событий при перезапуске. Если воркер упал в момент обработки пакета, задачи из очереди могут остаться в состоянии «in progress» и не вернуться в очередь. Решение простое: статус «in progress» с TTL — если задача не закрыта за 2 минуты, возвращается в очередь.
Частичное обновление. Elasticsearch-документ содержит и описание, и цену, и остаток. Если обновляете только остаток через event-driven, а описание — через scheduled batch, убедитесь, что при partial update вы используете update API с doc, а не перезаписываете весь документ. Иначе свежеобновлённое описание исчезнет после следующего события по остатку.
Как понять, что индекс врёт
Самый надёжный способ — сравнивать timestamp. В каждом Elasticsearch-документе мы храним поле indexed_at — дата последнего обновления документа в ES. Раз в 10 минут cron-задача берёт 100 случайных товаров, сравнивает indexed_at с DATE_MODIFY в Bitrix. Если разница больше порога (30 минут для описаний, 15 минут для цен, 5 минут для остатков) — алерт в Telegram.
Это не идеальная метрика, но достаточно дешёвая и надёжная. За полгода работы поймали три инцидента до того, как их заметили покупатели.
Elasticsearch умеет искать хорошо. Насколько хорошо он это делает — определяется настройкой маппинга и анализаторов. Насколько правдиво он это делает — определяется стратегией обновления. Это разные задачи, и их нельзя решать одновременно: сначала — что показывать, потом — как быстро обновлять.
Если хотите понять, где у вас теряется конверсия из поиска прямо сейчас, начните с лога нулевых результатов — это быстрее, чем разбираться с freshness.