Перейти к содержимому

Умный фильтр изнутри - от адреса до выборки товаров

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

Механика

Умный фильтр товары не выбирает. Компонент строит форму, собирает отмеченные значения и кладёт готовый массив условий в глобальную переменную с именем из параметра FILTER_NAME. Выбирает товары другой компонент, читающий эту же переменную.

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

Адрес фильтрации разбирает не фильтр, а комплексный компонент каталога вокруг него. Метод CComponentEngine::ParseComponentPath() подбирает шаблон маршрута под текущий путь и заполняет массив переменных. Возвращённый код страницы говорит, что открыто: список разделов, раздел или карточка товара.

После разбора адреса переменные восстанавливаются из запроса методом CComponentEngine::InitComponentVariables() с учётом псевдонимов вложенных компонентов. Только после этого раздел, инфоблок и отмеченные значения свойств становятся понятны фильтру и списку одновременно.

Отмеченные значения приходят полями формы arrFilter_<ID свойства>_<хеш значения>. Префикс здесь совпадает с именем переменной фильтра, а хвост однозначно указывает на свойство и его значение. Компонент разбирает эти поля обратно и складывает из них массив условий отбора.

Фасетный индекс - отдельная таблица готовых связок «значение свойства - раздел - элемент». Выборка по ней читает одну таблицу вместо соединения таблиц свойств, поэтому число условий почти не влияет на время ответа.

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

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

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

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

Шаги

  1. Убедиться, что у нужных свойств инфоблока включён признак участия в умном фильтре.
  2. Сверить имя переменной фильтра у формы и у списка товаров на той же странице.
  3. Посмотреть, какой шаблон маршрута сработал на текущем адресе и какие переменные он заполнил.
  4. Проверить состояние фасетного индекса, а для каталога с предложениями - индекса обоих инфоблоков.
  5. Повторить выборку списка товаров из кода и сравнить её с тем, что показывает витрина.

Код

Смотрим, какие свойства попали в фильтр:

\Bitrix\Main\Loader::includeModule('iblock');
$res = CIBlockProperty::GetList(['SORT' => 'ASC'],
['IBLOCK_ID' => 5, 'SMART_FILTER' => 'Y', 'ACTIVE' => 'Y']);
while ($prop = $res->Fetch()) {
printf("%-14s %-24s тип=%s показ=%s\n",
$prop['CODE'], $prop['NAME'], $prop['PROPERTY_TYPE'], $prop['DISPLAY_TYPE']);
}
// пустой список означает, что формы фильтра не будет вовсе

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

Разбираем адрес фильтрации на переменные:

$variables = [];
$page = CComponentEngine::ParseComponentPath(
'/catalog/', // папка комплексного компонента каталога
$arParams['SEF_URL_TEMPLATES'], // шаблоны маршрутов из его настроек
$variables);
print_r([$page, $variables]); // list, section или element

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

Восстанавливаем переменные из запроса:

CComponentEngine::InitComponentVariables($page, ['IBLOCK_ID', 'SECTION_ID'],
$aliases, $variables); // без адресов фильтрации первым аргументом идёт false
// псевдонимы приводят имена переменных вложенных компонентов к общему набору

Смотрим поля формы фильтра в запросе:

$request = \Bitrix\Main\Context::getCurrent()->getRequest();
parse_str((string)parse_url($request->getRequestUri(), PHP_URL_QUERY), $query);
foreach ($query as $key => $value) {
if (strpos($key, 'arrFilter_') === 0) { // поле умного фильтра
printf("%s = %s\n", $key, is_array($value) ? implode(',', $value) : $value);
}
}
// set_filter отправляет форму, del_filter сбрасывает весь отбор целиком

Имена полей собирает шаблон фильтра, и переданный руками параметр с другим именем компонент пропустит. Отсюда же видно, дошла ли отправка формы до сервера вообще.

Смотрим собранные фильтром условия:

// после подключения bitrix:catalog.smart.filter с FILTER_NAME => 'arrFilter'
print_r($GLOBALS['arrFilter']);
// ключи массива - это готовые условия выборки по свойствам и ценам
// пустой массив при отмеченных значениях означает, что форма не дошла до сервера

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

Добавляем своё условие, не потеряв условия фильтра:

$GLOBALS['arMyFilter'] = ['!PROPERTY_HIDDEN_VALUE' => 'Y'];
if (!empty($GLOBALS['arrFilter']) && is_array($GLOBALS['arrFilter'])) {
$GLOBALS['arMyFilter'] = array_merge_recursive(
$GLOBALS['arrFilter'], $GLOBALS['arMyFilter']); // порядок важен
}
// списку товаров передаём FILTER_NAME => 'arMyFilter'

