Поступила тут интересная задача: у клиента на сайте выводятся статьи. И вот он захотел, чтобы на странице с полным текстом статьи выводился блок с другими статьями, которые будут похожи на данную. (Под похожими имеются ввиду статьи, в названии или поисковых тегах которых совпадают хотя бы одно слово).
Поступила тут интересная задача: у клиента на сайте выводятся статьи. И вот он захотел, чтобы на странице с полным текстом статьи выводился блок с другими статьями, которые будут похожи на данную. (Под похожими имеются ввиду статьи, в названии или поисковых тегах которых совпадают хотя бы одно слово).
Решение:
Для вывода статей воспользуемся обычным news.list, а похожесть будем передавать фильтром в данный компонент.
Текст кода, с комментариями по ходу:
// 1. Получаем данные по выбранному элементу: его название и поисковыве теги $CurentElement=intval($_GET["ID"]); // берем ID текущей статьи из адресной строки // 2. Получаем данные по выбранному элементу: $res = CIBlockElement::GetByID($CurentElement); if($ar_res = $res->GetNext()) $arCurentElement=$ar_res; // 3. Из названия и поисковых тегов формируем строку, по словам которой будем искать все похожие записи: $tmpName=str_replace( array(".", ",","?","!","-"), "", trim($arCurentElement["NAME"]." ".$arCurentElement["TAGS"]) ); /*знаю, что кусок кода выше можно было сделать проще через регулярные выражения. Но, к сожалению, я с ними не дружу:( */ if(strlen($tmpName)>0){ $arLooksLike = array( "INCLUDE_SUBSECTIONS" => "Y", "!ID"=>intval($CurentElement) /*исключаем данный элемент из выборки*/ ); $NameItems=explode(" ",$tmpName); /* кто умеет пользоваться регулярными выражениями - можете предварительно не очищать от знаков пунктуации, а сразу тут выбирать уже массив готовых результатов */ $itemsArray=array(); foreach($NameItems as $item){ if(strlen($item)>1){ $itemsArray[]=array("NAME" => "%".$item."%"); // ищем элементы, у которых выбранное свойство есть в названии $itemsArray[]=array("TAGS" => "%".$item."%"); // ищем элементы, у которых выбранное свойство есть в поисковых тегах } } $tmpArray=array("LOGIC" => "OR"); // подключаем логику "ИЛИ" следующие 2 операции, думаю, можно ужать, но не стал заморачиваться: и так работает. $tmpArray=array_merge($tmpArray,$itemsArray); $addFArray=array( array($tmpArray), ); $GLOBALS["arLooksLike"]=array_merge($arLooksLike,$addFArray); }
В результате в глобальном массиве
$GLOBALS["arLooksLike"]
лежит подключаемый фильтр. Этот массив и подключаем к компоненту "news.list":
"FILTER_NAME" => "arLooksLike",
Всем спасибо за внимание!
Update 2019-12-04. Получение релевантных статей, похожий к выбранной
Понадобилось одному человеку сделать вывод похожих статей, но с максмимальной схожестью между собой.
В общем виде постановка задачи такая: нужно выдавать заданное количество элементов в заданном блоке.
Для этих целей немного переписал алгоритм:
if(!function_exists('get_words_from_string')){ function get_words_from_string($string){ $string = str_replace( array(",", "?", "!", "-","'","\"",""","«","»"), "", trim($string)); $words = explode(' ', str_replace( array(",", "?", "!", "-","'","\"",""","«","»"), "", trim($string)) ); return $words; } }
if(!function_exists('filter_words_by_section')){ function filter_words_by_section($words, $sid){ if(in_array(intval($sid),[91,103])){ $words = array_filter($words, function ($item) { return (strlen($item) > 1); }); } else { $words = array_filter($words, function ($item) { return (strlen($item) > 2); }); } return $words; } }
if(!function_exists('get_words_combinations')){ function get_words_combinations($words){ $words = array_unique($words); $arWordsCombinations = \Pai\Tools\CDev::uniqueCombination($words, 1, 3); if(count($arWordsCombinations)<50){ $arWordsCombinations = \Pai\Tools\CDev::uniqueCombination($words, 1, 5); } if(count($arWordsCombinations)<50){ $arWordsCombinations = \Pai\Tools\CDev::uniqueCombination($words, 1, 7); } if(count($arWordsCombinations)<50){ $arWordsCombinations = \Pai\Tools\CDev::uniqueCombination($words, 1, 10); } return $arWordsCombinations; } }
if(!function_exists('buildFilter')){ function buildFilter($arWordsCombinations,$arResult,$arParams,$arSame=[]){ $arFilter = array( 'IBLOCK_ID' => $arParams['IBLOCK_ID'], '!ID' => $arResult['ID'], 'IBLOCK_SECTION_ID' => intval($arResult['IBLOCK_SECTION_ID']), "INCLUDE_SUBSECTIONS" => "Y", "ACTIVE" => "Y" ); $itemsArray = array("LOGIC" => "OR"); $itemsArray[] = array("NAME"=>"%".trim($arResult['NAME'])."&"); foreach ($arWordsCombinations as $item) { if (count($item) > 0) { $itemsArray[] = array("NAME" => "%" . implode('&', $item) . "%"); } } if(!empty($arSame)){ $excludeID = array_merge([$arResult['ID']], array_map(function ($arItem) { return intval($arItem['ID']); },$arSame)); $arFilter['!ID'] = $excludeID; } $arFilter[] = $itemsArray; return $arFilter; } }
if(!function_exists('getElementsByFilter')){ function getElementsByFilter($arFilter, $arResult, $words, $arParams, $max_items = 200){ if(intval($max_items)<=0 || intval($max_items)>200){ $max_items = 200; } $arSame = \Pai\Tools\CDev::GetIblockElementItems([ 'filter' => $arFilter, 'select' => ['ID', 'IBLOCK_ID', 'NAME', 'IBLOCK_SECTION_ID', 'DETAIL_PAGE_URL', 'PREVIEW_PICTURE'], // тут можно добавить нужные для выборки поля, либо убрать этот массив - тогда выберется полный набор данных по каждому элементу 'page_params' => ['nTopCount'=>$max_items] ], $arParams['CACHE_TIME']); if(!empty($arSame)){ foreach ($arSame as $key => $arElement) { $arSame[$key]['SIMILARITY'] = \Pai\Tools\CDev::caclSimilarity($arElement['NAME'], $words,$arResult['NAME']); } } else { $arSame = []; } return $arSame; } }
$maxSameItems = 12; $words = filter_words_by_section(get_words_from_string($arResult['NAME']),$arResult['IBLOCK_SECTION_ID']); $words = array_map(function ($item){ return TrimExAll($item,'.'); },$words); $arWordsCombinations = get_words_combinations($words); $arFilter = buildFilter($arWordsCombinations,$arResult,$arParams); $arSame = getElementsByFilter($arFilter, $arResult, $words, $arParams); if(count($arSame)<$maxSameItems && strlen($arResult['TAGS'])>0){ $words = filter_words_by_section( get_words_from_string( $arResult['NAME'] . " " . str_replace(',','',$arResult["TAGS"]) ), $arResult['IBLOCK_SECTION_ID'] ); $arWordsCombinations = get_words_combinations($words); $arFilter = buildFilter($arWordsCombinations,$arResult,$arParams,$arSame); $arSameTags = getElementsByFilter($arFilter, $arResult, $words, $arParams); $arSame = array_merge($arSame,$arSameTags); } if(count($arSame) < $maxSameItems){ $arFilter = array( 'IBLOCK_ID' => $arParams['IBLOCK_ID'], 'IBLOCK_SECTION_ID' => intval($arResult['IBLOCK_SECTION_ID']), "INCLUDE_SUBSECTIONS" => "Y", "ACTIVE" => "Y" ); $excludeID = array_merge([$arResult['ID']], array_map(function ($arItem) { return intval($arItem['ID']); },$arSame)); $arFilter['!ID'] = $excludeID; $arSameTags = \Pai\Tools\CDev::GetIblockElementItems([ 'filter' => $arFilter, 'select' => ['ID', 'IBLOCK_ID', 'NAME', 'IBLOCK_SECTION_ID', 'DETAIL_PAGE_URL', 'PREVIEW_PICTURE'], // тут можно добавить нужные для выборки поля, либо убрать этот массив - тогда выберется полный набор данных по каждому элементу 'page_params' => ['nTopCount'=>$maxSameItems] ], $arParams['CACHE_TIME']); foreach ($arSameTags as $key => $arElement) { $arSameTags[$key]['SIMILARITY'] = \Pai\Tools\CDev::caclSimilarity($arElement['NAME'], $words,$arResult['NAME']); } $arSame = array_merge($arSame,$arSameTags); } usort($arSame,function ($a,$b){ return (floatval($a['SIMILARITY'])>floatval($b['SIMILARITY']) ? -1 : 1); }); $arResult['SIMILAR'] = array_slice($arSame,0,$maxSameItems); // обрезаем результирующий массив foreach ($arResult['SIMILAR'] as $key=>$arItem){ if(intval($arItem['PREVIEW_PICTURE'])>0){ $arResult['SIMILAR'][$key]['PREVIEW_PICTURE'] = \Pai\Tools\CDev::GetFileInfo($arItem['PREVIEW_PICTURE']); } }
Берем сначала название текущего элемента. Разбиваем его на отдельные слова, составляем комбинации из слов названия и передаем это все в поиск. Если ничего не нашли по словам из названия - добавляем поисковые теги к названию - получаем более расширенный набор слов, по которым искать. Также и место поиска подключаем поле с поисковыми тегами. Если и в этом случае ничего не нашли - тогда набиваем просто элементами из того же раздела. В итоге получаем заданное количество элементов для вывода в блоке с похожими элементами.
В алгоритме использованы функции:
class CDev { public static function GetIblockElementItems( $arParams = array('filter' => array(), 'select' => false, 'sort' => array('name' => 'asc'), 'page_params' => false, 'group' => false), $life_time = 3600) { if (!isset($arParams['filter']) || empty($arParams['filter'])) return false; $arFilter = $arParams['filter']; $arSelect = (isset($arParams['select'])) ? $arParams['select'] : false; $arSort = (isset($arParams['sort'])) ? $arParams['sort'] : array('name' => 'asc'); $pageParams = (isset($arParams['page_params'])) ? $arParams['page_params'] : false; $groupParams = (isset($arParams['group'])) ? $arParams['group'] : false; $result = false; $cache_params = array(); foreach (array_keys($arParams) as $array_key) { if (is_array($arParams[$array_key])) { foreach ($arParams[$array_key] as $key => $value) { $cache_params[$array_key . '-' . $key] = $value; } } elseif (is_bool($arParams[$array_key])) { $cache_params[$array_key] = $arParams[$array_key] ? 1 : 0; } } $cache_id = md5(serialize($cache_params)); $cache_dir = __CLASS__ . '/' . __FUNCTION__; $cache = \Bitrix\Main\Data\Cache::createInstance(); if ($life_time < 0) { $cache->clean($cache_id, $cache_dir); } if ($cache->initCache($life_time, $cache_id, $cache_dir)) { $result = $cache->getVars(); } elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock')) { $rsItems = \CIBlockElement::GetList($arSort, $arFilter, $groupParams, $pageParams, $arSelect); if (is_array($arSelect) && !empty($arSelect)) { while ($arElement = $rsItems->GetNext()) { $result[] = $arElement; } } else { while ($arElement = $rsItems->GetNextElement()) { $arFields = $arElement->GetFields(); $arFields['PROPERTIES'] = $arElement->GetProperties(); $result[] = $arFields; } } if (!empty($result)) { foreach ($result as $key => $arItem) { if (!empty($arItem['PROPERTIES'])) { foreach ($arItem['PROPERTIES'] as $pCode => $arProperty) { $result[$key]['PROPERTIES'][$pCode] = \CIBlockFormatProperties::GetDisplayValue( array('ID' => $arItem['ID'], 'NAME' => $arItem['NAME']), $arProperty, ''); } } } } $cache->endDataCache($result); } return $result; } public static function uniqueCombination($in, $minLength = 1, $max = 2000) { $in = array_map(function ($item){return strtolower($item);},$in); $in = array_unique($in); sort($in); $count = count($in); $members = pow(2, $count); $return = array(); for($i = 0; $i <= $members; $i ++) { $b = sprintf("%0" . $count . "b", $i); $out = array(); for($j = 0; $j <= $count; $j ++) { if(strlen($in[$j])>0){ $b{$j} == '1' and $out[] = $in[$j]; } } count($out) >= $minLength && count($out) <= $max and $return[] = $out; } foreach ($return as $key=>$item){ $item = array_filter($item,function ($t){ return (strlen($t)>0); }); $return[$key] = $item; } $return = array_filter($return,function ($t){ return !empty($t); }); $return = array_map('unserialize', array_unique(array_map('serialize', $return))); return $return; } public static function caclSimilarity($str,$words,$name = false){ if(strlen($str)<=0) return 0; if(empty($words)) return 99; if(strpos($str,$name)!==false) return 100; $koefficients = []; foreach ($words as $word){ if(strpos($name,$word)!==false){ $koefficients[$word] = 1 - strpos($name,$word) / strlen($name); } else { $koefficients[$word] = 1 / strlen($name); } } $cnt = 0; foreach ($words as $word){ if(strpos(mb_strtolower($str),mb_strtolower($word))!==false){ $cnt +=$koefficients[$word]; } } if($cnt>0){ return ((100*$cnt) / count($words))*0.99; } else { return 0; } } }
В функции подсчета релевантности, используются коэффициенты, которые также учитывают позицию слова в названии текущего элемента. Как вариант, эту проверку можно переписать - использовать вместо названия, например, поисковые теги, или какое-то свойство с перечнем слов для учета релевантности (хотя поисковые теги как раз эту функцию и выполняют) - тогда релевантность станет более точной. В таком случае вызов функции подбора анализа релевантности примет вид:
$name = $arResult['NAME']; if(strlen($arResult['TAGS'])>0){ $name = $arResult['TAGS'].' '.$name; } foreach ($arSame as $key => $arElement) { $arSame[$key]['SIMILARITY'] = \Pai\Tools\CDev::caclSimilarity($arElement['NAME'], $words,$name); }
Разработка сайта
Подайте заявку на разработку сайта на базе готового решения от компании 1С-Битрикс или одного из партнеров компании. Максимально подробно опишите, чему будет посвящен сайт, если это интернет-магазин - что он будет продавать, нужна ли мультиязычность, будут ли разные типы цен (розница, опт, крупный опт), будет ли интеграция с 1С, будет ли выгрузка товаров на различные торговые площадки...
Сопровождение сайта
Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение
Работы по сайту
Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.