laravel scoutでUserのnameとemailを横断検索する (meilisearch)
一般的に、RDBMSにおいてフィールドを跨ぐ検索は面倒くさい、ので「laravel scout」を使ってやってみよう。
laravel11でやってみたけどまあ10と変わらず動きそうってのが結論ではある。
そもそもlaravel scoutとは
バックエンドの検索エンジンと接続するためのインタフェースなのでバックエンドエンジンが必要となる。バックエンドといってもmysqlとかのRDBMSとは別に存在する事になるので結果としてDBシステムと検索システムが独立する事になる。ここは注意が必要なところだ。
バックエンドエンジンには
Algolia
Meilisearch
Typesense
という3つをサポートしており、Typesenseに関してはlaravel10からサポートしているようである。
なお、algoliaは有償の外部サービスなのでローカルでは利用できない。流石に便利なのだが無償ではインデックスの制限が厳しいという印象だ
それ以外はsailを使ってるならartisan sail:addで追加可能である。
meilisearchを使ってみる
sail:addで追加するとdocker-compose.ymlが変更される
+ - meilisearch
mysql:
image: 'mysql/mysql-server:8.0'
ports:
@@ -56,9 +57,30 @@ services:
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
networks:
- sail
+ meilisearch:
+ image: 'getmeili/meilisearch:latest'
+ ports:
+ - '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
+ environment:
+ MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}'
+ volumes:
+ - 'sail-meilisearch:/meili_data'
+ networks:
+ - sail
+ healthcheck:
+ test:
+ - CMD
+ - wget
+ - '--no-verbose'
+ - '--spider'
+ - 'http://localhost:7700/health'
+ retries: 3
+ timeout: 5s
てきな。もちろん、sailの再起動が必要だ。
ライブラリー的な準備
何を使うにしてもscoutを利用する場合はcomposerで当該ライブラリーを導入する必要がある。
composer require laravel/scout
続いて
artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
とすると config/scout.php が出来る。ある程度chatgptに翻訳してもろて重要なところを設定していこう
設定
まずエンジンを選定する必要がある。
/*
|--------------------------------------------------------------------------
| デフォルト検索エンジン
|--------------------------------------------------------------------------
|
| このオプションは、Laravel Scoutを使用している間に使用されるデフォルトの検索接続を制御します。
| この接続は、すべてのモデルを検索サービスに同期するときに使用されます。
| あなたのニーズに基づいてこれを調整する必要があります。
|
| サポート: "algolia", "meilisearch", "typesense",
| "database", "collection", "null"
|
*/
'driver' => env('SCOUT_DRIVER', 'algolia'),
このようにdefaultではalgoliaになっているが、.env(あるいは環境変数)で上書き可能だ。ここでは上記の通りmeilisearchを使うので.envにそのように仕込む
.env に追記
SCOUT_DRIVER=meilisearch
続く設定は基本的には変更の必要がないものだが、meilisearchとtypesenseの独自の設定は独立しているから、これを見る必要がある。もちろん今回はmeilisearchだけ見ることにする
/*
|--------------------------------------------------------------------------
| Meilisearch設定
|--------------------------------------------------------------------------
|
| ここでMeilisearch設定を構成できます。Meilisearchは、最小限の設定で使用できるオープンソースの検索エンジンです。
| 以下に、あなた自身のMeilisearchインストールのホストとキー情報を記述します。
|
| 参照: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
*/
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
// 'users' => [
// 'filterableAttributes'=> ['id', 'name', 'email'],
// ],
],
],
hostなどの設定を書き加える
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
さらに注目するのは
'index-settings' => [
// 'users' => [
// 'filterableAttributes'=> ['id', 'name', 'email'],
// ],
],
であるが、今はこのままにしてみよう。
あと、サーバーに関しては基本的にsailを使った場合はdefaultのport 7700で動作するはずなので現状では特に変更する必要は無いはずだ。
モデルの変更
ここではUserモデルに仕掛けるので、Userモデルを変更する。
use Laravel\Scout\Searchable;
class User extends Authenticatable
{
use HasFactory, Notifiable, HasRoles, Searchable;
このようにSearchableを付けるだけであーる。
meilisearch用のライブラリー
scoutは実際にはbackendの検索エンジンの抽象化レイヤーにすぎないので、それらのエンジンを稼動させるライブラリーを必要とする。ここではtypesenseを選定したわけだからそのライブラリーを導入しなければならない。ドキュメントの通り以下のようにする
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle
import
% ./vendor/bin/sail artisan scout:import "App\Models\User"
Imported [App\Models\User] models up to ID: 11
All [App\Models\User] records have been imported.
meilisearchの場合は何も書いてなくともimportが成功する。またindexは当該ホストのport7700にアクセスすると以下のような管理UIが見えるはずだ
以上のように、とりあえず全てのフィールドが含まれている事が理解できる。このように管理UIがある事がかなり利便性を高めているが、管理UIを活性化したくない場合はローカルバインドするなり、いろいろ処置が必要である。いずれにせよdefaultではインデックスが丸見えになるのでセキュリティーの配慮は十分気をつけたい。
検索してみる
tinkerで
> User::all(['id', 'name', 'email'])
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#6358
all: [
App\Models\User {#6359
id: 1,
name: "渡辺 舞",
email: "user1@example.com",
},
App\Models\User {#6360
id: 2,
name: "田辺 春香",
email: "user2@example.com",
},
App\Models\User {#6361
id: 3,
name: "佐々木 花子",
email: "user3@example.com",
},
App\Models\User {#6362
id: 4,
name: "吉田 七夏",
email: "user4@example.com",
},
App\Models\User {#6363
id: 5,
name: "佐々木 結衣",
email: "user5@example.com",
},
App\Models\User {#6364
id: 6,
name: "廣川 淳",
email: "user6@example.com",
},
App\Models\User {#6365
id: 7,
name: "工藤 春香",
email: "user7@example.com",
},
App\Models\User {#6366
id: 8,
name: "松本 涼平",
email: "user8@example.com",
},
App\Models\User {#6367
id: 9,
name: "三宅 加奈",
email: "user9@example.com",
},
App\Models\User {#6368
id: 10,
name: "高橋 学",
email: "user10@example.com",
},
App\Models\User {#6369
id: 11,
name: "Admin User",
email: "admin@example.com",
},
],
}
現在このようになっているとする。幸いfake()で「佐々木」が二人存在する。
> User::search('佐々木')->get()
= Illuminate\Database\Eloquent\Collection {#6400
all: [
App\Models\User {#6380
id: 3,
name: "佐々木 花子",
email: "user3@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
#password: "$2y$12$I6ND8V4rY9305tTLBsnH4.W.nR8MI9NHd5yDQKNuHBh2eFOIeWnz.",
#remember_token: "5hJv4pglHi",
created_at: "2024-04-19 13:33:13",
updated_at: "2024-04-19 13:33:13",
},
App\Models\User {#6379
id: 5,
name: "佐々木 結衣",
email: "user5@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
#password: "$2y$12$I6ND8V4rY9305tTLBsnH4.W.nR8MI9NHd5yDQKNuHBh2eFOIeWnz.",
#remember_token: "p8n4MX11Sp",
created_at: "2024-04-19 13:33:13",
updated_at: "2024-04-19 13:33:13",
},
],
}
このようにsearch()メソッドを使って検索する。ではメールはどうだろう。
> User::search('example.com')->get()
= Illuminate\Database\Eloquent\Collection {#6357
all: [
App\Models\User {#6382
id: 1,
name: "渡辺 舞",
email: "user1@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
#password: "$2y$12$I6ND8V4rY9305tTLBsnH4.W.nR8MI9NHd5yDQKNuHBh2eFOIeWnz.",
#remember_token: "ZoveCjCGWv",
created_at: "2024-04-19 13:33:13",
updated_at: "2024-04-19 13:33:13",
},
App\Models\User {#6448
id: 2,
name: "田辺 春香",
email: "user2@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
まあよさそうである。
余計なフィールドを含めたくない場合
今userモデルをimportしたら問答無用に全てのカラムの情報がindexされたが、実際には必要ないことがある。ここではnameだけを含めたいという場合の対策を考える。
まずUserモデルで
public function toSearchableArray(): array
{
return [
'id' => (int) $this->id,
'name' => $this->name,
];
}
このようにする。数値型は明示的にintだのfloatだのセットする必要があるようだ
そしたら一度indexを消去する
% ./vendor/bin/sail artisan scout:flush "App\Models\User"
All [App\Models\User] records have been flushed.
そして再度importする
% ./vendor/bin/sail artisan scout:import "App\Models\User"
そうすれば以下のように指定されたものだけになるだろう。
さらに絞りこむ場合
たとえばsearchした結果をさらにemailで絞りこむ場合とか、これはまずemail自体も検索フィールドに含める必要がある。meilisearchのindexを使ってさらにDBのフィールドだけというわけにはいかない。
public function toSearchableArray(): array
{
return [
'id' => (int) $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
これでindexを作りなおして
% ./vendor/bin/sail artisan scout:flush "App\Models\User"
All [App\Models\User] records have been flushed.
% ./vendor/bin/sail artisan scout:import "App\Models\User"
Imported [App\Models\User] models up to ID: 11
All [App\Models\User] records have been imported.
検索すると
> User::search('佐々木')->get()
= Illuminate\Database\Eloquent\Collection {#6434
all: [
App\Models\User {#6336
id: 3,
name: "佐々木 花子",
email: "user3@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
#password: "$2y$12$I6ND8V4rY9305tTLBsnH4.W.nR8MI9NHd5yDQKNuHBh2eFOIeWnz.",
#remember_token: "5hJv4pglHi",
created_at: "2024-04-19 13:33:13",
updated_at: "2024-04-19 13:33:13",
},
App\Models\User {#6360
id: 5,
name: "佐々木 結衣",
email: "user5@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
#password: "$2y$12$I6ND8V4rY9305tTLBsnH4.W.nR8MI9NHd5yDQKNuHBh2eFOIeWnz.",
#remember_token: "p8n4MX11Sp",
created_at: "2024-04-19 13:33:13",
updated_at: "2024-04-19 13:33:13",
},
],
}
> User::search('佐々木')->where('email', 'user5@example.com')->get()
Meilisearch\Exceptions\ApiException Attribute `email` is not filterable. This index does not have configured filterable attributes.
1:6 email="user5@example.com".
このように検索結果にemailを付けても動作しない
ここで、config/scout.phpを適切にセットする
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
'users' => [
'filterableAttributes'=> ['id', 'name', 'email'],
],
],
],
このfilterableAttributesに登録されていないとうまいことwhereできない(meilisearchは、だが)。
% ./vendor/bin/sail artisan scout:sync-index-settings
Settings for the [users] index synced successfully.
このように scout:sync-index-settings とすると
> User::search('佐々木')->where('email', 'user5@example.com')->get()
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#6368
all: [
App\Models\User {#6389
id: 5,
name: "佐々木 結衣",
email: "user5@example.com",
email_verified_at: "2024-04-19 13:33:13",
last_login_at: null,
#password: "$2y$12$I6ND8V4rY9305tTLBsnH4.W.nR8MI9NHd5yDQKNuHBh2eFOIeWnz.",
#remember_token: "p8n4MX11Sp",
created_at: "2024-04-19 13:33:13",
updated_at: "2024-04-19 13:33:13",
},
],
}
ということが可能になった
次回は
これをwebシステムへ組込む
この記事が気に入ったらサポートをしてみませんか?