PHP
laravel
LaravelDay 16

Laravel Scout + TNTSearchによる小規模プロジェクトへの全文検索機能の追加

Laravel ScoutはEloquentモデルに対し、全文検索を提供するパッケージです。

laravel scout

AlgoliaやElasticsearchを利用するほどでもない小規模なLaravelプロジェクト向けのLaravel Scoutについて記します。

Laravel Scoutについて思うところ

とあるプロジェクトで一人で社内向けの小規模なシステム開発をしており、Laravel Scoutによる全文検索の機能追加を検討したのですが、正直面倒だなと思っていました。というのも、公式ではドライバにAlgoliaを推奨しているのですが、開発中のシステムは外部サービスと連携するほど大それたものでもないし、一人で開発しているということもあり、管理コストが煩わしく感じました。
大体同じ理由でElasticsearchも面倒です。どうにかindexファイルを内部で保持しつつ、Laravel Scoutを利用できないかと調べていたところ、TNTSearchと連携すると要件とマッチする実装ができることがわかりました。

本記事ではLaravel Scout + TNTSearchによるお手軽全文検索の追加について記します。

導入

環境

  • php >= 7.0.0
  • laravel/framework 5.5.*
  • laravel/scout ^3.0
  • teamtnt/laravel-scout-tntsearch-driver ^3.0

Scoutインストール

$ composer require laravel/scout

以下のコマンドを実行し、scoutのconfigファイルを作成します。

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

TNTSearchドライバをインストール

Laravel ScoutのドライバとしてTNTSearchを利用する場合は以下をインストールする必要があります。

$ composer require teamtnt/laravel-scout-tntsearch-driver

導入が完了したら、config/app.phpへプロバイダ登録をします。

config/app.php
'providers' => [
    // ...
    Laravel\Scout\ScoutServiceProvider::class,
    TeamTNT\Scout\TNTSearchScoutServiceProvider::class,
],

.envファイルにて、SCOUT_DRIVERを設定します。

SCOUT_DRIVER=tntsearch

次に、scout関連の設定ファイルを弄ります。indexファイルの出力先を設定します。config/scout.phpを以下のように設定します。

config/scout.php
'tntsearch' => [
    'storage' => storage_path(),
    'asYouType' => false,
    'fuzziness' => true,
    'fuzzy' => [
        'prefix_length'  => 2,
        'max_expansions' => 50,
        'distance'       => 2,
    ]
]

Eloquentモデルと関連付け

検索対象としたいEloquentモデルに対し、以下の設定をします。

  • Laravel\Scout\Searchableトレイトを追加
  • toSearchableArray()追加
    • 検索対象としたいモデルデータ設定
<?php

 namespace App\Models;

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

  class User extends Model
  {
+     use Searchable;

...

+     public function toSearchAbleArray()
+     {
+         return [
+             'id'        => $this->id,
+             'firstName' => $this->firstName,
+             'mail'      => $this->mail,
+             .....
+         ];
+     }

検索対象としてるモデルでリレーションを含めたい場合は以下のようにします。

public function book() {
    return $this->hasMany('App\Models\Book')
}

public function toSearchableArray() {
    return [
        'id'        => $this->id,
        'firstName' => $this->firstName,
        'email'     => $this->email,
        'book_name' => $this->book->name, // リレーション
    ];
}

indexファイル作成

バッチによるインデックス作成

全文検索用のインデックスファイルを作成します。

$ php artisan make:command IndexUsers

クエリによるインデックスの作成ができます。
TNTSearchによるindexerでの作成をするなり、Eloquentのクエリの結果を登録するなり、ニーズに合わせてください。

    public function handle()
    {

        $indexer = TNTSearch::createIndex('users.index');
        $indexer->query('SELECT id, firstname, lastname, email, phone, bio FROM users;');
        $indexer->run();

        // or
        App\Models\User::get(['firstname', 'email'])->searchable();
    }
}

作成したコマンドを実行すると、config/scout.phpstorageに設定したパスにバイナリ形式のインデックスファイルが生成されます。storage_path()に設定した場合はstorageディレクトリ内に生成されます。

動作確認

php artisan tinkerで動作確認ができます。

$ php artisan tinker
>>> App\Models\User::search('john')->get()

検索にヒットした結果のリレーション取得について

例えばscoutによる検索結果を取得したeloquentモデルのcollectionで、リレーションを追加で取得したいケースがあるかと思います。
例えばscoutでuserの一覧を取得し、その結果よりリレーションであるitemモデルも取得したい場合、下手に実装するとN+1クエリになる可能性があります。この場合はEagerロードを利用することでN+1を避けることができます。

function searchUsersWithItemData($query) {
    // scoutで全文検索し、paginate化して取得
    $users = App\Models\Users::search($query)->paginate(50);

    // usersのリレーションであるcompanyモデル追加
    $users->setCollection($users->load('item'));

    return $users;
}

まとめ

他のサービス・システムと連携することなくLaravel Scoutの機能を追加することができました。
データの規模によってはこれを利用するまでもないケースもあるかもしれませんが、現状はこれで要件を満たせているので満足しています。
小規模Laravelプロジェクトにおすすめです。検討事項に含めてみてはいかがでしょうか。