Модифицируем фасету для ускорения умного фильтра

Актуально для сайтов с большим каталогом товаров и множеством свойств.

Для ускорения работы умного фильтра в 1С Битрикс в редакциях Малый бизнес и Бизнес есть функционал фасетного индекса. Фасета создается для каждого инфоблока и представляет из себя таблицу в которой хранятся комбинации свойств фильтра. Для каталогов товаров, где эта таблица небольшая(до 1млн. записей) фильтр работает быстро. Проблема возникает при каталогах свыше 100тыс. товаров, и когда большое количество свойств участвует в умном фильтре.

Один из моих клиентов, интернет-магазин по продаже электротоваров, обратился ко мне с проблемой медленной работы сайта в каталоге с фильтром.


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

Было понятно что ускорить сайт можно либо увеличив мощности сервера(это мы также сделали), либо уменьшив таблицу фасеты.

Для уменьшения фасеты было решено две задачи:


Удалить лишние запросы

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

Делает он это так:
В файле component.php компонента bitrix:catalog.smart.filter запрашиваются все данные из фасеты

$res = $this->facet->query($arResult["FACET_FILTER"]); 

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

while ($rowData = $res->fetch())
{
...
}

Получается что лишние запросы создаются из-за того что $this->facet->query возвращает свойства, которых нет в текущем разделе.

Чтобы исключить лишние запросы, мы модифицировали фасету.

В class Facet(/bitrix/modules/iblock/lib/propertyindex/facet.php) мы добавили метод GetPropsArray, который вместо битриксовского \CIBlockSectionPropertyLink::getArray возвращал нам массив используемых на странице свойств. Свойство может участвовать в умном фильтре, но для текущего раздела не нужно. Поэтому мы решили в разделах инфоблока с большим количеством элементов указывать какие именно свойства нужны. Для этого завели свойство раздела инфоблока каталога и из него передаем значения в глобальную переменную $UF_F_PROPS.

