Play
online · 30 мин
Туториалы 24 мин чтения 11.09.2026

Не работает поиск на Битрикс: фасетный индекс, переиндексация и модуль search

Поиск на сайте сломался? В Битрикс за это отвечают два независимых механизма — модуль search и фасетный индекс. Разбираем, как быстро найти причину и вернуть клиентам возможность находить товары.

Битрикс не работает поиск: с чего начать диагностику

К нам регулярно приходят с одной и той же формулировкой: «поиск на сайте сломался». За ней обычно стоит каталог на 20–50 тысяч SKU, живой импорт из 1С и жалоба менеджера, что клиент не находит половину товаров. В наших проектах с большим каталогом такая задача всплывает почти всегда после очередного массового обновления номенклатуры. И почти всегда причина не в самом поиске, а в том, что его данные разъехались с реальным содержимым инфоблока.

Ключевой момент, который экономит часы диагностики: в Битрикс за поиск по сайту отвечают два независимых механизма. Первый — модуль search, он строит полнотекстовый индекс по контенту (то, что выдаёт строка поиска). Второй — фасетный индекс инфоблоков, на котором работают фильтр и умный фильтр каталога. Наполняются они разными агентами, хранятся в разных таблицах и ломаются по отдельности.

Поэтому симптом «не работает поиск» почти всегда означает одно из двух: либо один из этих индексов не построен, либо он устарел после импорта и отдаёт старые данные. Прежде чем лезть в код компонента или в шаблон, стоит пройти короткую диагностику. Она занимает 10–15 минут и сразу показывает, где именно разрыв.

Вот пять шагов, с которых мы начинаем разбор у клиента:

  1. Проверить у элементов каталога поле Index_element — оно должно быть в значении Y (это дефолт, но импорт иногда его перетирает).
  2. Зайти в настройки модуля Поиск (Настройки → Настройки продукта → Настройки модулей → Поиск) и посмотреть, какая поисковая система выбрана и включена ли морфология.
  3. Запустить тестовый CSearch::Search с MODULE_ID = 'iblock' — это покажет, что реально лежит в индексе, минуя шаблон и права.
  4. Проверить наличие фасетного индекса у каталога через \Bitrix\Iblock\PropertyIndex\Manager — если его нет, умный фильтр и часть выборок работать не будут.
  5. Посмотреть логи агента переиндексации: когда он последний раз отрабатывал и не падает ли по таймауту.

Минимальный диагностический скрипт, который отвечает на главный вопрос — есть ли данные в индексе:

PHP
<?php
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');

use Bitrix\Main\Loader;
Loader::includeModule('search');

$query = 'название товара'; // замените на реальный запрос, который «не находит»
$siteId = 's1';              // замените на ваш SITE_ID

$obSearch = new CSearch();
$obSearch->Search(array(
        'QUERY'     => $query,
        'SITE_ID'   => $siteId,
        'MODULE_ID' => 'iblock',
));

echo 'Найдено записей: ' . (int)$obSearch->selectedRowsCount() . PHP_EOL;

while ($row = $obSearch->Fetch()) {
    echo $row['TITLE'] . ' — ' . $row['URL'] . PHP_EOL;
}

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

Конструктор CSearch отдаёт только те записи, которые доступны текущему пользователю согласно его уровню доступа. Админ под своей учёткой может видеть больше, чем гость. Проверяйте сценарий под тем пользователем, который жалуется, а не под администратором.

Отдельно стоит держать в голове, что запросы к CSearch::Search с MODULE_ID = false ищут по всем модулям сразу — для диагностики инфоблоков это лишний шум, поэтому в скрипте мы явно ограничиваем выборку каталогом. И помните: этот скрипт только читает, он ничего не перестраивает и безопасен для запуска на проде.

Подробнее: ускорить битрикс.

Подробнее: как обновить 1с битрикс.

Фасетный индекс Битрикс: зачем он нужен и как его построить

