見出し画像

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もtypesenseも追加可能

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システムへ組込む

この記事が気に入ったらサポートをしてみませんか?