Поступила тут интересная задача: у клиента на сайте выводятся статьи. И вот он захотел, чтобы на странице с полным текстом статьи выводился блок с другими статьями, которые будут похожи на данную. (Под похожими имеются ввиду статьи, в названии или поисковых тегах которых совпадают хотя бы одно слово).
Решение:
Для вывода статей воспользуемся обычным 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);
}
