Полнотекстовый поиск на Laravel 5.3 с использованием PostgreSQL

Если в проекте используется СУБД PostgreSQL то организовать полнотестовый поиск на сайте довольно просто и нет необходимости устанваливать ресурсоемкие системы типа Sphinx. Для этого необходимо только установить нужные словари  и произвести некоторые настройки.

Настройка конфигурации для поиска

Сначала настроим нашу СУБД для работы потом перейдем к установке модулей и использованию функционала полнотекстового поиска непосредственно в приложении на Laravel. Первым делом надо установить словари  скачать их можно здесь или найти другие. Далее нужно поместить их в папку tsearch_data в ubuntu она находится /usr/share/postgresql/9.3/tsearch_data в CentOS /usr/pgsql-9.5/share/tsearch_data в зависимости от ОС и версии пути могут немного отличатся здесь я привожу пример на моей локальной машине и серевере. Обращаю внимание, что словари, а также кодировка базы данных должна быть UTF8, а то не будет работать индексация русского языка.

Теперь переходим непосредственно к установке словарей, все действия будем выполнять в интерактивном терминале PostgreSQL psql, подключаемся к базе используя свой логин и пароль  и выполняем два запроса:

CREATE TEXT SEARCH DICTIONARY russian_ispell (
    TEMPLATE = ispell,
    DictFile = russian,
    AffFile = russian,
    StopWords = russian
);

CREATE TEXT SEARCH DICTIONARY english_ispell (
    TEMPLATE = ispell,
    DictFile = english,
    AffFile = english,
    StopWords = english
);

Здесь мы создали два словаря russian_ispell и english_ispell. Параметры которые здесь использованы: 

  • TEMPLATE - шаблон для создания словаря 
  • DictFile - название файла с расширением .dict c папки tsearch_data
  • AffFile -  файл аффиксов (.affix), которые нужны для присоединению к корню и образованию новых слов;
  • StopWords - файл стоп-слов (расширение файла - .stop). Содержит слова которые будут исключены из поиска. Обычно в файл помещают часто употребляемые слова, которые есть в каждом из искомых текстов. Этот файл уже есть в каталоге "tsearch_data", его можно расиширить или заменить на свой.

Проверям словарь: 

SELECT * FROM ts_lexize('russian_ispell', 'поиск');
 ts_lexize 
-----------
 {поиск}
(1 row)

Теперь надо создать свою конфигурацию которую сможем использовать в дальнейшем: 

CREATE TEXT SEARCH CONFIGURATION ru_config ( COPY = russian );

Теперь надо настроить словари для конфигурации: 

ALTER TEXT SEARCH CONFIGURATION ru_config
	ALTER MAPPING
		FOR word, hword, hword_part
		WITH russian_ispell, russian_stem;


ALTER TEXT SEARCH CONFIGURATION ru_config
	ALTER MAPPING
		FOR asciiword, asciihword, hword_asciipart
		WITH english_ispell, english_stem;

Можно просмотреть данную конфигурацию выполнив в терминале \dF+ ru_config, получим таблицу лексем конфигурации и словари которые используются для нее, подробнее о лексемах можно почитать на странице с документацией. Вот результат: 

 Text search configuration "public.ru_config"
Parser: "pg_catalog.default"
      Token      |        Dictionaries         
-----------------+-----------------------------
 asciihword      | english_ispell,english_stem
 asciiword       | english_ispell,english_stem
 email           | simple
 file            | simple
 float           | simple
 host            | simple
 hword           | russian_ispell,russian_stem
 hword_asciipart | english_ispell,english_stem
 hword_numpart   | simple
 hword_part      | russian_ispell,russian_stem
 int             | simple
 numhword        | simple
 numword         | simple
 sfloat          | simple
 uint            | simple
 url             | simple
 url_path        | simple
 version         | simple
 word            | russian_ispell,russian_stem
:

И так мы создали два словаря, создали конфигурцаию, настроили ее под наши словари, теперь можно использовать ее. Установим ее по умолчанию: 

SET default_text_search_config = 'ru_config';

и чтобы в дальнейшем не задавать ее каждый раз для сессии и в коде на сайте зададим ее по умолчанию в файле postgresql.conf () (/etc/postgresql/9.3/main/postgresql.conf - ubuntu, /var/lib/pgsql/9.5/data/postgresql.conf)

default_text_search_config = 'ru_config'

проверим конфигурацию:

SELECT * FROM ts_debug('ru_config', 'Search. Поиск. 152');

если все правильно должны получить подобную таблицу:

   alias   |    description    | token  |       dictionaries       | dictionary | lexemes  
-----------+-------------------+--------+--------------------------+------------+----------
 asciiword | Word, all ASCII   | Search | {ispell_en,english_stem} | ispell_en  | {search}
 blank     | Space symbols     | .      | {}                       |            | 
 word      | Word, all letters | Поиск  | {ispell_ru,russian_stem} | ispell_ru  | {поиск}
 blank     | Space symbols     | .      | {}                       |            | 
 uint      | Unsigned integer  | 152    | {simple}                 | simple     | {152}
