Магазин на Laravel 7, часть 25. Поиск по каталогу товаров, деплой проекта на хостинг TimeWeb
29.11.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Класс • Поиск • Практика • Таблица • Форма • ШаблонСайта
Поиск по каталогу товаров
Какой каталог товаров без поиска? Тем более, что и форма у нас уже есть. Искать будем по полям name и content таблицы products, полю name таблицы categories и полю name таблицы brands. У нас должен получиться примерно такой SQL-запрос для поиска «мужская зимняя обувь».
SELECT
`products`.*,
IF (`products`.`name` LIKE '%мужск%', 2, 0) + IF (`products`.`content` LIKE '%мужск%', 1, 0) +
IF (`categories`.`name` LIKE '%мужск%', 1, 0) + IF (`brands`.`name` LIKE '%мужск%', 2, 0) +
IF (`products`.`name` LIKE '%зимн%', 2, 0) + IF (`products`.`content` LIKE '%зимн%', 1, 0) +
IF (`categories`.`name` LIKE '%зимн%', 1, 0) + IF (`brands`.`name` LIKE '%зимн%', 2, 0) +
IF (`products`.`name` LIKE '%обув%', 2, 0) + IF (`products`.`content` LIKE '%обув%', 1, 0) +
IF (`categories`.`name` LIKE '%обув%', 1, 0) + IF (`brands`.`name` LIKE '%обув%', 2, 0)
AS `relevance`
FROM
`products`
INNER JOIN `categories` ON `categories`.`id` = `products`.`category_id`
INNER JOIN `brands` ON `brands`.`id` = `products`.`brand_id`
WHERE
`products`.`name` LIKE '%мужск%' OR `products`.`content` LIKE '%мужск%' OR
`categories`.`name` LIKE '%мужск%' OR `brands`.`name` LIKE '%мужск%' OR
`products`.`name` LIKE '%зимн%' OR `products`.`content` LIKE '%зимн%' OR
`categories`.`name` LIKE '%зимн%' OR `brands`.`name` LIKE '%зимн%' OR
`products`.`name` LIKE '%обув%' OR `products`.`content` LIKE '%обув%' OR
`categories`.`name` LIKE '%обув%' OR `brands`.`name` LIKE '%обув%'
ORDER BY
`relevance` DESC
Чтобы решить проблему окончания слов — используем стеммер Портера, который отсекает лишнее, оставляя только корень. Мне удалось найти на packagist.org готовый класс стеммера для русского языка.
Стеммер Портера — алгоритм стемминга, опубликованный Мартином Портером в 1980 году. Оригинальная версия стеммера была предназначена для английского языка. Впоследствии Мартин создал проект «Snowball» и, используя основную идею алгоритма, написал стеммеры для распространённых индоевропейских языков, в том числе для русского.
Алгоритм не использует морфологический словарь, а только применяя последовательно ряд правил, отсекает окончания и суффиксы, основываясь на особенностях языка, в связи с чем работает быстро, но не всегда безошибочно.
Итак, устанавливаем пакет с использованием composer:
> composer require ladamalina/lingua-stem-ru
Все, теперь нам доступен класс LinguaStemRu. Пример использования класса:
$stemmer = new LinguaStemRu(); echo $stemmer->stem_text('Любовь к Родине – это очень сильное чувство');
любов к родин – это очен сильн чувство
Добавляем новый маршрут в файле route/web.php:
/* * Каталог товаров: категория, бренд и товар */ Route::group([ 'as' => 'catalog.', // имя маршрута, например catalog.index 'prefix' => 'catalog', // префикс маршрута, например catalog/index ], function () { // главная страница каталога Route::get('index', 'CatalogController@index') ->name('index'); // категория каталога товаров Route::get('category/{category:slug}', 'CatalogController@category') ->name('category'); // бренд каталога товаров Route::get('brand/{brand:slug}', 'CatalogController@brand') ->name('brand'); // страница товара каталога Route::get('product/{product:slug}', 'CatalogController@product') ->name('product'); // страница результатов поиска Route::get('search', 'CatalogController@search') ->name('search'); });
Изменяем форму поиска в layout-шаблоне site.blade.php:
<form action="{{ route('catalog.search') }}" class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" name="query" placeholder="Поиск по каталогу" aria-label="Search"> <button class="btn btn-outline-light my-2 my-sm-0" type="submit">Поиск</button> </form>
Добавляем новый метод search() в контроллер CatalogController:
class CatalogController extends Controller { /* ... */ public function search(Request $request) { $search = $request->input('query'); $query = Product::search($search); $products = $query->paginate(6)->withQueryString(); return view('catalog.search', compact('products', 'search')); } }
Добавляем новый метод scopeSearch() в модель Product:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; use Stem\LinguaStemRu; class Product extends Model { /** * Позволяет искать товары по заданным словам * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $search * @return \Illuminate\Database\Eloquent\Builder */ public function scopeSearch($query, $search) { // обрезаем поисковый запрос $search = iconv_substr($search, 0, 64); // удаляем все, кроме букв и цифр $search = preg_replace('#[^0-9a-zA-ZА-Яа-яёЁ]#u', ' ', $search); // сжимаем двойные пробелы $search = preg_replace('#\s+#u', ' ', $search); $search = trim($search); if (empty($search)) { return $query->whereNull('id'); // возвращаем пустой результат } // разбиваем поисковый запрос на отдельные слова $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 (`products`.`name` LIKE '%" . $words[0] . "%', 2, 0)"; $relevance .= " + IF (`products`.`content` LIKE '%" . $words[0] . "%', 1, 0)"; $relevance .= " + IF (`categories`.`name` LIKE '%" . $words[0] . "%', 1, 0)"; $relevance .= " + IF (`brands`.`name` LIKE '%" . $words[0] . "%', 2, 0)"; for ($i = 1; $i < count($words); $i++) { $relevance .= " + IF (`products`.`name` LIKE '%" . $words[$i] . "%', 2, 0)"; $relevance .= " + IF (`products`.`content` LIKE '%" . $words[$i] . "%', 1, 0)"; $relevance .= " + IF (`categories`.`name` LIKE '%" . $words[$i] . "%', 1, 0)"; $relevance .= " + IF (`brands`.`name` LIKE '%" . $words[$i] . "%', 2, 0)"; } $query->join('categories', 'categories.id', '=', 'products.category_id') ->join('brands', 'brands.id', '=', 'products.brand_id') ->select('products.*', DB::raw($relevance . ' as relevance')) ->where('products.name', 'like', '%' . $words[0] . '%') ->orWhere('products.content', 'like', '%' . $words[0] . '%') ->orWhere('categories.name', 'like', '%' . $words[0] . '%') ->orWhere('brands.name', 'like', '%' . $words[0] . '%'); for ($i = 1; $i < count($words); $i++) { $query = $query->orWhere('products.name', 'like', '%' . $words[$i] . '%'); $query = $query->orWhere('products.content', 'like', '%' . $words[$i] . '%'); $query = $query->orWhere('categories.name', 'like', '%' . $words[$i] . '%'); $query = $query->orWhere('brands.name', 'like', '%' . $words[$i] . '%'); } $query->orderBy('relevance', 'desc'); return $query; } }
Искать будем по следующим полям таблиц базы данных:
- поле
nameтаблицыproducts(название товара) - поле
contentтаблицыproducts(описание товара) - поле
nameтаблицыcategories(название категории) - поле
nameтаблицыbrands(название бренда)
Причем вклад в релевантность у них разный — у названия товара и названия бренда больше, у названия категории и описания товара меньше.
Осталось только создать шаблон views/catalog/search.blade.php:
@extends('layout.site', ['title' => 'Поиск по каталогу']) @section('content') <h1>Поиск по каталогу</h1> <p>Поисковый запрос: {{ $search ?? 'пусто' }}</p> @if (count($products)) <div class="row"> @foreach ($products as $product) @include('catalog.part.product', ['product' => $product]) @endforeach </div> {{ $products->links() }} @else <p>По вашему запросу ничего не найдено</p> @endif @endsection
Деплой на хостинг TimeWeb
1. Создаем репозиторий на GitHub
Сначала создадим новый репозиторий на GitHub, у меня это laravel-7-shop:
Сразу после создания нового репозитория получим подсказки, что делать дальше.
На локальном компьютере тоже создаем репозиторой, чтобы выложить на GitHub:
$ cd D:/work/localhost21/www # это директория проекта
$ git init # создаем пустой репозиторий Initialized empty Git repository in D:/work/localhost21/www/.git/ $ git add --all # добавляем все файлы проекта $ git commit -m "Initial сommit" # первый коммит
У нас уже есть подсказка от GitHub, что надо сделать, чтобы выложить проект (не торопитесь выполнять):
$ git remote add origin https://github.com/tokmakov/laravel-7-shop.git $ git branch -M main # изменить имя с master на main $ git push -u origin main
master на main. Потому что имя master им представляется недостаточно толерантным. Но мы не будем следовать сомнительным советам, имя master вполне подходит.
Добавляем себе ссылку (pointer) на удаленный репозиторий:
$ git remote add origin https://github.com/tokmakov/laravel-7-shop.git
Теперь удаленный репозиторий доступен как origin:
$ git remote origin
Отправляем текущую ветку master (которая у нас сейчас всего одна) в удаленную ветку master репозитория origin:
$ git push origin master
Настроим текущую ветку master на отслеживание удаленной ветки master:
$ git branch -u origin/master Branch master set up to track remote branch master from origin.
2. Клонируем проект на сервере
В панели управления хостинга нужно включить доступ по SSH и подключиться из командной строки к серверу. На хостинге TimeWeb уже установлен Composer, только нужно задать алиасы (см. здесь) для composer и php.
$ pwd /home/c/ca12345 $ nano .bash_profile
alias composer='/opt/php72/bin/php -d memory_limit=1024M /usr/local/bin/composer' alias php='/opt/php72/bin/php -d memory_limit=1024M'
Чтобы применить изменения из файла .bash_profile:
$ source ~/.bash_profile
Проверяем, что Composer нам теперь доступен:
$ composer ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 1.9.0 2019-08-02 20:55:32 Usage: command [options] [arguments] ..........
Создаем директорию для нашего проекта в панели управления хостингом, у меня это laravel-7-shop. Внутри будет автоматически создана директория public_html, которая является корнем сервера. Нам это директория не нужна, потому что у нас корень сервера — директория public.
$ rm -r ~/laravel-7-shop/public_html
Клонируем в laravel-7-shop репозиторий с GitHub:
$ git clone https://github.com/tokmakov/laravel-7-shop.git laravel-7-shop
3. Устанавливаем проект на сервере
Устанавливаем проект с помощью Composer:
$ cd laravel-7-shop $ composer install --no-dev Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 122 installs, 0 updates, 0 removals - Installing phpdocumentor/reflection-common (2.2.0): Downloading (100%) - Installing phpdocumentor/type-resolver (1.4.0): Downloading (100%) .......... Discovered Package: nunomaduro/collision Package manifest generated successfully.
4. Создаем новую базу данных
В панели управления хостингом создаем новую базу данных, у меня это ca12345_lar7shop. Создаем файл конфигурации приложения, указываем имя БД, имя пользователя БД и пароль.
$ cp .env.example .env $ nano .env
APP_NAME="Магазин одежды и обуви" APP_ENV=production APP_KEY= APP_DEBUG=false APP_URL=http://laravel-7-shop.tokmakov.msk.ru LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=ca12345_lar7shop DB_USERNAME=ca12345_lar7shop DB_PASSWORD=FapeTZ61
Создаем ключ приложения Laravel:
$ php artisan key:generate Application key set successfully.
5. Создаем таблицы базы данных
Запускаем миграцию, чтобы создать таблицы БД и тестовые данные:
$ php artisan migrate:fresh --seed ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yes Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Illuminate\Database\QueryException SQLSTATE[HY000]: General error: 1709 Index column size too large. The maximum column size is 767 bytes. (SQL: alter table `users` add unique `users_email_unique`(`email`))
Исправить это легко, изменяем файл AppServiceProvider.php:
class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { Schema::defaultStringLength(191); } /* ... */ }
Выкладываем изменения на GitHub:
$ git commit -a -m "default string length" $ git push
На сервере получаем изменения:
$ git pull
Снова запускаем миграцию и получаем следующую порцию ошибок, что класс App\Category не найден. Забыл указать новое пространство имен, когда перемещал модели из директории app в директорию app/Models. Так что надо исправить файлы в директории database/seeds и в директории database/factories.
use Illuminate\Database\Seeder; class CategoryTableSeeder extends Seeder { public function run() { // создать 10 категорий factory(App\Models\Category::class, 10)->create(); } }
use Illuminate\Database\Seeder; class BrandTableSeeder extends Seeder { public function run() { // создать 10 брендов factory(App\Models\Brand::class, 10)->create(); } }
use Illuminate\Database\Seeder; class ProductTableSeeder extends Seeder { public function run() { // создать 30 товаров factory(App\Models\Product::class, 30)->create(); } }
use App\Models\Category; use Illuminate\Support\Str; use Faker\Generator as Faker; $factory->define(Category::class, function (Faker $faker) { /* ... */ });
use App\Models\Brand; use Faker\Generator as Faker; $factory->define(Brand::class, function (Faker $faker) { /* ... */ });
use App\Models\Product; use Faker\Generator as Faker; $factory->define(Product::class, function (Faker $faker) { /* ... */ });
use App\Models\User; use Faker\Generator as Faker; use Illuminate\Support\Str; $factory->define(User::class, function (Faker $faker) { /* ... */ });
Выкладываем изменения на GitHub:
$ git commit -a -m "seeder namespace error" $ git push
На сервере получаем изменения:
$ $ git pull
В третий раз запускаем миграцию:
$ php artisan migrate:fresh --seed Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.04 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.04 seconds) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (0.03 seconds) Migrating: 2020_09_28_130327_create_categories_table Migrated: 2020_09_28_130327_create_categories_table (0.04 seconds) Migrating: 2020_09_28_130335_create_brands_table Migrated: 2020_09_28_130335_create_brands_table (0.06 seconds) Migrating: 2020_09_28_130346_create_products_table Migrated: 2020_09_28_130346_create_products_table (0.13 seconds) Migrating: 2020_10_02_120033_create_baskets_table Migrated: 2020_10_02_120033_create_baskets_table (0.03 seconds) Migrating: 2020_10_02_120730_create_basket_product_table Migrated: 2020_10_02_120730_create_basket_product_table (0.13 seconds) Migrating: 2020_10_09_081701_alter_users_table Migrated: 2020_10_09_081701_alter_users_table (0.03 seconds) Migrating: 2020_10_10_105603_create_orders_table Migrated: 2020_10_10_105603_create_orders_table (0.08 seconds) Migrating: 2020_10_10_111729_create_order_items_table Migrated: 2020_10_10_111729_create_order_items_table (0.14 seconds) Migrating: 2020_10_25_090836_alter_orders_table Migrated: 2020_10_25_090836_alter_orders_table (0.04 seconds) Migrating: 2020_10_27_060337_create_pages_table Migrated: 2020_10_27_060337_create_pages_table (0.04 seconds) Migrating: 2020_11_01_064359_create_profiles_table Migrated: 2020_11_01_064359_create_profiles_table (0.07 seconds) Migrating: 2020_11_08_101100_alter_products_table Migrated: 2020_11_08_101100_alter_products_table (0.03 seconds) Seeding: CategoryTableSeeder Seeded: CategoryTableSeeder (0.06 seconds) Таблица категорий загружена данными! Seeding: BrandTableSeeder Seeded: BrandTableSeeder (0.01 seconds) Таблица брендов загружена данными! Seeding: ProductTableSeeder Seeded: ProductTableSeeder (0.05 seconds) Таблица товаров загружена данными! Database seeding completed successfully.
На этот раз все прошло успешно.
5. Создаем символические ссылки
Мы удалили директорию public_html внутри laravel-7-shop, которая на хостинге является корнем веб-сервера. Теперь вместо нее создадим символическую ссылку public_html (см. здесь), которая будет указывать на директорию public.
$ ln -s ~/laravel-7-shop/public ~/laravel-7-shop/public_html
Кроме того, создаем символическую ссылку public/storage на директорию storage/app/public:
$ php artisan storage:link
Исходные коды здесь, демо-сайт здесь.
Post Scriptum
Чтобы на сервере работала отправка почты, нужно создать почтовый ащик в панели управления хостинга, а потом отредактировать файл .env.
$ nano .env
MAIL_MAILER=smtp MAIL_HOST=smtp.timeweb.ru MAIL_PORT=465 MAIL_USERNAME=laravel-7-shop@tokmakov.msk.ru MAIL_PASSWORD=пароль-почтового-ящика MAIL_ENCRYPTION=ssl MAIL_FROM_ADDRESS=laravel-7-shop@tokmakov.msk.ru MAIL_FROM_NAME="${APP_NAME}"
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 22. Рефакторинг кода, работа над каталогом товаров и корзиной
- Магазин на Laravel 7, часть 21. Добавляем профили и используем их при оформлении заказа
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Класс • Поиск • Практика • Таблица • Форма • Шаблон сайта


