Фасетный индекс и умный фильтр

Фасетный индекс — отдельные таблицы, в которых модуль хранит значения свойств элементов в виде, удобном для фильтра. Компонент умного фильтра bitrix:catalog.smart.filter по индексу быстро считает, сколько элементов подходит под каждое значение свойства, и отбирает элементы. Без индекса компонент работает через обычные запросы к элементам, и на больших инфоблоках фильтр замедляется.

Индекс строят для каждого инфоблока отдельно. В него попадают свойства, у которых включен флажок «Показывать в умном фильтре», и только активные элементы. Модуль индексирует свойства любых типов — числа, строки, даты, списки, привязки, файлы. Для торгового каталога в индекс товаров попадают свойства торговых предложений и цены.

Перед работой подключите модуль iblock. Переменная $iblockId содержит идентификатор инфоблока. Примеры используют инфоблок новостей со свойством-списком SOURCE — источник новости.

Как работает индекс

Состояние индекса хранит поле PROPERTY_INDEX инфоблока.

Значение PROPERTY_INDEX Что означает Как работает умный фильтр
N или пустое Индекса нет Через запросы к элементам
I Индекс устарел и требует пересчета Через запросы к элементам, пока индекс не пересчитан
Y Индекс построен и актуален Через индекс

Модуль задает полю PROPERTY_INDEX значение I, например, в таких случаях:

  • свойство включают в умный фильтр,

  • раздел переносят в другой родительский раздел,

  • у свойства меняют тип или другую настройку, от которой зависит запись в индекс,

  • в административном разделе выбирают действие Отключить у индекса.

Когда индекс требует пересчета, модуль показывает администраторам в административном разделе уведомление со ссылкой на страницу построения индекса. Уведомление пропадает, когда индексов со значением I не остается. В Битрикс24 модуль уведомление не показывает.

Прочитать значение поля можно запросом \Bitrix\Iblock\IblockTable::getRow(['select' => ['PROPERTY_INDEX'], 'filter' => ['=ID' => $iblockId]]) или на странице Контент > Инфоблоки > Фасетные индексы.

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

Включить свойство в умный фильтр

Умный фильтр показывает свойства, у которых в настройках включен флажок «Показывать в умном фильтре». Флажок хранится в привязке свойства к разделам инфоблока — поле SMART_FILTER со значением Y. Привязки свойств к разделам работают, если у инфоблока включено поле SECTION_PROPERTY. Форма свойства в административном разделе включает это поле сама.

В коде включите флажок методами классического API.

  1. Включите у инфоблока привязку свойств к разделам методом CIBlock::Update() с ключом SECTION_PROPERTY. Метод включает привязку при любом переданном значении ключа.

  2. Передайте ключ SMART_FILTER со значением Y и ключ IBLOCK_ID в метод CIBlockProperty::Update() или CIBlockProperty::Add(). Без IBLOCK_ID метод Update() привязку не меняет.

Метод CIBlockProperty::Update() создает привязку свойства ко всему инфоблоку и помечает индекс как устаревший — поле PROPERTY_INDEX получает значение I. Если флажок уже был включен, метод индекс не помечает. Методы CIBlockProperty::Add() и Update() описаны в разделе Добавить свойства.

Пример. Найдите ID свойства Источник по символьному коду и включите свойство в умный фильтр.

use Bitrix\Iblock\PropertyTable;
use Bitrix\Main\Loader;

if (!Loader::includeModule('iblock'))
{
    throw new \RuntimeException('Не удалось подключить модуль iblock');
}

$iblockApi = new \CIBlock();

if (!$iblockApi->Update($iblockId, ['SECTION_PROPERTY' => 'Y']))
{
    throw new \RuntimeException($iblockApi->getLastError());
}

$property = PropertyTable::getRow([
    'select' => ['ID'],
    'filter' => ['=IBLOCK_ID' => $iblockId, '=CODE' => 'SOURCE'],
]);

if ($property === null)
{
    throw new \RuntimeException('Свойство SOURCE не найдено');
}

$propertyApi = new \CIBlockProperty();

