Tạo Laravel Package - MySql Search Fulltext với Laravel Scout

10/17/2021 Laravel

# Introduce:

Làm việc với Laravel thì việc sử dụng các package rất phổ biến. Đóng gói các thư viện thành package sẽ giúp cho việc phát triễn, maintain hay thay thế rất dễ dàng.

Các bước để tạo một package thì cũng không khó và trên document của Laravel (opens new window) cũng có hướng dẫn. Bài viết này sẽ hướng dẫn chi tiết hơn để tạo một Laravel package giúp search fulltext trong mysql (Một Engine search cho Laravel Scout).

Trước tiên cần tìm hiểu về Laravel Scout (opens new window). Mặc định, Laravel Scout có hổ trợ 2 search driver dùng service ngoài là Algolia (opens new window)MeiliSearch (opens new window). Về hiệu suất search rất nhanh nhưng cũng có nhược điểm như cần đăng kí service của nó, nếu data ngày càng lớn sẽ tốn phí sử dụng, dữ liệu trong của project sẽ phải phải share lên server của algolia hay meiloSearch.

Package: https://packagist.org/packages/nin/mysql-ft-search

Package này sẽ tạo một Laravel Scout search engine ở local và sử dụng fulltext index trong mysql.

# Requirement:

# Create package:

  1. Install laravel: Sử dụng luôn laravel version mới nhất hiện tại

    https://laravel.com/docs/8.x/installation

  2. Tạo cấu trúc package:

    Một package sẽ có dạng [Creator or Vendor]/[Package name].

    Code sẽ nằm trong folder src, unit test trong tests, configdatabase thì tùy vào package có thể không có.

    |- packages
    |		|- nin				        /* Creator or Vendor */
    |		|		|- config/			/* optional */
    |		|		|- database/ 		/* optional */
    |		|		|- src/ 		        
    |		|		|- tests/
    |		|		|- compose.json	
    
    1
    2
    3
    4
    5
    6
    7
  3. Init composer:

    Bạn cần tạo các thư mục chính, file composer.json sẽ được tạo khi init composer.

    Tại packages/nin:

    $ composer init
    
    1

    Sau khi nhập các thông tin file composer.json sẽ được tạo.

    {
        "name": "nin/mysql-ft-search",
        "type": "library",
        "license": "MIT",
        "authors": [
            {
                "name": "Nin",
                "email": "ninhnghia2@gmail.com"
            }
        ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  4. Đăng kí namespace:

    Tại file compose.json của laravel app đã install.

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/",
            "Nin\\MySqlFtSearch": "packages/nin/src"
        }
    },
    
    1
    2
    3
    4
    5
    6
    7
    8

    Run dump autoload.

    composer dump-autoload
    
    1
  5. Tạo file config và ServiceProvider:

    Tạo file config mysql-ft-search.php trong folder /config và file ServiceProvider.php extends class Illuminate\Support\ServiceProvider trong folder /src chứa 2 method boot() và register().

    Để có thể publish và overide lại file config tại main app, cần thực hiện publish và merge config trong ServiceProvider

    <?php
    
    namespace Nin\MySqlFtSearch;
    
    use Illuminate\Support\ServiceProvider as BaseServiceProvider;
    
    class ServiceProvider extends BaseServiceProvider
    {
        /**
         * Bootstrap any package services.
         *
         * @return void
         */
        public function boot()
        {
            $this->publishes([
                __DIR__ . '/../config/mysql-ft-search.php' => config_path('mysql-ft-search.php'),
            ]);
        }
    
        /**
         * Register any application services.
         *
         * @return void
         */
        public function register()
        {
            $this->mergeConfigFrom(
                __DIR__ . '/../config/mysql-ft-search.php', 'mysql-ft-search'
            );
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    Sau khi install package, file config sẽ được publish ra ngoài main app bạn chỉ cần run lệnh

    $ php artisan vendor:publish --provider="Nin\MySqlFtSearch\ServiceProvider"
    
    1

    Trong giai đoạn phát triển để Service Container có thể load qua ServiceProvider này bạn cần khai báo vào file config/app.php

    'providers' => [
        ...
        
        Nin\MySqlFtSearch\ServiceProvider::class
    ]
    
    1
    2
    3
    4
    5

    Còn khi sử dụng, với Laravel version > 5.5 thì không cần khai báo vào providers như trên nữa mà bạn cần khai báo trong composer.json

    "extra": {
        "laravel": {
            "providers": [
                "Nin\\MySqlFtSearch\\ServiceProvider"
            ],
            "aliases": {
                "FtSchema": "Nin\\MySqlFtSearch\\Facade"
            }
        }
    },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    Bao gồm các service provider và facade. Trong package này có tạo 1 facade sẽ đề cập chi tiết ở phần sau.

# Schema:

Class Illuminate\Support\Facades\Schema của Laravel không có hổ trợ tạo column fulltext index. Để tạo được column fulltext có thể dùng DB::statement trong migration nhưng để code đẹp hơn và dễ maintain sau này mình tạo 1 class kế thừa lại Schema có thêm xử lí tại fulltext index.

DB::statement('ALTER TABLE posts ADD FULLTEXT fulltext_index (title)');
1
  • Blueprint:

    Đây là class generate schema, xem chi tiết Blueprint (opens new window). Mình tạo class Nin\MySqlFtSearch\MySqlFtBlueprint kế thừa Illuminate\Database\Schema\Blueprint và thêm method fulltext để tạo fulltext index.

    public function fulltext($columns, $name = null, $algorithm = null)
    {
        return $this->indexCommand('fulltext', $columns, $name, $algorithm);
    }
    
    1
    2
    3
    4
  • Grammar:

    Đây là class compile ra các command tương ứng với các db driver, xem chi tiết MySqlGrammar (opens new window). Tương tự, tạo class Nin\MySqlFtSearch\MySqlFtGrammar kế thừa Illuminate\Database\Schema\Grammars\MySqlGrammar và thêm method compileFullText

    public function compileFullText(Blueprint $blueprint, Fluent $command)
    {
        return $this->compileKey($blueprint, $command, 'FULLTEXT');
    }
    
    1
    2
    3
    4
  • Schema:

    Tạo class builder schema Nin\MySqlFtSearch\Schema\FtSchemaBuilder và set các factory trên vào schema.

    use Nin\MySqlFtSearch\MySqlFtBlueprint as Blueprint;
    use Nin\MySqlFtSearch\MySqlFtGrammar;
    
    public function getSchema()
    {
        $connection = $this->getDbConnection();
        $schema = $connection->getSchemaBuilder();
    
        $schemaFtEnabled = $this->app['config']->get('mysql-ft-search.schema_ft_enabled', true);
        if ($schemaFtEnabled) {
            $this->checkDriver($connection->getDriverName());
    
            // Set Schema Grammar.
            // Add method to compile a fulltext index command.
            $connection->setSchemaGrammar(new MySqlFtGrammar());
    
            $schema = $connection->getSchemaBuilder();
    
            // Set the Schema Blueprint resolver callback.
            // Add method to create fulltext index.
            $schema->blueprintResolver(function ($table, $callback) {
                return new Blueprint($table, $callback);
            });
        }
    
        return $schema;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • Binding:

    Tạo Facade FTSchema để dễ dàng sử dụng Schema trên. Facade của laravel sẽ get dependence trong Container nên ta binding Schema trong ServiceProvider.

    <?php
    
    namespace Nin\MySqlFtSearch;
    
    use Illuminate\Support\ServiceProvider as BaseServiceProvider;
    use Nin\MySqlFtSearch\Schema\FtSchema;
    use Nin\MySqlFtSearch\Schema\FtSchemaBuilder;
    use Illuminate\Foundation\Application;
    
    class ServiceProvider extends BaseServiceProvider
    {
    
        /**
         * Register any application services.
         *
         * @return void
         */
        public function register()
        {
            ...
    
            $this->app->singleton(FtSchema::class, function (Application $app) {
                $ftSchemaBuilder = new FtSchemaBuilder($app);
                return $ftSchemaBuilder->getSchema();
            });
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    Binding singleton schema builder cho interface Nin\MySqlFtSearch\Schema\FtSchema

    Tạo class Nin\MySqlFtSearch\Facade với facade accessor là FtSchema

    protected static function getFacadeAccessor()
    {
        return FtSchema::class;
    }
    
    1
    2
    3
    4

    Để sử dụng Facade này ở main app chỉ cần khai báo tại composer.json như ở trên.

  • Schema Usage:

    Sử dụng Schema thì tương tự như class Illuminate\Support\Facades\Schema trong migration

    use Nin\MySqlFtSearch\Facade as FtSchema;
    
    public function up()
    {
        FtSchema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description');
    
            $table->fulltext(['title', 'description']);
        });
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

Tạo Engine kế thừa Laravel\Scout\Engines\Engine chứa các abstract method

use Laravel\Scout\Builder;

abstract public function update($models);
abstract public function delete($models);
abstract public function search(Builder $builder);
abstract public function paginate(Builder $builder, $perPage, $page);
abstract public function mapIds($results);
abstract public function map(Builder $builder, $results, $model);
abstract public function getTotalCount($results);
abstract public function flush($model);
1
2
3
4
5
6
7
8
9
10

Xem chi tiết tại Nin\MySqlFtSearch\MySqlSearchEngineNin\MySqlFtSearch\SearchBuilder

Để search fulltext index trong mysql dùng function MATCH

$builder->model->query()
->whereRaw("MATCH ({$columns}) AGAINST (? IN BOOLEAN MODE)", $this->fullTextWildcards($q))
1
2
  • Extending binding:

    Extending binding Engine cho Laravel\Scout\EngineManager

    <?php
    
    namespace Nin\MySqlFtSearch;
    
    use Illuminate\Support\ServiceProvider as BaseServiceProvider;
    use Laravel\Scout\EngineManager;
    
    class ServiceProvider extends BaseServiceProvider
    {
        /**
         * Bootstrap any package services.
         *
         * @return void
         */
        public function boot()
        {
            ...
    
            $scoutDriverName = $this->app['config']->get('mysql-ft-search.scout_driver_name', 'mysql');
            resolve(EngineManager::class)->extend($scoutDriverName, function (Application $app) {
                return new MySqlSearchEngine($app);
            });
        }
     
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  • Usage:

    1. Set ENV:

      SCOUT_DRIVER=mysql
      
      1
    2. Model:

      Khai báo property $searchable

      public $searchable = [
          'title',
          'description'
      ];
      
      1
      2
      3
      4

      Sử dụng search

       Post::search('foo')->get();
      
      1

# Submit package:

Submit package lên https://packagist.org/ (opens new window)

  • Tạo 1 git repository
  • Tạo account ở Packagist, tại Submit package nhập Url của git repository là xong. Packagist sẽ tự động get branch của git repo, để tạo version thì dùng git tag. Khi thêm brach hoặc tag Packagist cũng tự động cập nhật hoặc cập nhật thủ công ở phần my packages.