Магазин на Yii2, часть 16. Поиск по каталогу товаров, часть вторая
06.07.2019
Теги: Web-разработка • Yii2 • Запрос • ИнтернетМагазин • КаталогТоваров • Поиск • Практика • Фреймворк
У нашего поиска есть серьезная проблема — окончания слов. Например, в каталоге есть товар «Мужские зимние ботинки», а пользователь ищет «зимняя обувь». Этот товар не попадет в результаты поиска, потому что нет точного совпадения: в поисковом запросе используется слово «зимняя», а названии товара используется слово «зимние».
Для решения этой проблемы используем стеммер Портера, который отсекает окончания и суффиксы слова, оставляя только корень. Мне удалось найти на packagist.org готовый класс стеммера для русского языка. Его и будем использовать.
Стеммер Портера — алгоритм стемминга, опубликованный Мартином Портером в 1980 году. Оригинальная версия стеммера была предназначена для английского языка. Впоследствии Мартин создал проект «Snowball» и, используя основную идею алгоритма, написал стеммеры для распространённых индоевропейских языков, в том числе для русского.
Алгоритм не использует морфологический словарь, а только применяя последовательно ряд правил, отсекает окончания и суффиксы, основываясь на особенностях языка, в связи с чем работает быстро, но не всегда безошибочно.
Итак, устанавливаем пакет с использованием composer
:
> composer require ladamalina/lingua-stem-ru
Но установка завершилась ошибкой:
[InvalidArgumentException] Could not find a version of package ladamalina/lingua-stem-ru matching your minimum-stability (stable). Require it with an explicit version constraint allowing its desired stability. require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] [<packages>]...
Это потому, что в composer.json
параметр minimum-stability
имеет значение stable
. Так что еще одна попытка:
> composer require ladamalina/lingua-stem-ru:dev-master
Все, теперь нам доступен класс LinguaStemRu
. Примеры использования класса:
$stemmer = new LinguaStemRu(); echo $stemmer->stem_word('Автомобиль') . "<br/>"; echo $stemmer->stem_word('Автомобилем') . "<br/>"; echo $stemmer->stem_word('Автомобиля') . "<br/>";
$stemmer = new LinguaStemRu(); echo $stemmer->stem_text('Любовь к Родине – это очень сильное чувство.');
любов к родин – это очен сильн чувство.
Искать будем по следующим полям таблиц базы данных:
- поле
name
таблицыproduct
(название товара) - поле
keywords
таблицыproduct
(ключевые слова) - поле
name
таблицыcategory
(название категории) - поле
name
таблицыbrand
(название бренда)
Вносим изменения в класс модели Product
:
<?php namespace app\models; use Yii; use yii\data\Pagination; use yii\db\ActiveRecord; use yii\db\Query; use Stem\LinguaStemRu; class Product extends ActiveRecord { /*.....*/ public function getSearchResult($search, $page) { $search = $this->cleanSearchString($search); if (empty($search)) { return [null, null]; } // пробуем извлечь данные из кеша $key = 'search-'.md5($search).'-page-'.$page; $data = Yii::$app->cache->get($key); if ($data === false) { // данных нет в кеше, получаем их заново // разбиваем поисковый запрос на отдельные слова $temp = explode(' ', $search); $words = []; $stemmer = new LinguaStemRu(); foreach ($temp as $item) { if (iconv_strlen($item) > 3) { // получаем корень слова $words[] = $stemmer->stem_word($item); } else { $words[] = $item; } } $relevance = "IF (`product`.`name` LIKE '%" . $words[0] . "%', 3, 0)"; $relevance .= " + IF (`product`.`keywords` LIKE '%" . $words[0] . "%', 2, 0)"; $relevance .= " + IF (`category`.`name` LIKE '%" . $words[0] . "%', 1, 0)"; $relevance .= " + IF (`brand`.`name` LIKE '%" . $words[0] . "%', 1, 0)"; for ($i = 1; $i < count($words); $i++) { $relevance .= " + IF (`product`.`name` LIKE '%" . $words[$i] . "%', 3, 0)"; $relevance .= " + IF (`product`.`keywords` LIKE '%" . $words[$i] . "%', 2, 0)"; $relevance .= " + IF (`category`.`name` LIKE '%" . $words[$i] . "%', 1, 0)"; $relevance .= " + IF (`brand`.`name` LIKE '%" . $words[$i] . "%', 1, 0)"; } $query = (new Query()) ->select([ 'id' => 'product.id', 'name' => 'product.name', 'price' => 'product.price', 'image' => 'product.image', 'hit' => 'product.hit', 'new' => 'product.new', 'sale' => 'product.sale', 'relevance' => $relevance ]) ->from('product') ->join('INNER JOIN', 'category', 'category.id = product.category_id') ->join('INNER JOIN', 'brand', 'brand.id = product.brand_id') ->where(['like', 'product.name', $words[0]]) ->orWhere(['like', 'product.keywords', $words[0]]) ->orWhere(['like', 'category.name', $words[0]]) ->orWhere(['like', 'brand.name', $words[0]]); for ($i = 1; $i < count($words); $i++) { $query = $query->orWhere(['like', 'product.name', $words[$i]]); $query = $query->orWhere(['like', 'product.keywords', $words[$i]]); $query = $query->orWhere(['like', 'category.name', $words[$i]]); $query = $query->orWhere(['like', 'brand.name', $words[$i]]); } $query = $query->orderBy(['relevance' => SORT_DESC]); // посмотрим, какой SQL-запрос был сформирован // print_r($query->createCommand()->getRawSql()); // постраничная навигация $pages = new Pagination([ 'totalCount' => $query->count(), 'pageSize' => Yii::$app->params['pageSize'], 'forcePageParam' => false, 'pageSizeParam' => false ]); $products = $query ->offset($pages->offset) ->limit($pages->limit) ->all(); // сохраняем полученные данные в кеше $data = [$products, $pages]; Yii::$app->cache->set($key, $data); } return $data; } /*.....*/ }
Для поискового запроса «зимняя обувь» будет сформирован SQL-запрос:
SELECT `product`.`id` AS `id`, `product`.`name` AS `name`, `product`.`price` AS `price`, `product`.`image` AS `image`, `product`.`hit` AS `hit`, `product`.`new` AS `new`, `product`.`sale` AS `sale`, IF (`product`.`name` LIKE '%зимн%', 3, 0) + IF (`product`.`keywords` LIKE '%зимн%', 2, 0) + IF (`category`.`name` LIKE '%зимн%', 1, 0) + IF (`brand`.`name` LIKE '%зимн%', 1, 0) + IF (`product`.`name` LIKE '%обув%', 3, 0) + IF (`product`.`keywords` LIKE '%обув%', 2, 0) + IF (`category`.`name` LIKE '%обув%', 1, 0) + IF (`brand`.`name` LIKE '%обув%', 1, 0) AS `relevance` FROM `product` INNER JOIN `category` ON `category`.`id` = `product`.`category_id` INNER JOIN `brand` ON `brand`.`id` = `product`.`brand_id` WHERE `product`.`name` LIKE '%зимн%' OR `product`.`keywords` LIKE '%зимн%' OR `category`.`name` LIKE '%зимн%' OR `brand`.`name` LIKE '%зимн%' OR `product`.`name` LIKE '%обув%' OR `product`.`keywords` LIKE '%обув%' OR `category`.`name` LIKE '%обув%' OR `brand`.`name` LIKE '%обув%' ORDER BY `relevance` DESC
- Магазин на Yii2, часть 15. Поиск по каталогу товаров, часть первая
- Магазин на Yii2, часть 35. Админка: загрузка картинок для страниц и страница 404
- Магазин на Yii2, часть 34. Показываем меню страниц в публичной части
- Магазин на Yii2, часть 33. Админка: приводим в порядок CRUD-код для страниц
- Магазин на Yii2, часть 32. Админка: удаление категорий и CRUD для страниц
- Магазин на Yii2, часть 31. Админка: загрузка изображений для категорий и брендов
- Магазин на Yii2, часть 30. Админка: WYSIWYG-редактор и изображение для товара
Поиск: Web-разработка • Yii2 • Запрос • Интернет магазин • Каталог товаров • Поиск • Практика • Фреймворк