if (!$propertyApi->Update((int)$property['ID'], ['IBLOCK_ID' => $iblockId, 'SMART_FILTER' => 'Y']))
{
    throw new \RuntimeException($propertyApi->getLastError());
}

После этого постройте индекс заново способом из раздела Построить индекс.

Привязку можно записать и D7-методом Bitrix\Iblock\SectionPropertyTable::add() с ключами IBLOCK_ID, SECTION_ID — 0 для всего инфоблока, PROPERTY_ID и SMART_FILTER. Метод не помечает индекс как устаревший, поэтому после него вызовите Bitrix\Iblock\PropertyIndex\Manager::markAsInvalid($iblockId). Иначе новое свойство не попадет в индекс, пока его не пересчитают.

Кроме флажка SMART_FILTER, привязка хранит настройки отображения свойства в фильтре. Их передают в те же методы вместе с SMART_FILTER.

  • DISPLAY_TYPE — вид поля, например флажки или выпадающий список. Допустимые коды для типа свойства возвращает метод CIBlockSectionPropertyLink::getDisplayTypes(),

  • DISPLAY_EXPANDED — Y, если поле в фильтре развернуто,

  • FILTER_HINT — подсказка к полю.

Чтобы убрать свойство из умного фильтра, передайте SMART_FILTER со значением N. Пересчитывать индекс не нужно. Компонент перестанет показывать свойство после сброса своего кеша.

Показать свойство только в разделе

Если свойство нужно показывать в умном фильтре только в одном разделе и его подразделах, привяжите свойство к разделу, а не ко всему инфоблоку. Передайте в метод CIBlockSection::Add() или Update() ключ SECTION_PROPERTY — массив, где ключ — ID свойства, а значение — настройки привязки с ключами SMART_FILTER, DISPLAY_TYPE, DISPLAY_EXPANDED и FILTER_HINT.

  • Метод Update() заменяет все привязки раздела. Привязки свойств, которых нет в массиве, метод удаляет, поэтому передавайте полный набор свойств раздела. Текущие настройки свойств раздела возвращает метод CIBlockSectionPropertyLink::GetArray($iblockId, $sectionId) — массив по ID свойств с теми же ключами и признаком наследования INHERITED.

  • Метод Update() помечает индекс как устаревший, если включает SMART_FILTER. Метод Add() индекс не помечает, после него постройте индекс заново.

Пример. Покажите свойство Источник в фильтре только в разделе Мероприятия и его подразделах.

$sectionApi = new \CIBlockSection();

$isUpdated = $sectionApi->Update($sectionId, [
    'SECTION_PROPERTY' => [
        $sourcePropertyId => ['SMART_FILTER' => 'Y'],
    ],
]);

if (!$isUpdated)
{
    throw new \RuntimeException($sectionApi->getLastError());
}

Переменная $sectionId содержит ID раздела, $sourcePropertyId — ID свойства SOURCE. Свойство не должно быть привязано ко всему инфоблоку, иначе фильтр покажет его во всех разделах. Привязку ко всему инфоблоку удаляет метод CIBlockSectionPropertyLink::Delete(0, $sourcePropertyId). После записи постройте индекс заново.

Построить индекс

Индекс строят в административном разделе на странице Контент > Инфоблоки > Фасетные индексы. Инфоблок появляется в списке, когда хотя бы одно его свойство включено в умный фильтр. Если индекс требует пересчета, в строке инфоблока есть действие Создать, а над списком — кнопка Создать все. Страница строит индекс по шагам, чтобы не превысить время выполнения скрипта.

В коде индекс строит объект Bitrix\Iblock\PropertyIndex\Indexer, который возвращает метод Manager::createIndexer(). Индекс строят три метода объекта, их вызывают по порядку.

  1. startIndex() — удаляет прежний индекс и создает пустые таблицы.

  2. continueIndex($seconds) — индексирует элементы по возрастанию ID примерно указанное число секунд и возвращает количество обработанных элементов. Значение 0 снимает ограничение времени.

  3. endIndex() — помечает индекс как актуальный, поле PROPERTY_INDEX получает значение Y. Метод также сбрасывает кеш настроек инфоблока.

