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

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

> [laravel scout](https://github.com/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を利用する場合は以下をインストールする必要があります。

- [teamtnt/laravel-scout-tntsearch-driver](https://github.com/teamtnt/laravel-scout-tntsearch-driver)

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

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

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

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

```.env
SCOUT_DRIVER=tntsearch
```

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

```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()`追加
  - 検索対象としたいモデルデータ設定


```diff
<?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,
+             .....
+         ];
+     }
```

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

```php

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のクエリの結果を登録するなり、ニーズに合わせてください。

```php
    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.php`の`storage`に設定したパスにバイナリ形式のインデックスファイルが生成されます。`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を避けることができます。

```php
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プロジェクトにおすすめです。検討事項に含めてみてはいかがでしょうか。