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

> CSearch::ReIndexAll из скрипта без шагов индексирует первую 1000 элементов инфоблока, а остальные удаляет из поиска. Разбираем по коду ядра, почему так и как запускать правильно.

20 октября 2026 · Заря · 6 минут

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

_Продолжение срабатывает только при max_execution_time &gt; 0_

```php
// 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 следующего нет. Работа над модулем закончена после первой порции.

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

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

_Финал ReIndexAll_

```php
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 вернёт число проиндексированных документов.

_Рабочий вариант для CLI_

```php
// без шагов модуль ИБ отдаёт первую 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С-Битрикс](https://it-zarya.ru/services/bitrix-cms/) [Обсудить задачу](https://it-zarya.ru/contacts/)

---

Источник: https://it-zarya.ru/blog/bitrix-search-reindex-1000/ · IT-агентство «Заря» · itzarya@mail.ru
Markdown-версия любой страницы сайта — тот же адрес с `.md` на конце. Индекс для ассистентов: https://it-zarya.ru/llms.txt