Самый частый симптом, с которым приходят после жалобы «фильтр тормозит»: умный фильтр на каталоге в 30 тысяч SKU открывается по 8–15 секунд, а часть значений свойств вообще не отображается в списке. Причина почти всегда одна — фасетный индекс не построен или построен частично. Фасетный индекс — это отдельная структура в модуле «Информационные блоки», появившаяся в версии 15.0.1. Он строится по значениям свойств каталога и используется классом Bitrix\Iblock\PropertyIndex\Facet для быстрой фильтрации. Без него компонент умного фильтра вынужден каждый раз обходить все свойства всех элементов инфоблока, чтобы посчитать доступные значения и количество товаров по каждому из них.

Разница ощущается не в «скорости на пару процентов», а в порядке величины: с индексом фильтр читает заранее посчитанную таблицу соответствий, без него — строит тяжёлые JOIN-запросы по b_iblock_element_prop_s*. Именно поэтому на каталогах от 20–30 тысяч позиций отсутствие фасетного индекса превращается в заметную проблему, а на 100 тысячах — в нерабочий фильтр.

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

Сколько места занимает фасетный индекс

Первый вопрос, который возникает у администратора перед запуском: «а не разрастётся ли база?». По документации Битрикса, для каталога на 520 тысяч записей данные фасетного индекса занимают около 12 Мбайт, сами индексы — 30 Мбайт, а средняя длина строки в индексной таблице — 24 байта. Это ориентир, чтобы не пугаться размеров таблиц b_iblock_element_prop_s* и b_iblock_element_prop_m*, которые рядом выглядят куда внушительнее.

На практике на каталоге в 30–50 тысяч SKU фасетный индекс — это единицы мегабайт данных и десятки мегабайт индексов, что несопоставимо с объёмом самих свойств товаров. Так что решение строить его или нет — вопрос не дискового места, а корректности работы фильтра. Если места на диске впритык, стоит сначала посмотреть на другие таблицы, а не экономить на фасетном индексе.

Как запустить построение индекса через агент

Построение фасетного индекса — операция пошаговая: за один проход агент обрабатывает порцию элементов и запоминает позицию, чтобы продолжить с неё в следующий запуск. Запускается это через \Bitrix\Iblock\PropertyIndex\Manager::createIndexer($iblockId), который возвращает объект-индексатор.

PHP
<?php
use Bitrix\Iblock\PropertyIndex\Manager;

// ID инфоблока каталога — замените на ваш
$iblockId = 15;

$indexer = Manager::createIndexer($iblockId);

// Запускаем построение — индексатор сам разложит работу по шагам агента
$indexer->start();

// В цикле прогоняем шаги, пока индексатор не сообщит, что закончил
while ($indexer->continue()) {
    $indexer->next();
}

// После завершения — фиксируем результат
$indexer->end();

Объект-индексатор работает пошагово через агента, поэтому на больших каталогах построение может занять несколько часов. Запускать его в разгар рабочего дня не стоит: пока индекс строится, часть запросов фильтра будет работать медленнее обычного, а на слабом сервере это заметят и посетители, и менеджеры.

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

Если у вас несколько инфоблоков с фильтрацией (например, основной каталог и раздел сопутствующих товаров), индексатор создаётся отдельно для каждого $iblockId — один на все инфоблоки не работает.

Грабля с обновлением разделов. Если после CIBlockElement::SetElementSection() не вызвать PropertyIndex\Manager::updateElementIndex($iblockId, $elementId), товар останется привязанным к старому разделу в фасетном индексе. Внешне всё в порядке — карточка товара открывается, раздел в админке обновился, — но в умном фильтре товар пропадёт из нового раздела и продолжит «висеть» в старом. Лечится это только вызовом updateElementIndex() после каждой смены привязки, либо полной перестройкой индекса инфоблока.

Переиндексация Битрикс: CSearch::ReIndexAll и CSearch::Index

Типичный сценарий: после крупного импорта или правки шаблона поиск перестал находить половину каталога. Часть товаров в индексе устарела, часть вообще выпала. На форумах и в первой попытке решения всплывает одно слово — «переиндексация». И почти сразу за ним путаница: почему в одних примерах вызывают CSearch::ReIndexAll, а в других CSearch::Index? Это не взаимозаменяемые методы, а два разных инструмента под две разные задачи.

