
В данной статье я постараюсь рассказать, как можно реализовать Ajax поиск на UMI.CMS.
На wiki.umisoft.ru рассказывается как реализовать поиск с подсказкой, но это немного другой функционал. Суть заключается в том, что скрипт вытягивает из базы данных список слов, и далее при вводе поисковой фразы появляются подсказки.
Я пошел немного дальше и реализовал именно Ajax поиск. В качестве примера для реализации я взял с rozetka.com.ua.
Сразу хочу отметить, что код, которой я написал/модернизировал потребует доп. проработки, но для того, чтобы стартовать, его достаточно.
Дополнительно сообщаю, что код протестирован в последних версиях браузеров Firefox и Chrome.
Подготовка.
Для начала вам следует убедится в том, что у вас есть проиндексированные товары. Для этого следует зайти в модуль «Поиск»(/admin/search/index_control/), и убедиться что у вас есть нечто схожее с изображение ниже:
Если ничего не проиндексировано, то сделайте это.
Программирование.
Если вы используете новый формат хранения шаблонов, то у вас в шаблоне должна присутствовать следующая иерархия — templates\[название_шаблона]\classes\modules\search\.
В папке search должно быть 2 файла:
- permissions.php
- class.php
Если же вы предпочитаете использовать старый формат хранения шаблонов, вам нужно в папке \classes\modules\search\ создать три файла, если еще нет :
- permissions.custom.php
- __custom.php
Содержание файла permissions.php (permissions.custom.php)
<?php $permissions['search'] = 'ajaxSearch'; ?>
Содержание файла class.php (__custom.php)
<?php
class search_custom extends def_module {
/*
* @param $str строка для поиска
* @param $field поле в котором производить поиск, 'h1' по умолчанию
* @param $type_id иерархический тип из /admin/data/config/.xml
* @param $search_branches раздел сайта в которм произовдить поиск
*
* @param $photo_field поле из которого нужно вытащить изображение
* @param $photo_width ширина изображения
* @param $photo_height высота изображения
* @param $photo_quality качество изображения изображения
*
**/
public function ajaxSearch($str,
$field = 'h1',
$type_id,
$search_branches,
$qty_to_show = 10,
$photo_field = 'photo',
$photo_width = 50,
$photo_height = 50,
$photo_quality = 100) {
$template = 'default';
list($items_template, $item_template) = def_module::loadTemplates('search/' . $template, 'items', 'item');
// Если поле $field не указано, указываем принудительно
if(strlen($field) <= 0) $field = 'h1';
// если запрошен поиск только по определенным типам
if (strlen($type_id) > 0) {
$type_id = preg_split("/[\s,]+/", $type_id);
$type_id = array_map('intval', $type_id);
}
// если запрошен поиск только по определенным веткам
if (strlen($search_branches) > 0) {
$arr_search_by_rels = array();
$arr_branches = preg_split("/[\s,]+/", $search_branches);
foreach ($arr_branches as $i_branch => $v_branch) {
$arr_branches[$i_branch] = $this->analyzeRequiredPath($v_branch);
}
$arr_branches = array_map('intval', $arr_branches);
$arr_search_by_rels = array_merge($arr_search_by_rels, $arr_branches);
$o_selection = new umiSelection;
$o_selection->addHierarchyFilter($arr_branches, 100, true);
$o_result = umiSelectionsParser::runSelection($o_selection);
$sz = sizeof($o_result);
for ($i = 0; $i < $sz; $i++) $arr_search_by_rels[] = intval($o_result[$i]);
}
$hierarchy = umiHierarchy::getInstance();
$products_id_list = $this->ajaxRunSearch($str, $type_id, $arr_search_by_rels, false);
$item = array();
$items = array();
$count = 0;
$total = 0;
foreach($products_id_list as $product_id){
$product = $hierarchy->getElement($product_id);
// Поиск вхождения поисковой фразы в указаном поле
if(stripos(mb_convert_case($product->getValue($field), MB_CASE_LOWER, "UTF-8"), mb_convert_case($str, MB_CASE_LOWER, "UTF-8")) !== false){
$emarket = cmsController::getInstance()->getModule("emarket"); // "Подключение" к модулю emarket
$item['attribute:id'] = $product->id;
$item['attribute:link'] = $product->link;
$item['name'] = $product->getName();
$item["$field"] = $product->getValue($field);
$item["price"] = $emarket->getPrice($product); // Получение цены с учетом скидки
// Формирование изображения
if (strlen($photo_field) > 0) {
$system = &system_buildin_load("system"); // "Подключение" к модулю system
$photo = '.' . $product->getValue($photo_field);
$thumb = makeThumbnailFull($photo, $system->thumbs_path, $photo_width, $photo_height, true, 5, false, $photo_quality);
$item['photo'] = $thumb['src'];
}
$count++;
$items[] = def_module::parseTemplate($item_template, $item);
if($count == $qty_to_show) break;
}
}
$items = array('subnodes:items' => $items);
foreach($products_id_list as $product_id){
$product = $hierarchy->getElement($product_id);
if(stripos($product->getValue($field), $str) !== false){
$total++;
}
}
$items['total'] = $total;
return def_module::parseTemplate($items_template, $items);
}
/**
* Искать по поисковому индексу
* @param String $str поисковая строка
* @param Array $search_types = NULL если указан, то будут выраны только страницы с необходимым hierarchy-type-id
* @param Array $hierarchy_rels = NULL если указан, то искать только в определенном разделе сайта
* @param Boolean $orMode = false если true, то искать в режиме OR, иначе в режиме AND
* @return Array массив, стостоящий из id найденых страниц
*
* Исходная функция расположена в \classes\system\subsystems\models\search
*/
public function ajaxRunSearch($str, $search_types = NULL, $hierarchy_rels = NULL, $orMode = false) {
$words_temp = $this->ajaxSplitString($str);
$words = Array();
foreach ($words_temp as $word) {
if (wa_strlen($word) >= 2) {
$words[] = $word;
}
}
$elements = $this->ajaxBuildQueries($words, $search_types, $hierarchy_rels, $orMode);
return $elements;
}
public static function ajaxSplitString($str) {
if(is_object($str)) { //TODO: Temp
return NULL;
}
$to_space = Array(" ", ""e;", ". ", ", ", " .", " ,", "?", ":", ";", "%", ")", "(", "/", "<", ">", "- ", " -", "«", "»");
$str = str_replace(">", "> ", $str);
$str = str_replace("\"", " ", $str);
$str = strip_tags($str);
$str = str_replace($to_space, " ", $str);
$str = preg_replace("/([ \t\r\n]{1-100})/u", " ", $str);
$str = wa_strtolower($str);
$tmp = explode(" ", $str);
$res = Array();
foreach ($tmp as $v) {
$v = trim($v);
if (wa_strlen($v) <= 1) continue;
$res[] = $v;
}
return $res;
}
public function ajaxBuildQueries($words, $search_types = NULL, $hierarchy_rels = NULL, $orMode = false) {
$lang_id = cmsController::getInstance()->getCurrentLang()->getId();
$domain_id = cmsController::getInstance()->getCurrentDomain()->getId();
$morph_disabled = mainConfiguration::getInstance()->get('system','search-morph-disabled');
$words_conds = Array();
$wordsChoose = Array();
foreach ($words as $i => $word) {
if (wa_strlen($word) < 2) {
unset($words[$i]);
continue;
}
$word = l_mysql_real_escape_string($word);
$word = str_replace(Array("%", "_"), Array("\\%", "\\_"), $word);
$word_subcond = "siw.word LIKE '{$word}%' ";
if (!$morph_disabled) {
$word_base = language_morph::get_word_base($word);
if ((wa_strlen($word_base) >= 3) && ($word_base != $word) ) {
$word_base = l_mysql_real_escape_string($word_base);
$word_subcond .= " OR siw.word LIKE '{$word_base}%'";
}
}
$words_conds[] = "(" . $word_subcond . ")";
$wordsChoose[] = " WHEN (" . $word_subcond . ") THEN '{$word}'";
}
$words_cond = implode(" OR ", $words_conds);
$wordsChooseString = "(CASE" . implode($wordsChoose) . " END) as search_word";
if ($words_cond == false) {
return Array();
}
$perms_sql = "";
$perms_tbl = "";
if (!permissionsCollection::getInstance()->isSv()) {
$users = cmsController::getInstance()->getModule("users");
$user_id = $users->user_id;
$user = umiObjectsCollection::getInstance()->getObject($user_id);
$groups = $user->getValue("groups");
$groups[] = $user_id;
$groups[] = regedit::getInstance()->getVal("//modules/users/guest_id");
$groups = array_extract_values($groups);
$groups = implode(', ', $groups);
$perms_sql = " AND c3p.level >= 1 AND c3p.owner_id IN({$groups})";
$perms_tbl = "INNER JOIN cms3_permissions as `c3p` ON c3p.rel_id = s.rel_id";
}
$types_sql = "";
if (is_array($search_types)) {
if (sizeof($search_types)) {
if ($search_types && $search_types[0]) {
$types_sql = " AND s.type_id IN (" . l_mysql_real_escape_string(implode(", ", $search_types)) . ")";
}
}
}
$hierarchy_rels_sql = "";
if (is_array($hierarchy_rels) && count($hierarchy_rels)) {
$hierarchy_rels_sql = " AND h.rel IN (" . l_mysql_real_escape_string(implode(", ", $hierarchy_rels)) . ")";
}
l_mysql_query("CREATE TEMPORARY TABLE temp_search (rel_id int unsigned, tf float, word varchar(64), search_word varchar(64))");
$sql = <<<EOF
INSERT INTO temp_search SELECT SQL_SMALL_RESULT HIGH_PRIORITY
s.rel_id,
si.weight,
siw.word,
$wordsChooseString
FROM cms3_search_index_words as `siw`
INNER JOIN cms3_search_index as `si` ON si.word_id = siw.id
INNER JOIN cms3_search as `s` ON s.rel_id = si.rel_id
INNER JOIN cms3_hierarchy as `h` ON h.id = s.rel_id
{$perms_tbl}
WHERE
({$words_cond}) AND
s.domain_id = '{$domain_id}' AND
s.lang_id = '{$lang_id}' AND
h.is_deleted = '0' AND
h.is_active = '1'
{$types_sql}
{$hierarchy_rels_sql}
{$perms_sql}
GROUP BY s.rel_id, si.weight, search_word
EOF;
$res = Array();
l_mysql_query($sql);
if($orMode) {
$sql = <<<SQL
SELECT rel_id, SUM(tf) AS x
FROM temp_search
GROUP BY rel_id
ORDER BY x DESC
SQL;
} else {
$wordsCount = sizeof($words);
$sql = <<<SQL
SELECT rel_id, SUM(tf) AS x, COUNT(word) AS wc
FROM temp_search
GROUP BY rel_id
HAVING wc >= '{$wordsCount}'
ORDER BY x DESC
SQL;
}
$result = l_mysql_query($sql);
while(list($element_id) = mysql_fetch_row($result)) {
$res[] = $element_id;
}
l_mysql_query("DROP TEMPORARY TABLE IF EXISTS temp_search");
return $res;
}
public function prepareContext($element_id, $uniqueOnly = false) {
if (!($element = umiHierarchy::getInstance()->getElement($element_id))) {
return false;
}
if ($element->getValue("is_unindexed")) return false;
$context = Array();
$type_id = $element->getObject()->getTypeId();
$type = umiObjectTypesCollection::getInstance()->getType($type_id);
$field_groups = $type->getFieldsGroupsList();
foreach($field_groups as $field_group_id => $field_group) {
foreach($field_group->getFields() as $field_id => $field) {
if ($field->getIsInSearch() == false) continue;
$field_name = $field->getName();
$data_type = $field->getFieldType()->getDataType();
$val = $element->getValue($field_name);
if ($data_type == 'relation') {
if (!is_array($val)) {
$val = array($val);
}
foreach ($val as $i => $v) {
if ($item = selector::get('object')->id($v)) {
$val[$i] = $item->name;
}
}
$val = implode(' ', $val);
}
if (is_null($val) || !$val) continue;
if(is_object($val)) {
continue;
}
$context[] = $val;
}
}
if ($uniqueOnly) {
$context = array_unique($context);
}
$res = "";
foreach ($context as $val) {
if(is_array($val)) {
continue;
}
$res .= $val . " ";
}
$res = preg_replace("/%[A-z0-9_]+ [A-z0-9_]+\([^\)]+\)%/im", "", $res);
$res = str_replace("%", "%", $res);
return $res;
}
}
?>
После того как вы это сделали, нужно модифицировать шаблон сайта.
Для начала откройте файл в котором хранится форма поиска и:
1. В сроку поиска добавьте id="ajaxSearch"
2. Ниже следует добавить:
<div id="ajaxSearch_repose" class="b-ajaxSearch_repose" />
У вас должно получиться нечто следующее:
<form class="search" action="/search/search_do/" method="get">
<input id="ajaxSearch" type="text" value="&search-default-text;" name="search_string" class="textinputs" onblur="javascript: if(this.value == '') this.value = '&search-default-text;';" onfocus="javascript: if(this.value == '&search-default-text;') this.value = '';" x-webkit-speech="" speech="" />
<div id="ajaxSearch_repose" class="b-ajaxSearch_repose" />
</form>
Данный код взят с файла — \templates\demodizzy\xslt\modules\search\search-form.xsl
Далее в ваш файл стилей добавить стилизации:
.b-ajaxSearch_repose {
position: absolute;
}
.b-ajaxSearch_repose__items {
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__item {
color: #4A89BA;
display: block;
margin-bottom: 10px;
text-decoration: none;
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__item .__item-image {
padding: 2px;
border: 1px solid #BABABA;
background: #fff;
float: left;
width: 40px; /* Связь :) */
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__item .__item-info {
margin-left: 50px; /* Связь :) */
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__item .__item-title {
font-size: 14px;
text-decoration: underline;
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__item:hover .__item-title {
text-decoration: none;
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__item .__item-price {
color: #363636;
font-weight: bold;
}
.b-ajaxSearch_repose__items .b-ajaxSearch_repose__all {
text-decoration: none;
float: right;
}
.spacer {
clear: both;
}
После этого нужно либо создать новый js-файл или добавить код в существующий:
/**
* AJAX поиск для UMI.CMS
*/
jQuery(function($) {
var input_id = $('#ajaxSearch'); // ID поля ввода для поиска
var repose_id = '#ajaxSearch_repose'; // Элемент, в который будет вставлен результат на странице
// Шаблон для вставки
var wrapper_class = 'b-ajaxSearch_repose__container'; // Класс для var wrapper
var items_wrapper_id = ''; //
var wrapper = $('<div class="' + wrapper_class + '"></div>').hide(); // Элемент, в который будет вставлен результат
var html = ''; // строка, в которую будет записывать результат работы AJAX запроса
var field = 'h1'; // поле в котором производить поиск, 'h1' по умолчанию
var type_id = 45; // иерархический тип из /admin/data/config/.xml
var search_branches = ''; // раздел сайта в которм произовдить поиск
var qty_to_show = 5; // количество отображаемых элементов в результате
var photo_field = 'photo'; // поле из которого нужно вытащить изображение
var photo_width = 30; // ширина изображения
var photo_height = 30; // высота изображения
var photo_quality = 100; // качество изображения изображения
var suffix = ' грн.'; // пробел обязателен перед названием
var timer = null;
var delay = 1500; // кол-во секунд, через которое скрипт отправит запрос на сервер
var empty_msg = 'По вашему запросу ничего не найдено.<br />Попробуйте уточнить свой запрос. '; // Ничего не найденно
var str_cache = null; // Переменная, в которую будет записан результат поиска
var all_result = '/search/search_do/?search_string=';
var all_result_text = 'Все результаты поиска →';
// Стилизация выпадающего блока с результатам поиска
wrapper.css({
'display': 'none',
'background-color': '#F3F3F3',
'border': '1px solid #DEDEDE',
'border-radius': '9px',
'padding': '10px',
'position': 'relative',
'top': '15px',
'width': '250px'
});
$(input_id).keyup(function(e) {
var $this = $(this);
// фраза для поиска
var str = $this.val();
// Если в поле введено меньше 2 символов, то скрипт не сработает
if(str.length < 2) return false;
// Если введенная строка эквивалентна предыдущему запросу
// ajax запрос не совершается а показываются предыдущие результаты
if(str === str_cache) {
$('.' + wrapper_class).show(1, function() {
hideResult(wrapper_class);
});
return false;
} else str_cache = str; // сохраняем предыдущий запрос
// Чистим timer от предыдущего ввода
if (timer != null) clearTimeout(timer);
timer = setTimeout(function() {
var path = '/udata/search/ajaxSearch';
path += '/' + str;
path += '/' + field;
path += '/' + type_id;
path += '/' + search_branches;
path += '/' + qty_to_show;
path += '/' + photo_field;
path += '/' + photo_width;
path += '/' + photo_height;
path += '/' + photo_quality;
$.ajax({
type: "POST",
dataType: 'json',
url: path + '.json',
beforeSend : function(data) {
console.log(data);
},
success : function(data) {
if(html.length > 1) html = '';
html += '<div class="b-ajaxSearch_repose__items" id="' + items_wrapper_id + '">';
// Если нет объектов в ответе нужно выдать ошибку
if(typeof data.items.item != 'undefined') {
// Перебор объектов
for (var i in data.items.item) {
var item = data.items.item[i];
html += '<a href="' + item['link'] + '" class="b-ajaxSearch_repose__item">';
html += '<div class="__item-image">';
html += '<img src="' + item[photo_field] + '" title="' + item[field] + '" />';
html += '</div>';
html += '<div class="__item-info">';
html += '<div class="__item-title">' + item[field] + '</div>';
html += '<div class="__item-price">' + item['price'] + suffix + '</div>';
html += '</div>';
html += '<div class="spacer"></div>';
html += '</a>';
}
// Если найденных элементов больше чем показано в HTML
// показываем ссылку на разельтаты поиска
if(data.total > qty_to_show) {
html += '<a href="'+ all_result + str +'" class="b-ajaxSearch_repose__all">';
html += all_result_text;
html += '</a>';
}
} else {
html += empty_msg;
}
html += '<div class="spacer"></div>';
html += '</div>';
},
complete: function() {
$(wrapper).html(''); // Подчищаем предыдущий результат
$(html).appendTo(wrapper); // Вставляем только что сгенерированный html
$(wrapper).appendTo(repose_id); // Вставляем объект на страницу
$(wrapper).show();
$('.' + wrapper_class).show(1, function() {
hideResult(wrapper_class);
});
},
error : function(data) {
console.log(data);
}
});
}, delay);
});
// Ловим фокус на поле ввода
$(input_id).click(function() {
// Если есть результаты, то они будут показаны
// $('.' + wrapper_class).show(1, function() {
$('.' + wrapper_class).show(1, function() {
hideResult(wrapper_class);
});
});
function hideResult(class_name) {
$(document).bind({
'click.ajaxSearch' : function(e) {
var target = $(e.target);
if (target.is('.' + class_name) || target.parents('.' + class_name).length) return;
$('.' + class_name).hide();
$(document).unbind('click.ajaxSearch', arguments.callee);
}
});
//$(document).mousedown(function(e){
// var target = $(e.target);
// console.log(target);
/*if (target.is('.' + class_name) || target.parents('.' + class_name).length) return;
$(document).unbind('click', arguments.callee);
$('.' + class_name).hide();*/
//});
}
});
На этом с кодом все.
В результате, если вы введете в строку поиска поисковый запрос, то должны увидеть следующее:
Напоминаю, что демо реализации можно увидеть вот здесь — umi.pontyk.com.ua. Весь исходный код доступен на bitbucket.org
Информация по функции ajaxSearch. Функция принимает следующие параметры:
$str — строка для поиска
$field поле в котором производить поиск, 'h1' по умолчанию, могут быть другие поля.
$type_id иерархический тип данных объекта каталога. Посмотреть можно вот здесь — /admin/data/config/.xml
$search_branches раздел сайта в котором следует производить поиск
$photo_field поле из которого нужно вытащить изображение
$photo_width ширина изображения
$photo_height высота изображения
$photo_quality качество изображения изображения
Информация по js-скрипту приводить не буду, так как в он достаточно хорошо прокомментирован.
Что было доработано:
По умолчанию в UMI.CMS ищет поисковую фразу, которая равна или более 3 символов. Мною были взяты функции UMI, которые отвечали за поиск и модифицированы, чтобы поиск осуществлялся от 2 символов.
И прочие штуки...
UPD 09.04.2014 Спасибо Владиславу за корректировку.