Переиндексация поиска из скрипта: почему в индексе осталась ровно тысяча товаров
Каталог перенесли с одного контура на другой, поиск нужно переиндексировать. Пишем короткий скрипт с CSearch::ReIndexAll, ограничиваем его модулем инфоблоков, запускаем из консоли. Скрипт отрабатывает без ошибок. А потом выясняется, что из 1152 активных товаров в поиске 1000, а 220 документов, включая посадочные страницы, пропали из индекса. Разбираем по исходникам ядра, откуда берётся эта тысяча и почему остальное не просто не проиндексировалось, а удалилось.
Какой вызов приводит к потере товаров?
Вызов без пошагового режима — с max_execution_time, равным нулю, — и с фильтром по модулю. Выглядит он совершенно безобидно:
$NS = ["MODULE_ID" => "iblock", "SITE_ID" => "s1"];
$count = CSearch::ReIndexAll(false, 0, $NS);Первый аргумент false означает «не полная переиндексация»: таблицы поиска не очищаются, документы обновляются поверх. Второй, 0, — «без ограничения по времени, сделать всё за один вызов». Ожидание понятное: вызов переиндексирует все инфоблоки сайта и вернёт число документов. На деле он обрабатывает одну порцию и стирает всё, что в неё не попало.
Откуда берётся ровно тысяча?
Из обработчика модуля инфоблоков. CIBlock::OnSearchReindex на MySQL выбирает элементы с LIMIT 1000 и, выбрав порцию, возвращает строку «ID инфоблока.ID элемента», с которого нужно продолжить:
// 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 ответ обработчика разбирается так:
// 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 следующего нет. Работа над модулем закончена после первой порции.
Почему остальные товары не остались в индексе, а удалились?
Из-за чистки устаревших документов в конце неполной переиндексации. Если бы индекс просто недообновился, остальные товары лежали бы в нём со старым содержимым. Но после цикла стоит вот это:
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 вернёт число проиндексированных документов.
// без шагов модуль ИБ отдаёт первую 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:
-- документы инфоблока в поисковом индексе
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_*: сторонние модули держат в индексе свои записи и не всегда умеют их восстановить
- После переиндексации сверяйте число документов с каталогом: ошибки не будет, будет только меньшее число
Общий вывод тот же, что и с большинством тихих сбоев Битрикса: если функция ядра ведёт себя иначе в админке и в скрипте, разница почти всегда в параметре, который включает другой режим работы. Быстрее прочитать этот режим в исходнике, чем искать пропавшие товары после выкладки.
Если поиск на сайте находит не всё
Неполный индекс не выдаёт себя ошибками: просто часть товаров не находится. Расскажите, что видите, — сверим индекс с каталогом и найдём, на каком шаге он теряет документы.