Переиндексация в Битрикс — это не одна кнопка, а два метода с разной областью действия. CSearch::ReIndexAll(bFull, max_execution_time, NS, clear_suggest) запускает пошаговую переиндексацию всего сайта — всех модулей, которые передают данные в поиск. А CSearch::Index пересобирает одну конкретную позицию, идентифицируемую парой MODULE_ID и ITEM_ID. Выбор метода зависит от того, что именно у вас сломалось: весь поиск целиком или один товар, у которого после правки не обновились данные.

Разница принципиальная — по времени, по нагрузке и по ситуации применения.

Метод Что делает Когда использовать Ключевые параметры
CSearch::ReIndexAll Пошаговая переиндексация всего сайта Сменили структуру данных, массовый импорт, поиск «сломался» глобально bFull, max_execution_time, NS, clear_suggest
CSearch::Index Пересобирает одну позицию по MODULE_ID и ITEM_ID Изменился один товар или новость — не нужно гонять весь сайт MODULE_ID, ITEM_ID, SITE_ID
CSearch::ReindexModule Переиндексация одного модуля целиком Данные менял только один модуль — например, инфоблоки MODULE_ID, bFull

Пошаговая переиндексация через CSearch::ReIndexAll

Главная особенность ReIndexAll в том, что он не пытается переиндексировать весь сайт за один вызов. Метод делает один шаг, возвращает состояние в переменной $NS и завершается. Ваша задача — вызывать его в цикле, передавая $NS между итерациями, пока он не вернёт true. Это защищает от таймаута на больших каталогах.

PHP
<?php
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');

use Bitrix\Main\Loader;

Loader::includeModule('search');

$NS = false;
$step = 0;

do {
    // bFull=true — очистить индекс перед началом, false — инкрементально
    // max_execution_time=20 — ограничение шага по времени в секундах
    // NS — состояние между шагами (передаём по ссылке)
    // clear_suggest=false — не трогать подсказки поиска
    $NS = CSearch::ReIndexAll(true, 20, $NS, false);
    $step++;

    if ($NS === true) {
        break; // переиндексация завершена
    }

    // Небольшая пауза, чтобы не грузить БД на 100%
    usleep(200000);
} while (true);

echo "Готово, шагов: {$step}\n";

Переиндексация одной позиции через CSearch::Index

Если меняется один товар — гонять весь сайт бессмысленно. CSearch::Index пересобирает индекс ровно для одной записи: передаёте MODULE_ID (для инфоблоков — 'iblock'), ITEM_ID (ID элемента) и SITE_ID. Метод сам подтянет актуальные данные из источника и обновит запись в поисковом индексе.

PHP
<?php
use Bitrix\Main\Loader;

Loader::includeModule('search');

$elementId = 12345;   // ID товара, который изменили
$siteId    = 's1';    // замените на ваш SITE_ID

$result = CSearch::Index('iblock', $elementId, $siteId);

if ($result) {
    // индекс обновлён
} else {
    // запись не попала в индекс — проверьте Index_element у элемента
}

Обратите внимание: если у элемента инфоблока поле Index_element установлено в N, метод молча ничего не сделает — элемент просто исключён из индексации. Это первое, что стоит проверить, когда точечная переиндексация «не срабатывает».

Переиндексация по Cron: как не уронить сайт

На каталоге от 20 тысяч SKU переиндексация через браузер почти всегда отваливается: PHP-таймаут, обрыв соединения, «белый экран» на середине процесса. Мы видели, как администратор жмёт «Переиндексировать» в админке, ждёт минуту и получает 504 — а индекс остаётся в недособранном состоянии, и поиск начинает находить половину товаров. Стандартное решение — вынести CSearch::ReIndexAll в отдельный CLI-скрипт и запускать его по Cron. Тогда переиндексация идёт фоном, шагами, и не мешает посетителям.

Задача решается в три шага: сам скрипт, строка в crontab и защита от наложения запусков.

Ниже — рабочий скелет, который можно сразу положить в проект.

PHP
<?php
// /local/cron/reindex.php
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_CRONTAB', true);
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');

set_time_limit(0);
@ignore_user_abort(true);

$stateFile = $_SERVER['DOCUMENT_ROOT'] . '/local/cron/reindex.state';
$lockFile  = $_SERVER['DOCUMENT_ROOT'] . '/local/cron/reindex.lock';

