
Поиск по сайту является обязательным функционалом любого современного сайта, особенно интернет-магазина с большим количеством товаров, так как значительно упрощает их поиск и делает сайт удобным для посетителей.
В данной статье мы, шаг за шагом сделаем поиск по сайту, точнее по каталогу, а также, добавим функционал для улучшения релевантности поиска.
Предлагаю опустить процесс установки фреймворка Laravel, настройки и подключения базы данных. Уверен, что для Вас не проблема, а при необходимости Вы можете легко это сделать самостоятельно, так как это подробно описано в самой документации к фреймворку и многочисленных гайдах в сети.
И так представим, что в процессе разработки интернет-магазине нам нужно сделать поиск товаров каталога по их названиям. Фреймворк уже установлен, база данных подключена, необходимые модели и миграции созданы, в том числе и таблица базы данных товаров: products и модель Product.
В таблица товаров products – содержит ряд полей с информацией о товаре: цене, характеристиках товара, описании и т.д., в том числе поле: name, строковое значение которого содержит информацию о названии товара. Именно по нему мы будем выбирать товары каталога.
И так приступим, сам процесс создания поиска сделаем в несколько этапов. Создадим:
- Форму поиска по каталогу
- Маршрут, к методу контроллера обработчику запроса
- Метод контроллера – обработчик поискового запроса
- View – шаблон для вывода результатов поиска.
Форма поиска по каталогу
Форма поиска в моем случае находится в шапке, в блоке HEADER сайта, и элемент View – шаблона отвечающего за вывод формы поиска выглядит так:
Обратите внимание, данная форма отправляется методом GET, содержит action – с маршрутом, который мы сейчас создадим. Так же, вы можете заметить код обработки сообщения о наличии валидационных ошибок: @error('s') is-invalid @enderror.
Маршрут, к методу контроллера обработчику запроса
Создаем маршрут, который отправляет наш запрос на сервер методом GET:
// routes/web.php
Route::get('/search', [App\Http\Controllers\Front\CatalogController::class, 'search'])->name('search');
Как видите, маршрут обращается к методу search контроллера CatalogController, я не стал создавать для поиска по сайту отдельный контроллер, а просто добавил метод в уже имеющейся котроллер.
Метод котроллера для обработки запроса формы
public function search(Request $request)
{
$s = $request->s;
$request->validate([
's' => 'required',
]);
$products = Product::where('name', 'LIKE', "%{$s}%")->paginate(20);
return view('front.catalog.search', compact('products', 's'));
}
- Тут мы сохраняем в переменную $s – запрос и валидируем полученные данные. Всего одно правило валидации – required, то есть значение обязательно, и в случае если пользователь отправит пустую форму, контролер вернет валидационную ошибку.
- Выбираем товары, соответствующие поисковой фразе, с использованием постраничной навигации.
- Возвращаем результат. Файл шаблона search.blade.php - и переменные: $s – содержание поискового запроса, и $products – с данными выбранных товаров, название которых соответствует запросу пользователя.
View – шаблон для вывода результатов поиска.
@section('content')
@forelse ($products as $product)
{{$product->name}}
//и другие данные товара
@empty
По запросу {{$s}} товаров не найдено.
@endforelse
{{$products->links()}}
@endsection
Тут все просто, в цикле blade шаблона выводим результаты поиска с пагинацией, или сообщение об отсутствии соответствующих запросу товаров.
Все, можно сказать, что поиск по каталогу работает. Однако, далеко не идеален, так как конструкция: Product::where('name', 'LIKE', "%{$s}%")->paginate(20); – в методе нашего контроллера, выберет только товары название которых строго соответствует запросу пользователя, точнее точному вхождению.
Предположим, что наш магазин расположен в Химках и в его каталоге содержится группа товаров:
- Член деревянный 0,5 м;
- Член деревянный 1 м;
- Член деревянный 1,5 м и т.д.
Однако пользователь может ввести в форму поиска запрос, как член из дерева, деревянные члены, деревянный член, и т.д., и в таком случае результат поиска окажется пустым, так как нет точного соответствия вхождения запроса в название товаров.
Для решения данной проблемы, модифицируем наш запрос, а именно:
- удалим лишние символы и пробелы из запроса,
- разобьем запрос на отдельные слова,
- у каждого полученного слова уберем склонения, окончания, оставим только корень - часть слова,
- и наконец сделаем запрос, обращение к базе данных для поиска по каждой отдельной полученной части слова.
Для получения частей слова, удаления лишних окончаний воспользуемся алгоритмом Стеммером Портера.
Стеммер Портера — алгоритм нахождения основы слова, примененный Мартином Портером в 1980 году. Первая версия стеммера была создана для работы со словами на английском языке, и позже стала применятся для других, в том числе для русского языка.
Для использования алгоритма, можете воспользоваться композером и установить пакет ladamalina/php-lingua-stem-ru, однако при установке данного пакета могут возникнуть проблемы, так как данный пакет был написан под старые версии PHP и давно не обновлялся.
Но ничто на мешает нам воспользоваться готовым классом. Для этого создадим папку library в корне проекта Laravel, а в ней класс со следующим содержанием:
Stem_Caching && isset($this->Stem_Cache[$word])) {
return $this->Stem_Cache[$word];
}
$stem = $word;
do {
if (!preg_match($this->RVRE, $word, $p)) break;
$start = $p[1];
$RV = $p[2];
if (!$RV) break;
# Step 1
if (!$this->s($RV, $this->PERFECTIVEGROUND, '')) {
$this->s($RV, $this->REFLEXIVE, '');
if ($this->s($RV, $this->ADJECTIVE, '')) {
$this->s($RV, $this->PARTICIPLE, '');
} else {
if (!$this->s($RV, $this->VERB, ''))
$this->s($RV, $this->NOUN, '');
}
}
# Step 2
$this->s($RV, '/и$/', '');
# Step 3
if ($this->m($RV, $this->DERIVATIONAL))
$this->s($RV, '/ость?$/', '');
# Step 4
if (!$this->s($RV, '/ь$/', '')) {
$this->s($RV, '/ейше?/', '');
$this->s($RV, '/нн$/', 'н');
}
$stem = $start.$RV;
} while(false);
if ($this->Stem_Caching) $this->Stem_Cache[$word] = $stem;
return $stem;
}
/**
* Стэмит все русские слова в тексте, оставляя пробелы и прочие знаки препинания на месте.
* @param $text
* @return string
*/
public function stem_text($text)
{
$separators_arr= array('?',' ', '.', ',', ';','!','"','\'','`',"\r","\n","\t");
$pos = 0;
while($posstem_word($word);
$word_stemmed_part = str_replace($word,$word_stemmed,$word_part);
$text = mb_substr($text,0,$pos) . $word_stemmed_part . mb_substr($text, $newpos);
$pos = $newpos - (mb_strlen($word)-mb_strlen($word_stemmed));
}
}
return $text;
}
public function stem_caching($parm_ref)
{
$caching_level = @$parm_ref['-level'];
if ($caching_level) {
if (!$this->m($caching_level, '/^[012]$/')) {
die(__CLASS__ . "::stem_caching() - Legal values are '0','1' or '2'. '$caching_level' is not a legal value");
}
$this->Stem_Caching = $caching_level;
}
return $this->Stem_Caching;
}
public function clear_stem_cache()
{
$this->Stem_Cache = array();
}
}
Для выше перечисленных задач по модификации поисковой фразы, и построения запроса к базе данных создадим scope метод в модели Product:
public function scopeLike($query, $s)
{
$s= iconv_substr($s, 0, 64);
$s = preg_replace('#[^0-9a-zA-ZА-Яа-яёЁ]#u', ' ', $s);
$s = preg_replace('#\s+#u', ' ', $s);
$s = trim($s);
if (empty($s)) {
return $query->whereNull('id'); // возвращаем пустой результат
}
$temp = explode(' ', $s);
$words = [];
$stemmer = new \library\LinguaStemRu;
foreach ($temp as $item) {
if (iconv_strlen($item) > 3) {
$words[] = $stemmer->stem_word($item);
} else {
$words[] = $item;
}
}
$relevance = "IF (`products`.`name` LIKE '%" . $words[0] . "%', 2, 0)";
for ($i = 1; $i < count($words); $i++) {
$relevance .= " + IF (`products`.`name` LIKE '%" . $words[$i] . "%', 2, 0)";
}
$query->select('products.*', \DB::raw($relevance . ' as relevance'))
->where('products.name', 'like', '%' . $words[0] . '%');
for ($i = 1; $i < count($words); $i++) {
$query = $query->orWhere('products.name', 'like', '%' . $words[$i] . '%');
}
$query->orderBy('relevance', 'desc');
return $query;
}
И уже теперь, код в методе контроллера будет таким:
public function search(Request $request)
{
$s = $request->s;
$request->validate([
's' => 'required',
]);
$products = Product::like($request->s)->paginate(20);
return view('front.catalog.search', compact('products', 's'));
}
Таким образом, мы сделали максимально точный, соответствующий поисковой фразе, и при этом простой поиск товаров каталога сайта на фреймворке Laravel.
Комментарии
Андрей
Гость
Регистрация Войти