•   Новости. Советы. Уроки.
Блог / Советы и уроки / Делаем простой поиск при помощи Laravel Scout и Vue.js

Делаем простой поиск при помощи Laravel Scout и Vue.js

Rachid Laasri
Виталий Николенко
24.09.2016

Сегодня посмотрим, как сделать простой поиск при помощи Laravel Scout и Vue.js. На случай, если вы не знаете, что такое Laravel Scout, вот отрывок из документации Laravel:

Laravel Scout представляет собой простое, основанное на драйверах, решение по внедрению полнотекстового поиска по моделям Eloquent. При помощи “наблюдателей” за моделями, Scout автоматически поддерживает актуальность поискового индекса при изменении записей Eloquent.

Это официальный пакет, правда не включенный в базовую поставку, но его довольно просто подключить при помощи Composer. Он поставляется со встроенным драйвером Algolia, но драйвер можно легко сменить, согласно документации:

На текущий момент Scout поставляется с драйвером Algolia; однако написать собственный драйвер довольно просто, и можно свободно расширять Scout собственными реализациями поиска.

Прежде чем погрузимся в код, посмотрим что в итоге должно получиться

Установка Laravel:

От переводчика: честно не знаю, зачем в каждой статье автор считает необходимым начать с этого :)

Выполним следующую команду в терминале:

composer create-project --prefer-dist laravel/laravel search

После выполнения команды, удостоверьтесь, что директории storage и bootstrap/cache открыты на запись серверу.

Вот что вы должны увидеть в браузере после успешной установки:

Конфигурация базы данных

Для этого примера я буду использовать SQLite, можете прописать у себя MySQL или любую другую БД.

DB_CONNECTION=sqlite

Если не указать имя базы данных, по умолчанию будет использоваться database/database.sqlite.

Модели и миграции:

Для нашего небольшого приложения будем использовать только одну модель Product и таблицу products.

Давайте их создадим:

php artisan make:model Product -m

Флаг -m дает указание команде artisan make:model создать миграцию.

Опишем следующие поля:

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('image');
            $table->integer('price');
            $table->text('description');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

Сохраните файл и запустите миграцию php artisan migrate

Тестовые данные:

При помощи фабрик моделей (Model Factories) мы заполним таблицу данными для тестов. Создадим файл database/factories/ModelFactory.php следующего содержания:

$factory->define(App\Product::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->sentence(),
        'image' => 'http://loremflickr.com/400/300?random='.rand(1, 100),
        'price' => $faker->numberBetween(3, 100),
        'description' => $faker->paragraph(2)
    ];
});

Наша фабрика готова, давайте заполним таблицу данными. Запустите команду php artisan tinker и затем factory(App\Product::class, 100)→create();. Записей можно создать сколько угодно, 100 меня устраивает.

От переводчика: Можно добавить вызов фабрики в миграцию

Маршруты и контроллеры:

Нам понадобятся следующие два маршрута:

  • GET : / - главная страница приложения. В файле routes/web.php:
Route::get('/', function () {
    return view('home');
});

Можно использовать и контроллер, но для такой мелкой операции считаю это излишним.

  • GET : api/search - этот маршрут будет обрабатывать результаты поиска.

routes/api.php

Route::get('/search', [
    'as' => 'api.search',
    'uses' => 'Api\SearchController@search'
]);

Для создания класса SearchController, просто запустите: php artisan make:controller Api\SearchController. Если вы не знали, то при указании неймспейса Laravel создаст директорию автоматически и поместит созданный контроллер в нее.

class SearchController extends Controller
{
    public function search(Request $request)
    {
        // we will be back to this soon!
    }
}

Установка Laravel Scout:

Установка и конфигурация Laravel Scout тривиальна. Пакет можно установить при помощи команды composer require laravel/scout. Когда процесс завершится, подключите сервис-провайдер ScoutServiceProvider в массив провайдеров в config/app.php:

Laravel\Scout\ScoutServiceProvider::class,

Затем необходимо опубликовать конфигурационные файлы пакета при помощи команды vendor:publish. Эта команда опубликует файл scout.php внутри директории config :

php artisan vendor:publish –provider="Laravel\Scout\ScoutServiceProvider"

И в качестве заключительного шага добавьте трейт Laravel\Scout\Searchable в модель Product для возможности поиска по ней:

<?php

namespace App;

use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use Searchable;
}

Экспорт данных в Algolia

Теперь, когда Laravel Scout установлен, самое время установить драйвер Algolia

composer require algolia/algoliasearch-client-php

Далее необходимо экспортировать в Algolia все наши тестовые данные, которые мы сгенерили выше.

На сайте Algolia необходимо создать эккаунт ( бесплатный).

После регистрации на странице https://www.algolia.com/api-keys скопируйте ключи Application ID и Admin API Key и добавьте их в файл .env таким образом:

ALGOLIA_APP_ID=J4YK3E00YT
ALGOLIA_SECRET=9088241f69034ee7db231e67bc2cb0b4

Запустите команду

php artisan scout:import "App\Product"

Если вы все сделали верно, увидите примерно такое сообщение, которое означает, что все данные из таблицы products скопированы на сервер Algolia.

Удостовериться в этом можно, зайдя на страницу Indices своего эккаунта.

Пишем метод поиска:

Вернемся в наш контроллер app\Http\Controllers\Api\SearchController.php.

    /**
     * Поиск по таблице products .
     *
     * @param  Request $request
     * @return mixed
     */
    public function search(Request $request)
    {
        // Определим сообщение, которое будет отображаться, если ничего не найдено 
        // или поисковая строка пуста
        $error = ['error' => 'No results found, please try with different keywords.'];

        // Удостоверимся, что поисковая строка есть
        if($request->has('q')) {

            // Используем синтаксис Laravel Scout для поиска по таблице products.
            $posts = Product::search($request->get('q'))->get();

            // Если есть результат есть, вернем его, если нет  - вернем сообщение об ошибке.
            return $posts->count() ? $posts : $error;

        }

        // Вернем сообщение об ошибке, если нет поискового запроса
        return $error;
    }

Не забудьте импортировать модель Product при помощи use App\Product; Теперь при переходе по адресу http://your.local.domain/api/search?q=code вы должны увидеть JSON представление данных.

Клиентская часть:

В рамках этого урока мы особо не будем заострять внимание на дизайне. Вот шаблон, который мы будем использовать. Сохраните его в resources/home.blade.php.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link rel="stylesheet" href="/">
        <title>Search with Laravel Scout and Vue.js!</title>
    </head>
    <body>
        <div class="container">
            <div class="well well-sm">
                <div class="form-group">
                    <div class="input-group input-group-md">
                        <div class="icon-addon addon-md">
                            <input type="text" placeholder="What are you looking for?" class="form-control">
                        </div>
                        <span class="input-group-btn">
                            <button class="btn btn-default" type="button">Search!</button>
                        </span>
                    </div>
                </div>
            </div>
            <div id="products" class="row list-group">
            </div>
        </div>
    </body>
</html>

Как видите здесь я набросал базовую структуру и подключил Twitter Bootstrap через CDN, в реальном приложении так делать не стоит.

Подключение Vue.js и vue-resource:

Добавьте эти строки перед закрывающим тегом body :

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/1.0.1/vue-resource.min.js"></script>
<script src="/js/app.js"></script>

И опять же, в реальном приложении лучше использовать менеджер зависимостей а не просто подключать библиотеки через CDN, но для нашего примера подойдет и этот способ.

Затем создайте app.js файл внутри директории public/js (удалите, если есть уже такая) с таким содержанием:

new Vue({
    el: 'body',
});

Это означает, что мы создаем инстанс Vue и привязываем ее к элементу body.

Vue.js данные и их привязка к шаблону:

Для хранения пользовательского ввода, отображения результатов и ошибок заведем следубщие переменные:

data: {
    products: [],
    loading: false,
    error: false,
    query: ''
},

Теперь нам надо немного изменить наш HTML, вот что мы сделаем:

1) Добавим атрибут `v-model` к строке поиска для того что бы связать ее с атрибутом в объекте данных
<input type="text" placeholder="What are you looking for?" class="form-control" v-model="query">

2) Добавим отображение надписи Searching... на кнопке пока происходит поисковый запрос к серверу. Если запрос не исполняется, то надпись на кнопке будет Search!

<button class="btn btn-default" type="button" v-if="!loading">Search!</button>
<button class="btn btn-default" type="button" disabled="disabled" v-if="loading">Searching...</button>

3) Для отображения ошибки добавим в конце следующий контейнер

<div class="alert alert-danger" role="alert" v-if="error">
    <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
    @{{ error }}
</div>

Для отображения переменных Vue.js использует синтаксис {{ }}, но, т.к. Laravel использует такой же синтаксис в шаблонизаторе используем @ в начале, чтобы предотвратить рендеринг на сервере.

Получение и отображение данных:

Нам остался последний и самый важный шаг – получить данные поиска. Начнем с добавления слушателя события нажатия кнопки поиска, делается это при помощи атрибута @click:

<button class="btn btn-default" type="button" @click="search()" v-if="!loading">Search!</button>

Добавим метод search в наш объект:

methods: {
    search: function() {
        // Очистим сообщение об ошибке.
        this.error = '';
        // Опустошим набор данных.
        this.products = [];
        // Установим признак загрузки данных в true,  
        // для отображения процесса поиска "Searching...".
        this.loading = true;

        // делаем get запрос к нашему API и передаем в него поисковый запрос.
        this.$http.get('/api/search?q=' + this.query).then((response) => {
            // Елси ошибки нет, заполняем массив products, в случае ошибки заполняем ее
            response.body.error ? this.error = response.body.error : this.products = response.body;
            // Запрос завершен. Меняем статус загрузки
            this.loading = false;
            // Очищаем поисковое слово.
            this.query = '';
        });
    }
}

И наконец добавляем HTML для вывода наших товаров.

<div class="item col-xs-4 col-lg-4" v-for="product in products">
    <div class="thumbnail">
        <img class="group list-group-image" :src="product.image" alt="@{{ product.title }}" />
        <div class="caption">
            <h4 class="group inner list-group-item-heading">@{{ product.title }}</h4>
            <p class="group inner list-group-item-text">@{{ product.description }}</p>
            <div class="row">
                <div class="col-xs-12 col-md-6">
                    <p class="lead">$@{{ product.price }}</p>
                </div>
                <div class="col-xs-12 col-md-6">
                    <a class="btn btn-success" href="#">Add to cart</a>
                </div>
            </div>
        </div>
    </div>
</div>

Здесь мы сделали вывод результатов в цикле при помощи директивы v-for и отобразили данные, как и раньше при помощи синтаксиса {{}}.

У вас должно получиться примерно следущее:

Заключение:

Код к статье можно взять здесь https://github.com/RachidLaasri/laravel-scout-and-vuejs Автор будет рад ответить на ваши вопросы и предложения в комментариях к оригинальной статье