// Блокировка: второй запуск молча выходит
$fp = fopen($lockFile, 'c');
if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
    fwrite(STDERR, "Reindex already running\n");
    exit(1);
}

// Читаем сохранённое состояние ($NS) с прошлого шага
$NS = is_file($stateFile) ? unserialize(file_get_contents($stateFile)) : false;

CModule::IncludeModule('search');
$NS = CSearch::ReIndexAll(false, 20, $NS);

if (is_array($NS)) {
    // Шаг не завершён — сохраняем $NS и продолжим в следующий запуск
    file_put_contents($stateFile, serialize($NS));
    fwrite(STDERR, "Step done, continue next run\n");
} else {
    // false — переиндексация завершена, чистим состояние
    @unlink($stateFile);
    fwrite(STDERR, "Reindex complete\n");
}

flock($fp, LOCK_UN);
fclose($fp);
SHELL
# Переиндексация раз в 10 минут, лог в файл
*/10 * * * * /usr/bin/php -f /home/bitrix/www/local/cron/reindex.php >> /home/bitrix/www/local/cron/reindex.log 2>&1

Ключевой момент — периодичность Cron должна быть чуть больше max_execution_time, переданного в ReIndexAll. Мы передаём 20 секунд, а запускаем раз в 10 минут. Запас рекомендуется для корректной работы: если шаг не успел завершиться, следующий запуск может начать индексировать те же записи. На больших каталогах параллельная запись в таблицы b_search_content и b_search_index создает избыточную нагрузку, что может привести к нежелательным задержкам и конфликтам при обращении к базе данных.

Поэтому состояние $NS храним не в памяти процесса, а в файле — так его подхватит следующий запуск. И оборачиваем весь скрипт в flock с флагом LOCK_NB: если предыдущий процесс ещё держит блокировку, новый просто выходит с кодом 1, ничего не делая. Это дешевле любого «умного» планировщика и работает на любом хостинге.

Варианты под проект. Если сервер на нескольких нодах и flock не работает (разные контейнеры, NFS без поддержки блокировок) — переходим на распределённый лок через Redis SETNX с TTL, равным таймауту шага. Если каталог небольшой и укладывается в один шаг — можно упростить: без файла состояния, только flock. Если переиндексация запускается после импорта — вызывайте тот же скрипт через exec() из агента, а не дублируйте логику в обработчике.

Морфологический поиск и Error_on_empty_stem: почему не находит по падежам

Самый частый запрос на эту тему звучит так: «ввожу «кроссовки» — находит только «кроссовок», а если ввести «кроссовками», вообще пусто». Это не баг и не ошибка индексации. Модуль search по умолчанию работает в режиме точного совпадения слов, и пока стемминг не активирован, «кроссовок» и «кроссовки» для него — два разных слова, никак не связанных между собой. Покупатель этого не знает, менеджер каталога тоже, а в поддержку прилетает тикет «поиск сломался».

Морфологический поиск включается флагом в настройках модуля «Поиск» (Настройки → Настройки продукта → Настройки модулей → Поиск). Внутри он опирается на стемминг: символы, которые участвуют в обработке слова, возвращает функция "stemming_letter_".$sLang, а сами правила берутся из настроек модуля. Как только флаг стоит — «кроссовки», «кроссовками», «кроссовках» начинают находить один и тот же товар, потому что все три формы редуцируются к общему стему.

PHP
<?php
// init.php или отдельный скрипт настройки поиска
use Bitrix\Main\Loader;

Loader::includeModule('search');

// Управляем поведением поиска, когда стем не найден.
// false — система не прерывает поиск, а отдаёт результаты
// по исходной словоформе (мягкий режим).
CSearch::SetOptions([
        'Error_on_empty_stem' => false,
]);

Разберём, что здесь ключевое. Опция Error_on_empty_stem — это не про включение морфологии, а про то, как вести себя, если стем для слова не построился. Такое бывает с аббревиатурами, транслитерацией, редкими заимствованиями. При true поиск по такому слову фактически завершается ошибкой и ничего не возвращает; при false он откатывается к точному совпадению и хотя бы что-то находит. Для каталога с большим количеством брендов и моделей мы почти всегда ставим false — пустая выдача раздражает покупателя сильнее, чем неидеальное совпадение.