public static function GetPropsArray($IBLOCK_ID, $SECTION_ID = 0)
    {
		global $UF_F_PROPS;
		
		global $DB;
		
		foreach($UF_F_PROPS as $key => $val){
			
		    $rs = $DB->Query("
				SELECT
					B.SECTION_PROPERTY,
					BP.ID PROPERTY_ID,
					BSP.SECTION_ID LINK_ID,
					BSP.SMART_FILTER,
					BSP.DISPLAY_TYPE,
					BSP.DISPLAY_EXPANDED,
					BSP.FILTER_HINT,
					BP.SORT,
					BP.NAME,
					BP.CODE,
					0 LEFT_MARGIN,
					B.NAME LINK_TITLE,
					BP.PROPERTY_TYPE,
					BP.USER_TYPE,
					BP.ACTIVE
				FROM
					b_iblock B
					INNER JOIN b_iblock_property BP ON BP.IBLOCK_ID = B.ID
					LEFT JOIN b_iblock_section_property BSP ON BSP.SECTION_ID = 0 AND BSP.PROPERTY_ID = BP.ID
				WHERE
					B.ID = ".$IBLOCK_ID."  AND BP.CODE = \"".$val."\"
				ORDER BY
					BP.SORT ASC, BP.ID ASC
			");
			if ($ar = $rs->Fetch())
			{
							$result[$ar["PROPERTY_ID"]] = array(
								"PROPERTY_ID" => $ar["PROPERTY_ID"],
								'SMART_FILTER' => 'Y',
								//"DISPLAY_TYPE" => 'F',
								"SORT" => $ar["SORT"],
								"ACTIVE" => $ar["ACTIVE"],								
								"ID" => $ar["PROPERTY_ID"],
								"PROPERTY_ID" => $ar["PROPERTY_ID"],
								"IBLOCK_ID" => $IBLOCK_ID,
								"CODE" => $ar["CODE"],
								"~NAME" => $ar["NAME"],
								"NAME" => htmlspecialcharsEx($ar["NAME"]),
								"PROPERTY_TYPE" => $ar["PROPERTY_TYPE"],
								"USER_TYPE" => $ar["USER_TYPE"],
								"USER_TYPE_SETTINGS" => $ar["USER_TYPE_SETTINGS"],
								"DISPLAY_TYPE" => $ar["DISPLAY_TYPE"],
								"DISPLAY_EXPANDED" => $ar["DISPLAY_EXPANDED"],
								"FILTER_HINT" => $ar["FILTER_HINT"],
								"VALUES" => array(),
							);
			}	
		}	
	
		
		return $result;
    }

В методе getSectionFilterProperty($sectionId) класса Facet проверяем установлены ли для раздела свойства.

if(is_array($UF_F_PROPS) && count($UF_F_PROPS)>0){
	$r = $this->GetPropsArray($this->iblockId, $sectionId);
}		
else{
	$r = \CIBlockSectionPropertyLink::getArray($this->iblockId, $sectionId);
}

В результате мы оставляем только данные нужных нам свойств. Это действие дало ускорение фильтрации примерно в 1,5 раза.

Уменьшить размер таблицы фасеты

Время запроса к большой таблице все-равно было большое(около 4 секунд), поэтому мы создали фасеты для отдельных разделов.

Создаем файл facet.php со следующим содержимым:

require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
define ('CATALOG_IBLOCK_ID', 13);
define ('DB_NAME', 'sql_ledlus_ru');
// 1. пределим исходные
// CATALOG_IBLOCK_ID - id ИБ
// SECTION_ID - id раздела, для которого создаем таблицу

function createSectionFacet($data, $data1, $db_new, $db_clon_table){

	$connection = \Bitrix\Main\Application::getConnection();

	$res = $connection->query('SHOW TABLES FROM '.DB_NAME.' LIKE \''.$db_new.'\'');	 
	if ($row = $res->Fetch())
	{	
		$resDel = $connection->query('DROP TABLE '.$db_new.'');
	}

	$resCreate = $connection->query('CREATE TABLE '.$db_new.' LIKE '.$db_clon_table.'');	
	
	$resInsert = $connection->query('INSERT INTO '.$db_new.' SELECT * FROM '.$db_clon_table.' WHERE SECTION_ID=\''.$data.'\' OR SECTION_ID=\''.$data1.'\'');	
}

//Получаем массив id разделов, у которых установлено св-во создания фасеты
$arFilter = Array('IBLOCK_ID'=>13, '>DEPTH_LEVEL'=>1);  

if(strlen($_REQUEST['fid'])>0){
	$arFilter[">ID"] = intval($_REQUEST['fid']);
}
$db_list = CIBlockSection::GetList(array('id'=>'asc'), $arFilter, TRUE, array("IBLOCK_ID", "ID", "UF_CREATE_FACET", 'ELEMENT_CNT',"NAME"));  

$i = 0;
if($arSection = $db_list->GetNext())
{
	if($arSection['ELEMENT_CNT'] > 2500)
	{
		$i++;

		$data = $arSection["ID"];
		$data1 = $arSection["ID"];
		$db_clon_table = "b_iblock_13_index";
		$db_new = "b_iblock_13_index_".$arSection["ID"];
		createSectionFacet($data, $data1, $db_new, $db_clon_table);	
	}
}

if(intval($arSection['ID'])>0)	{

setTimeout(() => {
    window.location.href = 'https://ДОМЕН.ru/facet.php?fid=';
}, "1000");
	
}
else{
	$db_clon_table = "b_iblock_13_index";
	$arFilter = Array('IBLOCK_ID'=>13, 'DEPTH_LEVEL'=>1);  
	$db_list = CIBlockSection::GetList(array('id'=>'asc'), $arFilter, TRUE, array("IBLOCK_ID", "ID", "UF_CREATE_FACET", 'ELEMENT_CNT',"NAME"));  
	while($arSection = $db_list->GetNext())
	{
		$connection = \Bitrix\Main\Application::getConnection();		
		$resInsert3 = $connection->query('DELETE FROM '.$db_clon_table.' WHERE SECTION_ID=\''.$arSection["ID"].'\' OR SECTION_ID=\''.$arSection["ID"].'\'');	
		if ($resInsert3)
		{	
			echo "
DELETE SECTION_ID = ".$data; } } }?>

При запуске файла из браузера будет создаваться по одной фасете для каждого раздела, содержащего более 2500 товаров.

Мы модифицировали метод facet->query , в нем переопределили переменную $tableName, возвращающую наименование таблицы.

$tableName = $this->storage->getTableName();
define ('DB_NAME', 'sql_ledlus_ru'); // наименование БД
global $UF_CREATE_FACET; // указывает, нужно ли подменять таблицу(переменная берет значение из свойства раздела) 
if($UF_CREATE_FACET == 1)
{
	$res = $connection->query('SHOW TABLES FROM '.DB_NAME.' LIKE \''.$this->storage->getTableName()."_".$this->sectionId.'\'');	 
	if ($row = $res->Fetch())
	{	
		$tableName = $this->storage->getTableName()."_".$this->sectionId;
	}
}


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

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


03.10.2023

Семен Голиков.