Метод estimateElementCount() возвращает количество активных элементов, которые попадут в индекс. Его используют, чтобы показать прогресс.

Метод endIndex() не сбрасывает кеш компонентов и не снимает уведомление администраторам. После него сделайте то же, что делает административная страница, — сбросьте кеш компонента умного фильтра и тегированный кеш инфоблока и вызовите Manager::checkAdminNotification().

Не запускайте построение индекса одного инфоблока параллельно. Метод startIndex() удаляет таблицы, которые заполняет другой процесс.

Пример. Постройте индекс инфоблока в консольном скрипте.

use Bitrix\Iblock\PropertyIndex\Manager;
use Bitrix\Main\Loader;

if (!Loader::includeModule('iblock'))
{
    throw new \RuntimeException('Не удалось подключить модуль iblock');
}

$indexer = Manager::createIndexer($iblockId);
$indexer->startIndex();

$total = $indexer->estimateElementCount();
$indexed = 0;

do
{
    // Индексировать порциями примерно по 20 секунд
    $count = $indexer->continueIndex(20);
    $indexed += $count;
}
while ($count > 0);

$indexer->endIndex();

// Сбросить кеш умного фильтра и тегированный кеш инфоблока, снять уведомление
\CBitrixComponent::clearComponentCache('bitrix:catalog.smart.filter');
\CIBlock::clearIblockTagCache($iblockId);
Manager::checkAdminNotification();

echo 'Проиндексировано ' . $indexed . ' из ' . $total . PHP_EOL;

В консольном скрипте перед примером задайте $_SERVER['DOCUMENT_ROOT'] — путь к корню сайта — и подключите ядро файлом /bitrix/modules/main/include/prolog_before.php.

Если вы строите индекс в веб-запросе или агенте, выполняйте один шаг за запуск. Сохраните ID последнего обработанного элемента из метода getLastElementId(), например в настройке модуля методом Bitrix\Main\Config\Option::set(). Перед следующим вызовом continueIndex() передайте сохраненный ID в setLastElementId(). Метод startIndex() вызывайте только на первом шаге, а endIndex() — когда continueIndex() вернет 0. Агенты описаны в статье Агенты и фоновые задачи.

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

Таблица показывает, какие способы записи элемента обновляют индекс. Поведение одинаково для обеих версий хранения свойств. ORM-запись элемента описана в разделе Если элемент записывает ORM.

Способ записи Индекс
CIBlockElement::Add(), Update(), Delete() Обновляется. Неактивный элемент метод Update() удаляет из индекса, при повторной активации возвращает
CIBlockElement::SetPropertyValuesEx() и SetPropertyValues() Не обновляется
ORM-метод save() объекта элемента, когда изменены поля элемента Обновляется
ORM-метод save() объекта элемента, когда изменены только значения свойств Не обновляется
Цены и товарные параметры через модели модуля catalog, например Bitrix\Catalog\Model\Price, и через классический CPrice::Update() Не обновляется. Индекс обновляют административная форма товара и групповое изменение цен

Если индекс не обновился, вызовите метод Manager::updateElementIndex($iblockId, $elementId) для каждого измененного элемента. Метод ничего не делает, если индекса у инфоблока нет.

Пример. Обновите индекс после записи свойства методом SetPropertyValuesEx().

use Bitrix\Iblock\PropertyIndex\Manager;

\CIBlockElement::SetPropertyValuesEx($elementId, $iblockId, [
    'SOURCE' => $sourceEnumId,
]);

Manager::updateElementIndex($iblockId, $elementId);

Переменная $sourceEnumId содержит ID варианта списка. Способ найти его описан в разделе Найти вариант списка.

При записи многих элементов методами CIBlockElement::Add() и Update() индекс можно обновить один раз после цикла. Способ описан в разделе Отложить обновление кеша и индексов. Отложенное обновление не помогает при записи только свойств методом SetPropertyValuesEx() — этот метод не ставит элемент в очередь индекса. В таком случае вызовите updateElementIndex() для каждого элемента или постройте индекс заново после импорта.