Отдельный момент, который всплывает на мультиязычных сайтах: стемминг настраивается для каждого языка отдельно. Русский и английский обрабатываются разными наборами правил. Если сайт ведёт две языковые версии, а флаг морфологии включён только для одной — на второй поиск будет вести себя как точный. В проектах с двумя-тремя языковыми версиями это регулярно становится источником тикетов «на английской версии поиск хуже, чем на русской». Проверяется в тех же настройках модуля: набор языков и включённость стемминга по каждому.

Стемминг не работает для артикулов и числовых кодов. «AB-1234» и «AB1234» для морфологии — два разных слова, и никакая настройка их не сблизит: дефис и пробел разбивают токен до того, как стеммер вообще получит слово. Если клиент ищет товар по артикулу — заводите отдельное свойство с включённой опцией «Участвует в поиске» либо пишите свой обработчик, который нормализует запрос до сравнения.

Поиск по иерархии разделов: как искать товары в подкатегориях

Представим каталог, где обувь разложена по дереву: «Обувь» → «Спортивная обувь» → «Кроссовки для бега». Покупатель вводит в поиске «обувь» и получает пустоту — хотя в каталоге тысячи пар. Стандартный поиск по инфоблокам не учитывает иерархию разделов. Модуль search индексирует поля элемента: название, описание, свойства, привязанные разделы. Названия родительских категорий в этот список не попадают. Товар в подкатегории «Кроссовки для бега» не найдётся по запросу «Обувь», потому что строки «Обувь» в его индексируемых данных просто нет.

Мы сталкиваемся с этим на проектах с глубокой структурой каталога, обычно от трёх уровней вложенности и выше. Чем аккуратнее менеджеры разложили товары по подкатегориям, тем заметнее проблема: чем глубже дерево, тем реже покупатель угадывает точное название конечной категории. Решается это отдельным строковым свойством «Цепочка разделов для поиска» с включённой опцией «Участвует в поиске». В это свойство мы пишем полный путь разделов, и запрос «Обувь» начинает находить товар в любой подкатегории. Хорошо комбинируется с морфологическим поиском и с настройкой фасетного индекса, если нужно ещё и фильтровать по этим же категориям.

PHP
<?php
// init.php — запись цепочки разделов в свойство при сохранении товара
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;

Loader::includeModule('iblock');

$IBLOCK_ID = 15;        // замените на ваш IBLOCK_ID
$PROPERTY_CODE = 'SECTION_CHAIN'; // строковое свойство «Цепочка разделов для поиска»

$handler = function (&$arFields) use ($IBLOCK_ID, $PROPERTY_CODE) {
    if ((int)$arFields['IBLOCK_ID'] !== $IBLOCK_ID) {
        return;
    }

    $sectionIds = array_filter((array)($arFields['IBLOCK_SECTION'] ?? []));
    if (!$sectionIds) {
        return;
    }

    $chain = [];
    foreach ($sectionIds as $sectionId) {
        $nav = CIBlockSection::GetNavChain($IBLOCK_ID, (int)$sectionId, ['ID', 'NAME']);
        while ($section = $nav->Fetch()) {
            $chain[] = $section['NAME'];
        }
    }
    $chain = array_unique(array_filter($chain));

    // Пишем в свойство, не затирая значения пользователя
    $arFields['PROPERTY_VALUES'][$PROPERTY_CODE] = [
        'n0' => ['VALUE' => implode(' ', $chain)],
    ];
};

EventManager::getInstance()->addEventHandler(
    'iblock',
    'OnAfterIBlockElementAdd',
    $handler
);
EventManager::getInstance()->addEventHandler(
    'iblock',
    'OnAfterIBlockElementUpdate',
    $handler
);

Ключевое здесь — CIBlockSection::GetNavChain(): он возвращает путь от корня до текущего раздела. Мы собираем имена всех разделов цепочки, склеиваем их в строку через пробел и кладём в свойство. Дальше модуль search индексирует это свойство как обычный контент, так же, как индексирует название или описание. Запрос «Обувь» находит товар в любой подкатегории, потому что строка «Обувь» физически присутствует в индексируемых данных.