(5 rows)

С настройкой все. Переходим к нашему приложению.

Установка, настройка и использование полнотекстового поиска в приложении на Laravel 5.3

Прежде всего нужно установить два модуля laravel/scout и pmatseykanets/laravel-scout-postgres, для этого в консоле в папке нашего проекта выполняем:

composer require laravel/scout
composer require pmatseykanets/laravel-scout-postgres

Еще для корректной работы модуля важно чтобы была устновлена конфигурция по умолчанию в файле postgresql.conf, как мы делали выше.

Добавляем в файл config/app.php два сервис провайдера:

// config/app.php
'providers' => [
    ...
    Laravel\Scout\ScoutServiceProvider::class,
    ScoutEngines\Postgres\PostgresEngineServiceProvider::class,
],

Создаем файл конфига:

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

в него добавляем настройки для драйвера на PostgreSQL в этот файл:

// config/scout.php
...
'pgsql' => [
    'connection' => env('DB_CONNECTION', 'pgsql'),
    // You may want to update index documents directly in PostgreSQL (i.e. via triggers).
    // In this case you can set this value to false.
    'maintain_index' => true,
],
...

Теперь создаем файл миграции для добавления столбца searchable типа  tsvector. Я на некоторых проектах использую и MySQL то у меня файл миграции выглядит примерно так: 


use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddFullTextSearch extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        if(Config::get('database.default')=='pgsql') {
            DB::statement('ALTER TABLE content_translations ADD searchable tsvector NULL');
            DB::statement('CREATE INDEX content_translations_searchable_index ON content_translations USING GIN (searchable)');
        }
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        if(Config::get('database.default')=='pgsql') {
            Schema::table('content_translations',function (Blueprint $table){
                $table->dropColumn('searchable');
            });
        }
    }
}

т.е. данный столбец добавится только если мы используем PostgreSQL в проекте.

Далее переходим к моделе. Первое что нужн сделать - добавить трейт Laravel\Scout\Searchable; и переопределить некоторые методы. У меня есть таблица contents связанная с таблицей переводов content_translations, поиск мы будем осуществлять по второй с учетом текущей локали  так вот пример модели ContentTranslation:


namespace App;

use Laravel\Scout\Searchable;

class ContentTranslation extends Base
{
    use Searchable;
    public $timestamps = false;

    public function searchableOptions()
    {
        return [
            // You may wish to change the default name of the column
            // that holds parsed documents
            'column' => 'searchable',
            // You may want to store the index outside of the Model table
            // In that case let the engine know by setting this parameter to true.
            'external' => true,
            // If you don't want scout to maintain the index for you
            // You can turn it off either for a Model or globally
            'maintain_index' => true,
            // Ranking groups that will be assigned to fields
            // when document is being parsed.
            // Available groups: A, B, C and D.
            'rank' => [
                'fields' => [
                    'name' => 'A',
                    'description' => 'C',
                    'text' => 'D',
                    'tags' => 'B'
                ],
                // Ranking weights for searches.
                // [D-weight, C-weight, B-weight, A-weight].
                // Default [0.1, 0.2, 0.4, 1.0].
                'weights' => [0.1, 0.2, 0.4, 1.0],
                // Ranking function [ts_rank | ts_rank_cd]. Default ts_rank.
                'function' => 'ts_rank',
                // Normalization index. Default 0.
                'normalization' => 32,
            ],
        ];


    }

    public function toSearchableArray()
    {
        return [
            'name' => $this->name,
            'description' => $this->description,
            'text' => $this->text,
// тут поллучаю запись с таблицы контента в ней получаю все теги привязынне 
//к данной записи собираю их в строку через запятую и формирую поле tags для индекса
            'tags' => $this->content()
                            ->first()
                            ->tags
                            ->pluck('text')
                            ->implode(', ')
        ];
    }

    public function content()
    {
        return $this->belongsTo(Content::class);
    }
}

Теперь можем создать индексы для нашего сайта выполнив команду в терминале, если получилось что-то в этом роде тогда все сделали верно:

php artisan scout:import "App\ContentTranslation"

Imported [App\ContentTranslation] models up to ID: 24
All [App\ContentTranslation] records have been imported.

Вот пример использования в коде:

public function search($query)
{
    $res = ContentTranslation::search($query)->where('locale',LaravelLocalization::getCurrentLocale())->get();
    dd($res);
}

И так подведем итоги. Мы создали два словаря russian_ispell и english_ispell, настроили конфигурацию ru_config для их использования. Подключили нужные нам модули в приложение на Laravel, Настроили модель и увидели как использовать поиск. Более подробно о методах и настройках можно прочесть на официальном сайте документации https://laravel.com/docs/master/scout и на GitHub https://github.com/pmatseykanets/laravel-scout-postgres.