После обновления индекса компонент может показывать прежние результаты из своего кеша. Сбросьте кеш умного фильтра методом CBitrixComponent::clearComponentCache('bitrix:catalog.smart.filter') и тегированный кеш инфоблока методом CIBlock::clearIblockTagCache($iblockId), подробнее — в разделе Обновить кеш и индексы.

Отключить индекс

Метод Manager::deleteIndex() удаляет таблицы индекса и задает полю PROPERTY_INDEX значение N. Умный фильтр продолжит работать через запросы к элементам.

Пример. Отключите индекс инфоблока.

\Bitrix\Iblock\PropertyIndex\Manager::deleteIndex($iblockId);

Действие Отключить в строке инфоблока на странице Контент > Инфоблоки > Фасетные индексы таблицы не удаляет. Оно задает полю PROPERTY_INDEX значение I, и фильтр переходит на запросы к элементам до нового построения. Так в административном разделе пересоздают индекс — Отключить, затем Создать.

Метод CIBlock::Update() с ключом PROPERTY_INDEX и значением N не удаляет таблицы индекса, но выключает у инфоблока привязку свойств к разделам SECTION_PROPERTY. Отключайте индекс методом Manager::deleteIndex().

Индекс торгового каталога

Для товаров с торговыми предложениями индекс строят для инфоблока товаров. Метод Manager::createIndexer() с ID инфоблока предложений возвращает индексатор инфоблока товаров. В индекс товара попадают свойства торговых предложений с флажком «Показывать в умном фильтре» и цены.

Умный фильтр использует индекс, только если он актуален у обоих инфоблоков — товаров и предложений. Метод endIndex() помечает актуальными оба инфоблока сразу. Метод updateElementIndex() с ID инфоблока предложений и ID предложения обновляет запись товара, к которому относится предложение.

Цены и товарные параметры, записанные моделями модуля catalog, индекс не обновляют. После методов Model\Price::add(), update() и delete() в индексе остается прежнее значение цены, подробнее — в разделе Поддерживать индекс актуальным. После таких изменений вызовите updateElementIndex() с ID инфоблока товаров и ID товара.

Устройство торгового каталога описано в статье Товары и торговые предложения.

Если фильтр работает не так

Что происходит Возможная причина Что сделать
Фильтр работает медленно Индекса нет, поле PROPERTY_INDEX равно I или у торгового каталога индекс актуален только у одного из инфоблоков — товаров или предложений Постройте индекс способом из раздела Построить индекс
Фильтр не показывает свойство У свойства не включен флажок «Показывать в умном фильтре», у инфоблока выключено поле SECTION_PROPERTY или свойство привязано к другому разделу Включите свойство способом из раздела Включить свойство в умный фильтр или Показать свойство только в разделе
Фильтр не находит элемент по новому значению Значение записано методом SetPropertyValuesEx(), ORM-записью только свойств или моделью модуля catalog Вызовите Manager::updateElementIndex(), подробнее — в разделе Поддерживать индекс актуальным
Фильтр показывает прежние значения и количества Результат хранится в кеше компонента Сбросьте кеш методами CBitrixComponent::clearComponentCache('bitrix:catalog.smart.filter') и CIBlock::clearIblockTagCache($iblockId)
Фильтр показывает неактивный элемент Элемент снят с публикации в обход CIBlockElement::Update() Вызовите Manager::updateElementIndex() для элемента

Проверить результат

Перед публикацией кода выполните на тестовом инфоблоке:

  1. Включите свойство в умный фильтр и проверьте, что поле PROPERTY_INDEX получило значение I. Способ прочитать поле описан в разделе Как работает индекс.

  2. Постройте индекс и проверьте, что поле PROPERTY_INDEX получило значение Y, а количество проиндексированных элементов совпадает с estimateElementCount().

  3. Измените значение свойства тем способом, который использует проект, и проверьте в умном фильтре, что элемент находится по новому значению. Если не находится, добавьте вызов Manager::updateElementIndex().

  4. Снимите элемент с публикации и проверьте, что умный фильтр его не показывает.