Что стоит адаптировать под свой проект. Во-первых, $IBLOCK_ID: если инфоблоков несколько, обработчик удобнее вынести в отдельный класс и передавать список ID массивом. Во-вторых, разделитель. Пробел работает с морфологией, но если в названиях категорий бывают предлоги и союзы, иногда лучше склеивать через запятую. В-третьих, если товар лежит сразу в нескольких разделах, наш код объединяет все цепочки. Для поиска это правильно, но при больших деревьях строка может разрастись. По опыту, для каталогов до 50 тысяч SKU это не проблема, а вот на миллионных каталогах свойство лучше ограничивать двумя-тремя цепочками. Ещё вариант — писать цепочку не только в свойство, но и в скрытое поле описания элемента, если по каким-то причинам нельзя завести новое свойство.

Sphinx, MySQL и Bitrix: какую поисковую систему выбрать

В настройках модуля «Поиск» (Настройки > Настройки продукта > Настройки модулей > Поиск) можно выбрать одну из четырёх систем: Bitrix, Sphinx (с версии 14.0.0), OpenSearch или Полнотекстовый поиск MySQL (с версии 14.0.1). Раньше выбор был между «родным» индексом и MySQL, но с 14.x появились внешние движки — и это добавило вопросов.

Разработчики обычно ставят Sphinx «на всякий случай» ещё до того, как каталог перевалит за 10 тысяч SKU, а потом удивляются: почему релевантность стала хуже, чем на встроенном поиске Bitrix. Или наоборот — тянут на MySQL каталог в 80 тысяч позиций и ждут от него скорости. Оба сценария — следствие того, что выбор делали по принципу «что моднее», а не по объёму и структуре данных.

Дальше разберём, какую систему под какой проект имеет смысл ставить — по опыту, а не по маркетингу. Выбор влияет на три вещи: скорость выдачи, качество морфологии и сложность поддержки (отдельный сервис или нет).

Система Когда выбирать Плюсы Минусы Сложность настройки
Bitrix Малые каталоги, простой контент Из коробки, без внешних сервисов Медленнее на больших объёмах Низкая
MySQL fulltext До 20–30k SKU, минимум морфологии Быстрее встроенного, ничего ставить не надо Слабая морфология, ограничения InnoDB Низкая
Sphinx Большие каталоги, высокая нагрузка Скорость, гибкие веса, свои стеммеры Отдельный сервис, свои индексы Высокая
OpenSearch Уже есть в инфраструктуре Мощный анализ текста, кластеризация Ресурсоёмкий, требует опыта эксплуатации Высокая

Для каталога до 20–30 тысяч SKU обычно хватает встроенного Bitrix или полнотекстового MySQL — оба варианта не требуют отдельного сервиса, индексация идёт через тот же CSearch::ReIndexAll, который мы разбирали выше. MySQL-поиск чуть быстрее на простых запросах, но морфологию обрабатывает хуже: падежи и словоформы в нём ловятся грубее, чем в стеммере Bitrix.

Sphinx даёт заметный выигрыш на объёмах от нескольких десятков тысяч позиций и при высокой нагрузке на поиск. Но за скорость приходится платить: это отдельный сервис (демон searchd), свои конфиги, свои индексы, отдельная переиндексация по крону. Модуль Bitrix в этом случае только передаёт данные в Sphinx — сам поиск идёт уже на стороне движка. Без человека, который будет это сопровождать, Sphinx легко превращается в «поставили и забыли, а потом он отвалился».

OpenSearch стоит выбирать в одном случае — если он уже используется в вашей инфраструктуре для других задач (логи, аналитика, поиск по документации). Ставить его с нуля ради поиска по каталогу Bitrix — почти всегда оверкилл: ресурсов он ест больше, чем Sphinx, а выигрыш в релевантности для типового интернет-магазина не оправдывает затрат на эксплуатацию.

Самые частые ошибки, которые ломают поиск на Битрикс