Своя переменная фильтра поверх чужой затирает отбор посетителя целиком. Слияние массивов сохраняет оба набора условий, а витрина продолжает отвечать на форму.

Повторяем выборку списка товаров вручную:

$res = CIBlockElement::GetList(['SORT' => 'ASC'],
array_merge(['IBLOCK_ID' => 5, 'ACTIVE' => 'Y', 'SECTION_ID' => $sectionId,
'INCLUDE_SUBSECTIONS' => 'Y'], $GLOBALS['arrFilter']),
false, false, ['ID', 'NAME']);
while ($row = $res->Fetch()) { echo $row['ID'], ' ', $row['NAME'], PHP_EOL; }
// раздел и подразделы список подставляет сам, фильтр про них ничего не знает
// расхождение с витриной означает разные условия, а не сбой самого фильтра

Обновляем индекс после правки свойств:

use Bitrix\Iblock\PropertyIndex;
PropertyIndex\Manager::updateElementIndex($iblockId, $elementId); // один элемент
PropertyIndex\Manager::deleteElementIndex($iblockId, $elementId); // снять из индекса
PropertyIndex\Manager::markAsInvalid($iblockId); // весь индекс инфоблока невалиден
// вызывать после смены свойств, перемещения разделов и внешней выгрузки

Правка через ORM механизмы старого ядра не запускает. Индекс, поисковый индекс и ресайз картинок при таком способе записи остаются на разработчике.

Индексируем инфоблок торговых предложений:

$offerIblockId = 6; // инфоблок предложений того же каталога
PropertyIndex\Manager::updateElementIndex($offerIblockId, $offerId);
PropertyIndex\Manager::markAsInvalid($offerIblockId); // после смены состава свойств
// свойства предложений живут отдельно от свойств товара и индексируются отдельно

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

Ограничения

Фильтр не отвечает за выборку и не знает о списке товаров ничего, кроме имени переменной. Расхождение между формой и выдачей ищут в этом имени, а не в шаблоне формы и не в параметрах отображения.

Адреса фильтрации живут внутри комплексного компонента каталога. Вынесенный за его пределы фильтр форму построит, но собранный им путь разбирать будет некому.

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

Сборка индекса на большом каталоге идёт минутами и не атомарна. Пока она не закончилась, выдача неполна, поэтому полное построение планируют на ночное окно.

Свойства товара и свойства предложений остаются разными свойствами разных инфоблоков. Одноимённые «цвет» и «размер» дают в форме два отдельных блока, и штатных настроек для их слияния нет.

Типичные проблемы

Товары исчезают из каталога, пока строится индекс.

Фасетный индекс пересобирается не мгновенно, и выдача читает наполовину заполненную таблицу. Полная картина возвращается после завершения сборки, поэтому построение выносят в ночное окно.

Фильтр собирает адрес вида /filter/clear/apply/.

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

Свой фильтр на странице стирает выбор посетителя.

Переменная фильтра переприсваивается своим массивом вместо слияния с уже собранным. Условия умного фильтра лежат в той же переменной и теряются при перезаписи.

Индекс сам переходит в состояние «отключён» после обмена.

Внешний скрипт обмена пишет цены и свойства мимо штатного сохранения элемента каталога. Такая запись помечает индекс невалидным, и он остаётся выключенным до ручного построения.

В форме дублируются цвет и размер по два раза.

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

У части товаров нет фильтра по цене, у остальных есть.

Выгрузка завела элемент инфоблока, но не создала запись товара в каталоге. Без неё цена не индексируется, и ручное сохранение карточки в админке чинит один товар.

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

Как получить из умного фильтра ID товаров после фильтрации?

Список идентификаторов фильтр не отдаёт: он отдаёт условия в переменной FILTER_NAME. Эту переменную передают другому компоненту или повторяют выборку из кода тем же массивом условий.

Работает ли умный фильтр в комплексном компоненте новостей?

Форму он построит, а адреса фильтрации разбирать будет некому: маршруты собирает каталог. Вне каталога условия придётся передавать параметрами запроса и связывать компоненты вручную.

Можно ли вывести каталог с заранее заданными условиями?

Да, своим массивом условий в отдельной переменной фильтра. Чтобы выбор посетителя не пропал, свой массив сливают с массивом умного фильтра, а не подставляют вместо него.

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

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

Почему часть значений в форме становится недоступной?

Компонент гасит значения, за которыми при текущем отборе не остаётся товаров. Считает он это по индексу, поэтому устаревший индекс гасит и вполне живые значения свойств.

Смежное

Первоисточник