Laravel で Algolia を利用していくつかのモデルのデータを混ぜて検索する方法
オンライン家庭教師マナリンク 開発の Technote です。
今回は Laravel で Algolia を利用していくつかのモデルのデータを混ぜて検索する方法を紹介します。
Laravel Scout
Algolia の導入自体は Laravel Scout を利用すると、モデルの更新・削除の際に検索に必要なデータの同期を自動でやってくれるのでとても簡単です。
ただ、今回はいくつかのモデルのデータを混ぜて index を作成したいので Laravel Scout だけでは難しいです。
Scout Extended
自分で実装すると大変そうですが、Algolia のドキュメントにもやり方があるように Scout Extended というパッケージを利用すると簡単に実装できます。
導入方法
1. パッケージの追加
composer require algolia/scout-extended
2. Aggregator の生成
php artisan scout:make-aggregator [Aggregator名]
このコマンドで app/Search
に Aggregator
を継承したクラスが生成されます。
ここでは AutocompleteItem
で生成したとして話を進めます。
3. models に検索対象にしたいモデルを追加
namespace App\Search;
use Algolia\ScoutExtended\Searchable\Aggregator;
use App\Models\CourseFeature; // コースの特徴
use App\Models\Subject; // 科目
use App\Models\Teacher; // 先生
class AutocompleteItem extends Aggregator
{
protected $models = [
CourseFeature::class,
Subject::class,
Teacher::class,
];
}
Aggregator
の bootSearchable
を呼び出し
4. Model で use された Searchable
などの Trait に関しては boot[Trait名] が Laravel によって勝手に呼ばれますが、Aggregator
は AppServiceProvider
などで bootSearchable
を明示的に呼び出す必要があります。
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
AutocompleteItem::bootSearchable();
}
}
$models
に追加するモデルは Searchable
を use すべきかどうか
ドキュメント内では特に言及はないですが、shouldBeSearchable
の実装例を見ると Searchable
を use しているかどうかで分岐させています。
public function shouldBeSearchable()
{
// Check if the class uses the Searchable trait before calling shouldBeSearchable
if (array_key_exists(Searchable::class, class_uses($this->model))) {
return $this->model->shouldBeSearchable();
}
}
shouldBeSearchable
は同期するかどうかの判定を実装するメソッドです。
例えば先生が一つもコースを作成していない場合は同期しないなどの条件を記述します。
これは Aggregator
からそれぞれのモデルの shouldBeSearchable
を呼ぶための実装ですが、これだけを見ると Searchable
の use が必要なように思えます。
ただ、実装を追うとわかりますがそれぞれのモデルでの Searchable
の use は必要ありません。
逆に Searchable
を use すると Searchable
での Observer (ModelObserver
) と Aggregator
での Observer (AggregatorObserver
) をそれぞれ observe
することになり、 Aggregator
での index とは別にそのモデルの index も作成されてしまいます。
saved => searchable (
Model
) =>$this->newCollection([$this])->searchable()
=> macro 登録されたsearchable
が起動 =>queueMakeSearchable
で同期
saved => searchable (
Aggregator
) =>AggregatorCollection::make([$this])->searchable()
=> macro 登録されたsearchable
が起動 =>queueMakeSearchable
で同期
disableSyncingFor
を使用するとそれも防ぐことができますが、そもそもそのモデルだけで検索しない場合は use しなければいいだけです。
それぞれのモデルでは Searchable
を use せず、Aggregator
の shouldBeSearchable
の実装を以下のように method_exists
による判定にすれば色々とうまくいきました。
public function shouldBeSearchable()
{
if (method_exists($this->model, 'shouldBeSearchable')) {
return $this->model->shouldBeSearchable();
}
return false;
}
それぞれのモデルで Searchable
を use していなくても toSearchableArray
で同期するデータを整形することもできます。
関連データの同期について
今回の AutocompleteItem
の例での CourseFeature
のコースの特徴は、紐づく公開されたコースがない場合は同期させない要件でした。
よりイメージしやすいように例えば「記事」とそれに紐づく「タグ」を考えます。
公開された記事が一つでも紐付いているタグだけを同期したい場合、タグの操作だけではなく、記事の作成、削除や公開フラグの更新などでもタグの同期処理が実行される必要があります。
ここで紹介されているように $touches
を使用したり saved
イベントを購読することである程度実装できます。
ただし $touches
は更新日時カラムが存在しなければいけない点に注意が必要です。
また saved イベントを購読する場合にもモデルの save
とタグの sync
の実行順序に注意が必要です。
以下のように更新と作成を共通化している場合はうまくいきません。
function save(Article $article, $entity) {
$article->title = $entity->getTitle();
// ...
$article->save(); // saved イベント発火(まだ tags は同期されていない)
$article->tags()->sync($entity->getTags());
}
function create($entity) {
save(new Article(), $entity);
}
function update($entity) {
save(Article::findOrFail($entity->getId()), $entity);
}
またこの例ではタグが外される場合(≠タグの削除)も考慮する必要があるので Pivot化して中間テーブルの deleted
イベントを購読するなどの工夫も必要です。
まとめ
Scout Extended を利用すると複数のモデルを混ぜた検索も簡単に導入できますが、関連データの同期なども考えると注意すべきことも少なくないです。
もっと Algolia の技術記事が増えてくれると嬉しいです。
オンライン家庭教師マナリンクを運営するスタートアップNoSchoolのテックブログです。 manalink.jp/ 創業以来年次200%前後で売上成長しつつ、技術面・組織面での課題に日々向き合っています。 カジュアル面談はこちら! forms.gle/fGAk3vDqKv4Dg2MN7
Discussion