За годы работы с Битриксом у нас сложился негласный «топ» причин, по которым поиск формально включён, но не находит товары. Симптомы разные: «пусто по артикулу», «находит только половину каталога», «после импорта пропали новые позиции». Корень почти всегда один — индекс не соответствует данным каталога. Модуль search не обходит сайт сам, он работает только с тем, что в него положили инфоблоки и кастомные модули. Если элемент не отдал данные в индекс, его там не будет. Сколько бы раз вы ни жали «Переиндексировать».

Мы собрали ошибки, которые регулярно всплывают при аудите поиска у магазинов с активным импортом и большим каталогом. Часть из них про настройки, часть про код, часть про порядок операций после массовых изменений. Ниже матрица «ошибка → причина → как исправить», а дальше разбираем каждую подробнее.

Ошибка Причина Как исправить
Index_element='N' у части товаров Флаг снят импортом или вручную Вернуть 'Y' и переиндексировать
Нет обработчика OnReIndex Кастомный модуль не отдаёт данные Зарегистрировать обработчик события
Устаревший фасетный индекс Массовый импорт без обновления Пересобрать через Manager::updateElementIndex
Не находит по падежам Отключён морфологический поиск Включить стемминг в настройках модуля
Пустая выдача по каталогу Неверный MODULE_ID в CSearch::Search Передать 'iblock' явно

Разберём каждую строку подробнее, с прицелом на то, где именно в проекте это чинится.

Первая и самая коварнаяIndex_element='N' у части товаров. Поле Char(1) в структуре инфоблока по умолчанию равно Y, но импорт из 1С или сторонний парсер легко сбрасывает его в N. Например, если в выгрузке пришло пустое значение, а логика обновления не отличает «не передано» от «выключить индексацию». Такие товары выпадают из поиска, хотя на сайте отображаются. Лечится массовым обновлением флага с последующей переиндексацией.

Вторая — отсутствие обработчика OnReIndex у кастомного модуля. Событие вызывается во время CSearch::ReindexModule и CSearch::ReIndexAll. Если ваш модуль хранит данные в своих таблицах, но не подписан на это событие, в индекс они не попадут никогда. Регистрируем через EventManager в init.php или в module.php самого модуля.

Третья — устаревший фасетный индекс после массового импорта. Фасетный поиск появился в модуле «Информационные блоки» с версии 15.0.1, и после CIBlockElement::SetElementSection() обязательно нужен вызов PropertyIndex\Manager::updateElementIndex($iblockId, $elementId). Пропустили — умный фильтр и часть поисковых сценариев работают по старым данным.

Четвёртая — отключённый морфологический поиск. Флаг в настройках модуля «Поиск», и без него «кроссовками» не найдёт «кроссовки». Пятая — неверный MODULE_ID в CSearch::Search. Если передать false, поиск идёт по всем модулям, а если указать не тот код — выдача окажется пустой. Для каталога на инфоблоках передаём 'iblock' явно.

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

Чеклист: что проверить, если поиск на Битрикс снова сломается

Когда поиск уже настраивали, а через месяц он снова «не находит», проходить диагностику с нуля смысла нет. У нас в студии для повторных обращений есть короткий чеклист — семь точек, которые покрывают 90% причин. Прогон занимает минут десять.

Сразу видно, где копать: в модуле search, в фасетном индексе или в шаблоне компонента. Дальше по результату: либо правим настройки, либо запускаем переиндексацию, либо идём в код шаблона.

  • Проверить поле Index_element у товаров — оно должно быть Y, иначе элемент не попадёт в индекс.
  • Открыть настройки модуля «Поиск» и убедиться, что выбрана нужная поисковая система и не слетели галочки.
  • Запустить CSearch::Search с MODULE_ID = 'iblock' и посмотреть, возвращает ли метод записи вообще.
  • Проверить наличие фасетного индекса у каталога — без него умный фильтр и часть поиска работают вслепую.
  • Посмотреть дату последней переиндексации: если она старше последнего импорта, индекс устарел.
  • Проверить логи агента createIndexer — зависший агент молча оставляет индекс недопостроенным.
  • Убедиться, что морфология включена, иначе поиск по падежам снова даст пустоту.

