Поиск похожих статей для выбранной

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

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

Решение: 

Для вывода статей воспользуемся обычным 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);
}
Количество показов: 2734
07.03.2013

Возврат к списку

Если вам была полезна статья можете отблагодарить автора:
Ethereum:

0x16Df809287333C49D3A237296C6248A6c08702Bc

Разработка сайта

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

Сопровождение сайта

Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение

Работы по сайту

Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.