Обсудить задачу

Переиндексация поиска из скрипта: почему в индексе осталась ровно тысяча товаров

Каталог перенесли с одного контура на другой, поиск нужно переиндексировать. Пишем короткий скрипт с CSearch::ReIndexAll, ограничиваем его модулем инфоблоков, запускаем из консоли. Скрипт отрабатывает без ошибок. А потом выясняется, что из 1152 активных товаров в поиске 1000, а 220 документов, включая посадочные страницы, пропали из индекса. Разбираем по исходникам ядра, откуда берётся эта тысяча и почему остальное не просто не проиндексировалось, а удалилось.

Какой вызов приводит к потере товаров?

Вызов без пошагового режима — с max_execution_time, равным нулю, — и с фильтром по модулю. Выглядит он совершенно безобидно:

PHPТак делать не надо
$NS = ["MODULE_ID" => "iblock", "SITE_ID" => "s1"];
$count = CSearch::ReIndexAll(false, 0, $NS);

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

Откуда берётся ровно тысяча?

Из обработчика модуля инфоблоков. CIBlock::OnSearchReindex на MySQL выбирает элементы с LIMIT 1000 и, выбрав порцию, возвращает строку «ID инфоблока.ID элемента», с которого нужно продолжить:

PHPМодуль инфоблоков отдаёт элементы порциями
// CIBlock::OnSearchReindex()
if($DB->type=="MYSQL")
{
    $limit = 1000;
    $strSql .= " LIMIT ".$limit;
}
// ...
if($limit !== false)
{
    $limit--;
    if($limit <= 0)
        return $IBLOCK_ID.".".$arIBlockElement["ID"];
}

Ограничение стоит ради памяти клиента MySQL, и само по себе оно разумно. Проблема в том, кто и как обрабатывает это возвращённое «продолжи отсюда».

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

Почему ReIndexAll не продолжает с нужного места?

Потому что продолжение предусмотрено только в пошаговом режиме. В CSearch::ReIndexAll ответ обработчика разбирается так:

PHPПродолжение срабатывает только при max_execution_time > 0
// CSearch::ReIndexAll()
$arResult = ExecuteModuleEventEx($arEvent, [$NS, $r, 'Index']);
if (is_array($arResult)) //old way
{
    // ...
}
else  //new method
{
    if ($max_execution_time > 0 && $arResult !== false && mb_strlen('.' . $arResult) > 1)
    {
        $DB->Commit();
        return [
            'MODULE' => $arEvent['TO_MODULE_ID'],
            'ID' => $arResult,
            // ...
        ];
    }
}
$NS['MODULE'] = '';

При max_execution_time = 0 условие ложно. Точка продолжения молча отбрасывается, цикл по обработчикам переходит к следующему модулю — а при фильтре MODULE_ID = iblock следующего нет. Работа над модулем закончена после первой порции.

Почему остальные товары не остались в индексе, а удалились?

Из-за чистки устаревших документов в конце неполной переиндексации. Если бы индекс просто недообновился, остальные товары лежали бы в нём со старым содержимым. Но после цикла стоит вот это:

PHPФинал ReIndexAll
if (!$bFull)
{
    CSearch::DeleteOld($NS['SESS_ID'], $NS['MODULE_ID'], $NS['SITE_ID']);
}

Каждый документ, проиндексированный в этом запуске, помечается идентификатором сессии. DeleteOld удаляет из индекса всё по модулю и сайту, что этой пометки не получило. Ядро считает, что прошлось по всем документам, и выбрасывает те, что больше не существуют. На самом деле прошлось оно по тысяче.

ШагЧто происходит
Обработчик инфоблоковотдаёт 1000 элементов и точку продолжения
ReIndexAll при max_execution_time = 0отбрасывает точку продолжения
DeleteOldудаляет из индекса всё, что не попало в эти 1000
Результатчисло вместо ошибки, в поиске нет части каталога

Как переиндексировать поиск из скрипта правильно?

Пошагово, так же как это делает админка: передавать положительный max_execution_time и повторять вызов, возвращая в него полученный $NS, пока результат — массив. В конце ReIndexAll вернёт число проиндексированных документов.

PHPРабочий вариант для CLI
// без шагов модуль ИБ отдаёт первую 1000, а DeleteOld стирает остальное
$NS = ["MODULE_ID" => "iblock", "SITE_ID" => "s1"];
$steps = 0;
do {
    $NS = CSearch::ReIndexAll(false, 20, $NS);
    $steps++;
} while (is_array($NS) && $steps < 1000);

echo is_array($NS) ? "не дошли до конца за $steps шагов\n" : "проиндексировано: $NS\n";
  • Второй аргумент больше нуля — включает пошаговый режим и передачу точки продолжения
  • $NS из ответа передаётся обратно целиком: в нём идентификатор сессии, по которому DeleteOld потом отличит свежие документы
  • Ограничение по числу шагов — страховка от бесконечного цикла, а не часть логики
  • Если вышли по ограничению, DeleteOld ещё не вызывался: индекс неполный, но ничего не удалено

После такого запуска индекс в том же разборе стал полным: 1152 товара из 1152 активных.

Как проверить, что индекс полный?

Сравнить число документов инфоблока в индексе с числом его активных элементов. Для элементов инфоблока ядро пишет ID инфоблока в поле PARAM2:

BashСверка индекса с каталогом
-- документы инфоблока в поисковом индексе
select count(*) from b_search_content
 where MODULE_ID = 'iblock' and PARAM2 = '<ID инфоблока>';

-- активные элементы того же инфоблока
select count(*) from b_iblock_element
 where IBLOCK_ID = <ID инфоблока> and ACTIVE = 'Y';

Расхождение на несколько документов бывает законным: в индекс попадают и разделы, а сторонние модули добавляют туда свои записи. Расхождение на сотни — повод разбираться.

Число, которое вернул ReIndexAll, полноту не доказывает. В том разборе функция отработала без ошибок и вернула ровно столько, сколько успела, — 1000.

Что ещё может стереть переиндексация «только инфоблоков»?

Записи сторонних модулей, которые пишут в индекс от имени модуля iblock. Так делает, например, модуль SEO-посадочных страниц sotbit.seometa: его страницы лежат в b_search_content с MODULE_ID = iblock и ITEM_ID вида seometa_…. Для DeleteOld это обычные документы модуля инфоблоков, которые не переиндексировались, — и они удаляются вместе с товарами.

Вернуть их собственной переиндексацией модуля в версии 2.5.5 не получилось: она падает с фатальной ошибкой — класс ReindexWriter не реализует абстрактный метод getWarnings. Причём падает уже после того, как удалила свои записи из индекса. Посадочные восстановили из резервной копии таблиц.

Что забрать себе

  • CSearch::ReIndexAll с max_execution_time = 0 не продолжает порционные обработчики: модуль инфоблоков на MySQL отдаёт 1000 элементов, и на этом всё
  • Неполная переиндексация заканчивается DeleteOld: всё, что не попало в этот запуск, удаляется из индекса, а не остаётся старым
  • Из скрипта запускайте пошагово: положительный max_execution_time и цикл с передачей $NS обратно, пока ответ — массив
  • Перед переиндексацией сохраните таблицы b_search_*: сторонние модули держат в индексе свои записи и не всегда умеют их восстановить
  • После переиндексации сверяйте число документов с каталогом: ошибки не будет, будет только меньшее число

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

Если поиск на сайте находит не всё

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

Посмотреть услугу: сайты на 1С-Битрикс Обсудить задачу