Идём строго сверху вниз и на каждом пункте фиксируем результат. Симптомы часто накладываются друг на друга. Если CSearch::Search возвращает записи, а сайт по-прежнему ничего не находит, значит индекс в порядке, и проблема в шаблоне компонента или в правах доступа текущего пользователя — конструктор класса CSearch фильтрует выдачу по уровню доступа. Смотрим .parameters.php компонента и проверяем, не сузили ли выборку по разделам или типам.

Если метод возвращает ноль записей, индекс пуст или устарел. Дальше идём в переиндексацию: CSearch::ReIndexAll(true) чистит и строит заново, CSearch::Index обновляет одиночную позицию. Отдельно проверьте Index_element — массовое обнуление этого поля после импорта встречается регулярно. И про морфологию не забываем: при выключенном флаге в настройках модуля поиск не найдёт «кроссовки», если в индексе лежит «кроссовок».

Частые вопросы

Можно ли запустить переиндексацию только для одного инфоблока, а не для всего сайта?

Да, передайте в CSearch::ReIndexAll() первым аргументом ID инфоблока или массив ID, а вторым — false, чтобы не сбрасывать уже существующий индекс. Для точечного обновления одного элемента используйте CSearch::Index($elementId, $iblockId) или CSearch::ReIndex($elementId, $iblockId).

Что делать, если после переиндексации поиск всё равно не находит товары, хотя они есть в каталоге?

Проверьте, что у элементов заполнены поля, участвующие в индексе (NAME, PREVIEW_TEXT, DETAIL_TEXT, а также свойства, добавленные в настройках модуля search), и что инфоблок не исключён в настройках модуля. Также убедитесь, что элементы активны и доступны по правам текущего пользователя — поиск учитывает права доступа.

Чем отличается фасетный индекс от обычного индекса модуля search?

Обычный индекс search хранит содержимое элементов для полнотекстового поиска, а фасетный индекс (таблица b_search_content_facet_index) строится по значениям свойств и используется для фильтрации и умного поиска с учётом параметров. Фасетный индекс строится отдельно через CSearch::ReIndexAll() с флагом фасетов или через агент, и без него фильтры в умном фильтре работают медленно или не работают вовсе.

А если у меня поиск на Sphinx — нужно ли всё равно строить фасетный индекс Битрикс?

Да, фасетный индекс и модуль search остаются нужны для фильтрации и логики умного фильтра, Sphinx в Битрикс используется как замена MySQL-поиску по контенту, но не отменяет фасетный индекс. Переиндексация Sphinx выполняется отдельной командой (например, через search.sphinx или агент), а не через CSearch::ReIndexAll().

Можно ли запускать переиндексацию по Cron без остановки сайта?

Да, запускайте CSearch::ReIndexAll() порциями через Cron с ограничением по времени и элементам (например, по 100–500 элементов за запуск), чтобы не блокировать таблицы b_search_content и не уронить MySQL. Используйте отдельный агент или CLI-скрипт с параметром шага и логированием, чтобы отслеживать прогресс.

Что делать, если морфологический поиск не находит слово в другом падеже и в логах Error_on_empty_stem?

Error_on_empty_stem означает, что стеммер не смог обработать слово — обычно из-за неверной кодировки, спецсимволов или отсутствия русского стеммера. Проверьте настройку «Морфология» в модуле search (должен быть выбран русский язык), кодировку сайта (UTF-8) и что в стоп-лист не попало нужное слово.

#Bitrix #Bitrix модули #PHP #MySQL
автор · Backend / SRE Engineer
Артем Колячек

Backend-разработчик и SRE в студии Paradigma. Занимается тем, что у других проектов обычно обнаруживается за неделю до запуска: переездом Bitrix-проектов между серверами, починкой кешей после миграций, выстраиванием pipeline для контейнеров, мониторингом под нагрузкой.

До Paradigma — 7 лет в backend (PHP + Postgres) и в operations (Linux, Docker, Coolify, restic-бэкапы). Любит когда логи разговаривают полным синтаксисом ошибки, а не «что-то пошло не так».

На блоге пишет ровно про те ситуации с которыми сам разбирался руками: какой Bitrix-апдейт сломал кеш и как откатить, почему Docker-сеть не находит контейнер после рекрейта, что делать когда asyncpg ругается на event loop.