イントロダクション
Laravel Scout(Scout、斥候)は、Eloquentモデルへ、シンプルなドライバベースのフルテキストサーチを提供します。モデルオブサーバを使い、Scoutは検索インデックスを自動的にEloquentレコードと同期します。
現在、ScoutはAlgoliaとMeiliSearchドライバを用意しています。さらに、Scoutは、ローカル開発用に設計した"collection"ドライバも用意しており、これは外部依存やサードパーティのサービスを必要としません。さらに、カスタムドライバの記述も簡単で、独自な検索実装を行い自由にScoutを拡張できます。
インストール
最初に、Composerパッケージマネージャを使い、Scoutをインストールします。
composer require laravel/scout
Scoutをインストールした後、vendor:publish
Artisanコマンドを実行してScout設定ファイルをリソース公開する必要があります。このコマンドは、scout.php
設定ファイルをアプリケーションのconfig
ディレクトリへリソース公開します。
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
最後に、検索可能にしたいモデルにLaravel\Scout\Searchable
トレイトを追加します。このトレイトは、モデルを検索ドライバと自動的に同期させるモデルオブザーバを登録します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
}
ドライバの事前要件
Algolia
Algoliaドライバを使用する場合、Algolia
id
とsecret
接続情報をconfig/scout.php
設定ファイルで設定する必要があります。接続情報を設定し終えたら、Algolia
PHP
SDKをComposerパッケージマネージャで、インストールする必要があります。
composer require algolia/algoliasearch-client-php
MeiliSearch
MeiliSearchは、非常に高速なオープンソースの検索エンジンです。ローカルマシンにMeiliSearchをインストールする方法がわからない場合は、Laravelの公式サポートのDocker開発環境であるLaravel Sailを利用できます。
Meilisearchドライバを使用する場合は、Composerパッケージマネージャを使用して、MeiliSearch PHP SDKをインストールする必要があります。
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle
次に、アプリケーションの.env
ファイル内のSCOUT_DRIVER
環境変数とMeiliSearchhost
とkey
認証情報を設定します。
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=masterKey
MeiliSearchの詳細については、MeiliSearchのドキュメントを参照してください。
さらに、MeiliSearchのバイナリ互換のドキュメントを見て、自分が使っているMeiliSearchのバイナリバージョンと互換性のあるバージョンのmeilisearch/meilisearch-php
をインストールしてください。
Note: MeiliSearchを利用しているアプリケーションのScoutをアップグレードする際には、常にMeiliSearchサービス自体に追加の破壊的な変更がないか確認する必要があります。
キュー投入
厳密にはScoutを使用する必要はありませんが、ライブラリを使用する前に、キュードライバの設定を強く考慮する必要があります。キューワーカを実行すると、Scoutはモデル情報を検索インデックスに同期するすべての操作をキューに入れることができ、アプリケーションのWebインターフェイスのレスポンス時間が大幅に短縮されます。
キュードライバを設定したら、config/scout.php
設定ファイルのqueue
オプションの値をtrue
に設定します。
'queue' => true,
設定
モデルインデックスの設定
各Eloquentモデルは、検索可能レコードすべてを含む、指定された検索「インデックス」と同期されます。言い換えれば、各インデックスはMySQLテーブルのようなものであると、考えられます。デフォルトで、各モデルはそのモデルの典型的な「テーブル」名に一致するインデックスへ保存されます。通常、モデルの複数形ですが、モデルのsearchableAs
メソッドをオーバーライドすることで、このモデルのインデックスを自由にカスタマイズ可能です。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
/**
* モデルに関連付けられているインデックスの名前を取得
*
* @return string
*/
public function searchableAs()
{
return 'posts_index';
}
}
検索可能データの設定
デフォルトでは、指定されたモデルのtoArray
形態全体が、検索インデックスへ保存されます。検索インデックスと同期するデータをカスタマイズしたい場合は、そのモデルのtoSearchableArray
メソッドをオーバーライドできます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
/**
* モデルのインデックス可能なデータ配列の取得
*
* @return array
*/
public function toSearchableArray()
{
$array = $this->toArray();
// データ配列をカスタマイズ
return $array;
}
}
モデルIDの設定
デフォルトでは、Scoutはモデルの主キーを、検索インデックスに保存されているモデルの一意のID/キーとして使用します。この動作をカスタマイズする必要がある場合は、モデルのgetScoutKey
メソッドとgetScoutKeyName
メソッドをオーバーライドできます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class User extends Model
{
use Searchable;
/**
* モデルのインデックスに使用する値の取得
*
* @return mixed
*/
public function getScoutKey()
{
return $this->email;
}
/**
* モデルのインデックスに使用するキー名の取得
*
* @return mixed
*/
public function getScoutKeyName()
{
return 'email';
}
}
ユーザーの識別
Scoutを使用すると、Algoliaを使用するときにユーザーを自動識別することもできます。認証済みユーザーを検索操作に関連付けると、Algoliaのダッシュボード内で検索分析を表示するときに役立つ場合があります。アプリケーションの.env
ファイルでSCOUT_IDENTIFY
環境変数をtrue
として定義することにより、ユーザー識別を有効にできます。
SCOUT_IDENTIFY=true
この機能を有効にすると、リクエストのIPアドレスと認証済みユーザーのプライマリ識別子もAlgoliaに渡されるため、これらのデータはそのユーザーが行った検索リクエストへ関連付けられます。
ローカル開発
ローカル開発時には、AlgoliaやMeiliSearchの検索エンジンを自由に使用することができますが、「コレクション(collection)」エンジンでスタートした方が便利な場合もあります。コレクション・エンジンは、既存データベースからの結果に対して、「where」節とコレクション・フィルタリングを用いて、クエリに該当する検索結果を決定します。このエンジンを使用する場合、Searchableモデルをインデックス化する必要はなく、シンプルにローカル・データベースから検索します。
コレクションエンジンを使用するには,環境変数SCOUT_DRIVER
の値をcollection
に設定するか,アプリケーションのscout
設定ファイルでcollection
ドライバを直接指定します。
SCOUT_DRIVER=collection
コレクションドライバを使用ドライバに指定したら、モデルに対して検索クエリの実行を開始できます。コレクションエンジンを使用する場合、AlgoliaやMeiliSearchのインデックスのシードに必要なインデックス作成などの検索エンジンのインデックス作成は不要です。
インデックス
バッチ取り込み
Scoutを既存のプロジェクトにインストールする場合は、インデックスへインポートする必要のあるデータベースレコードがすでに存在している可能性があります。Scoutは、既存のすべてのレコードを検索インデックスにインポートするために使用できるscout:import
Artisanコマンドを提供しています。
php artisan scout:import "App\Models\Post"
flush
コマンドは、検索インデックスからモデルの全レコードを削除するために使用します。
php artisan scout:flush "App\Models\Post"
インポートクエリの変更
バッチインポートで全モデルを取得するために使用されるクエリを変更する場合は、モデルにmakeAllSearchableUsing
メソッドを定義してください。これはモデルをインポートする前に、必要になる可能性のあるイエガーリレーションの読み込みを追加するのに最適な場所です。
/**
* 全モデルを検索可能にするときの、モデル取得に使用するクエリを変更
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function makeAllSearchableUsing($query)
{
return $query->with('author');
}
レコード追加
モデルにLaravel\Scout\Searchable
トレイトを追加したら、モデルインスタンスを保存
または作成
するだけで、検索インデックスに自動的に追加されます。キューを使用するようにScoutを設定した場合、この操作はキューワーカによってバックグラウンドで実行されます。
use App\Models\Order;
$order = new Order;
// ...
$order->save();
クエリによるレコード追加
Eloquentクエリを介してモデルのコレクションを検索インデックスに追加する場合は、searchable
メソッドをEloquentクエリにチェーンできます。searchable
メソッドはクエリの結果をチャンクし、レコードを検索インデックスに追加します。繰り返しますが、キューを使用するようにScoutを設定した場合、すべてのチャンクはキューワーカによってバックグラウンドでインポートされます。
use App\Models\Order;
Order::where('price', '>', 100)->searchable();
Eloquentリレーションインスタンスで
searchable
メソッドを呼び出すこともできます。
$user->orders()->searchable();
または、メモリ内にEloquentモデルのコレクションが既にある場合は、コレクションインスタンスでsearchable
メソッドを呼び出して、モデルインスタンスを対応するインデックスに追加できます。
$orders->searchable();
Tip!!
searchable
メソッドは、「アップサート(upsert)」操作と考えるられます。つまり、モデルレコードがすでにインデックスに含まれている場合は、更新され、検索インデックスに存在しない場合は追加されます。
レコード更新
検索可能モデルを更新するには、モデルインスタンスのプロパティを更新し、save
でモデルをデータベースへ保存します。Scoutは自動的に変更を検索インデックスへ保存します。
use App\Models\Order;
$order = Order::find(1);
// 注文を更新…
$order->save();
Eloquentクエリインスタンスでsearchable
メソッドを呼び出して、モデルのコレクションを更新することもできます。モデルが検索インデックスに存在しない場合は作成されます。
Order::where('price', '>', 100)->searchable();
リレーションシップ内のすべてのモデルの検索インデックスレコードを更新する場合は、リレーションシップインスタンスでsearchable
を呼び出すことができます。
$user->orders()->searchable();
または、メモリ内にEloquentモデルのコレクションが既にある場合は、コレクションインスタンスでsearchable
メソッドを呼び出して、対応するインデックスのモデルインスタンスを更新できます。
$orders->searchable();
レコード削除
インデックスからレコードを削除するには、データベースからモデルをdelete
するだけです。これは、ソフト削除モデルを使用している場合でも実行できます。
use App\Models\Order;
$order = Order::find(1);
$order->delete();
レコードを削除する前にモデルを取得したくない場合は、Eloquentクエリインスタンスでunsearchable
メソッドを使用できます。
Order::where('price', '>', 100)->unsearchable();
リレーション内のすべてのモデルの検索インデックスレコードを削除する場合は、リレーションインスタンスでunsearchable
を呼び出してください。
$user->orders()->unsearchable();
または、メモリ内にEloquentモデルのコレクションが既にある場合は、コレクションインスタンスでunsearchable
メソッドを呼び出して、対応するインデックスからモデルインスタンスを削除できます。
$orders->unsearchable();
インデックスの一時停止
モデルデータを検索インデックスに同期せずに、モデルに対してEloquent操作のバッチを実行する必要がある場合があります。これは、withoutSyncingToSearch
メソッドを使用して行うことができます。このメソッドは、すぐに実行される単一のクロージャを引数に取ります。クロージャ内で発行するモデル操作は、モデルのインデックスに同期されません。
use App\Models\Order;
Order::withoutSyncingToSearch(function () {
// モデルアクションを実行
});
条件付き検索可能モデルインスタンス
特定の条件下でのみ、モデルを検索可能にする必要がある場合も起きるでしょう。たとえば、App\Models\Post
モデルが、"draft"か"published"の2つのうち、どちらか1つの状態を取ると想像してください。「公開済み:published」のポストのみ検索可能にする必要があります。これを実現するには、モデルにshouldBeSearchable
メソッドを定義してください。
/**
* モデルを検索可能にする判定
*
* @return bool
*/
public function shouldBeSearchable()
{
return $this->isPublished();
}
shouldBeSearchable
メソッドは、save
およびcreate
メソッド、クエリ、またはリレーションを通してモデルを操作する場合にのみ適用されます。searchable
メソッドを使用してモデルまたはコレクションを直接検索可能にすると、shouldBeSearchable
メソッドの結果が上書きされます。
検索
search
メソッドにより、モデルの検索を開始しましょう。search
メソッドはモデルを検索するために使用する文字列だけを引数に指定します。get
メソッドを検索クエリにチェーンし、指定した検索クエリに一致するEloquentモデルを取得できます。
use App\Models\Order;
$orders = Order::search('Star Trek')->get();
Scoutの検索ではEloquentモデルのコレクションが返されるため、ルートやコントローラから直接結果を返せば、自動的にJSONへ変換されます。
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/search', function (Request $request) {
return Order::search($request->search)->get();
});
Eloquentモデルへ変換する前に素の検索結果を取得したい場合は、raw
メソッドを使用できます。
$orders = Order::search('Star Trek')->raw();
カスタムインデックス
検索クエリは通常、モデルのsearchableAs
メソッドで指定するインデックスに対して実行されます。ただし、within
メソッドを使用して、代わりに検索する必要があるカスタムインデックスを指定できます。
$orders = Order::search('Star Trek')
->within('tv_shows_popularity_desc')
->get();
Where節
Scoutを使用すると、検索クエリに単純な「where」節を追加できます。現在、これらの節は基本的な数値の同等性チェックのみをサポートしており、主に所有者IDによる検索クエリのスコープに役立ちます。
use App\Models\Order;
$orders = Order::search('Star Trek')->where('user_id', 1)->get();
whereIn
メソッドを使用すると、指定された値の集合に対して結果を制約できます。
$orders = Order::search('Star Trek')->whereIn(
'status', ['paid', 'open']
)->get();
検索インデックスはリレーショナルデータベースではないため、より高度な"where"節は現在サポートしていません。
ペジネーション
モデルのコレクションを取得することに加えて、paginate
メソッドを使用して検索結果をページ分割することができます。このメソッドは、従来のEloquentクエリをペジネーションする場合と同じように、Illuminate\Pagination\LengthAwarePaginator
インスタンスを返します。
use App\Models\Order;
$orders = Order::search('Star Trek')->paginate();
paginate
メソッドの第1引数として、各ページごとに取得したいモデル数を指定します。
$orders = Order::search('Star Trek')->paginate(15);
結果が取得できたら、通常のEloquentクエリのペジネーションと同様に、結果を表示し、Bladeを使用してページリンクをレンダーできます。
<div class="container">
@foreach ($orders as $order)
{{ $order->price }}
@endforeach
</div>
{{ $orders->links() }}
もちろん、ペジネーションの結果をJSONとして取得したい場合は、ルートまたはコントローラから直接ペジネータインスタンスを返すことができます。
use App\Models\Order;
use Illuminate\Http\Request;
Route::get('/orders', function (Request $request) {
return Order::search($request->input('query'))->paginate(15);
});
ソフトデリート
インデックス付きのモデルがソフトデリートされ、ソフトデリート済みのモデルをサーチする必要がある場合、config/scout.php
設定ファイルのsoft_delete
オプションをtrue
に設定してください。
'soft_delete' => true,
この設定オプションをtrue
にすると、Scoutは検索インデックスからソフトデリートされたモデルを削除しません。代わりに、インデックスされたレコードへ、隠し__soft_deleted
属性をセットします。これにより、検索時にソフトデリート済みレコードを取得するために、withTrashed
やonlyTrashed
メソッドがつかえます。
use App\Models\Order;
// 結果の取得時に、削除済みレコードも含める
$orders = Order::search('Star Trek')->withTrashed()->get();
// 結果の取得時に、削除済みレコードのみを対象とする
$orders = Order::search('Star Trek')->onlyTrashed()->get();
Tip!! ソフトデリートされたモデルが、
forceDelete
により完全に削除されると、Scoutは自動的に検索インデックスから削除します。
エンジンの検索のカスタマイズ
エンジンの検索動作の高度なカスタマイズを実行する必要がある場合は、
search
メソッドの2番目の引数にクロージャを渡せます。たとえば、このコールバックを使用して、検索クエリがAlgoliaに渡される前に、地理的位置データを検索オプションに追加できます。
use Algolia\AlgoliaSearch\SearchIndex;
use App\Models\Order;
Order::search(
'Star Trek',
function (SearchIndex $algolia, string $query, array $options) {
$options['body']['query']['bool']['filter']['geo_distance'] = [
'distance' => '1000km',
'location' => ['lat' => 36, 'lon' => 111],
];
return $algolia->search($query, $options);
}
)->get();
カスタムエンジン
エンジンのプログラミング
組み込みのScout検索エンジンがニーズに合わない場合、独自のカスタムエンジンを書き、Scoutへ登録してください。エンジンは、Laravel\Scout\Engines\Engine
抽象クラスを拡張してください。この抽象クラスは、カスタムエンジンが実装する必要のある、8つのメソッドを持っています。
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);
これらのメソッドの実装をレビューするために、Laravel\Scout\Engines\AlgoliaEngine
クラスが役に立つでしょう。このクラスは独自エンジンで、各メソッドをどのように実装すればよいかの、良い取り掛かりになるでしょう。
エンジンの登録
カスタムエンジンを作成したら、Scoutエンジンマネージャのextend
メソッドを使用してScoutへ登録します。Scoutのエンジンマネージャは、Laravelサービスコンテナが依存解決できます。App\Providers\AppServiceProvider
クラスのboot
メソッドまたはアプリケーションが使用している他のサービスプロバイダからextend
メソッドを呼び出せます。
use App\ScoutExtensions\MySqlSearchEngine
use Laravel\Scout\EngineManager;
/**
* 全アプリケーションサービスの初期起動処理
*
* @return void
*/
public function boot()
{
resolve(EngineManager::class)->extend('mysql', function () {
return new MySqlSearchEngine;
});
}
エンジンを登録したら、アプリケーションのconfig/scout.php
設定ファイルでデフォルトのスカウトdriver
として指定できます。
'driver' => 'mysql',
ビルダマクロ
カスタムのScout検索ビルダメソッドを定義する場合は、Laravel\Scout\Builder
クラスでmacro
メソッドが使用できます。通常、「マクロ」はサービスプロバイダのboot
メソッド内で定義する必要があります。
use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;
use Laravel\Scout\Builder;
/**
* 全アプリケーションサービスの初期起動処理
*
* @return void
*/
public function boot()
{
Builder::macro('count', function () {
return $this->engine->getTotalCount(
$this->engine()->search($this)
);
});
}
macro
関数は、最初の引数にマクロ名、2番目の引数にクロージャを取ります。マクロのクロージャは、Laravel\Scout\Builder
実装からマクロ名を呼び出すときに実行されます。
use App\Models\Order;
Order::search('Star Trek')->count();