Ajax поиск на UMI.CMS
В данной статье я постараюсь рассказать, как можно реализовать 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 Спасибо Владиславу за корректировку.