Laravelは今日、ウェブサービスを構築する開発者にとって非常に頼りになる存在です。

オープンソースソフトウェアであるLaravelには、すぐに使える機能が多数揃っており、堅牢で機能的なアプリケーションを構築できます。

そうした機能のひとつである、Laravel Scoutは、アプリケーションの検索インデックスを管理するライブラリです。非常に柔軟性があり、開発者は設定を細かく調整できる上、インデックスの保存先にAlgolia、Meilisearch、MySQL、Postgresドライバを選択できます。

この記事では、Laravel Scoutを詳細に解説し、Laravelアプリケーションにドライバ経由で全文検索機能を追加する方法をご説明します。後半では、模型列車の名前を保存するデモLaravelアプリケーションを作成し、Laravel Scoutを使用して検索機能を追加していきます。

前提条件

以下の条件を前提として解説していきます。

  • コンピュータにPHPコンパイラがインストールされている(本記事ではPHP 8.1を使用)
  • コンピュータにDockerエンジン、またはDocker Desktopがインストールされている
  • Algoliaのクラウドアカウントを所有している(無料で作成可能)

LaravelプロジェクトにScoutをインストールする方法

Scoutを使用するには、まず検索機能を追加するLaravelアプリケーションを作成します。Laravel-Scout Bashスクリプトには、Dockerコンテナ内でLaravelアプリケーションを生成するコマンドが含まれています。Dockerを使用すれば、MySQLデータベースなどの追加ソフトウェアのインストールは不要です。

Laravel-scoutスクリプトはBashスクリプト言語を使用するため、実行にはLinux環境が必要です。Windowsを使用している場合は、Windows Subsystem for Linux(WSL)を構成してください。

WSLを使用している場合は、ターミナルで以下のコマンドを実行し、好みのLinuxディストリビューションを設定します。

wsl -s ubuntu

次に、コンピューター上のプロジェクト格納場所に移動します。Laravel-Scoutスクリプトは、ここにプロジェクトディレクトリを生成します。desktopディレクトリ内にプロジェクトディレクトリを作成する場合、以下のようになります。

cd /desktop

以下のコマンドで、Laravel-Scoutスクリプトを実行します。このスクリプトは、定形のコードを含む、Dockerアプリケーションの雛形を生成するものです。

curl -s https://laravel.build/laravel-scout-app | bash

実行後、cd laravel-scout-appで、ディレクトリを変更します。次にプロジェクトフォルダー内でsail-upコマンドを実行して、アプリケーション用のDockerコンテナを起動します。

注意)多くのLinuxディストリビューションでは、一時的に権限を上げるためにsudoコマンドを介して、コマンドを実行する必要があります。

./vendor/bin/sail up

次のようなエラーが発生する場合があります。

ポートが割り当てられていることを示すエラー
ポートが割り当てられていることを示すエラー

これを解決するには、sail upコマンド内でAPP_PORT変数を使用してポートを指定します。

APP_PORT=3001 ./vendor/bin/sail up

次に、以下のコマンドを使って、PHPサーバー上のArtisanでアプリケーションを実行します。

php artisan serve
ArtisanでLaravelアプリケーションを稼働する
ArtisanでLaravelアプリケーションを稼働する

ウェブブラウザから実行中のアプリケーションに「http://127.0.0.1:8000」でアクセスすると、デフォルトルートで、Laravelのウェルカムページが表示されます。

Laravelアプリケーションのウェルカムページ
Laravelアプリケーションのウェルカムページ

アプリケーションにLaravel Scoutを追加する方法

ターミナルで次のコマンドを入力して、Composer PHPパッケージマネージャを有効にし、プロジェクトにLaravel Scoutを追加します。

composer require laravel/scout

次にvendor:publishコマンドを使用して、Scout構成ファイルを作成します。このコマンドは、アプリケーションのconfigディレクトリにscout.php設定ファイルを作成するものです。

 php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

次に、SCOUT_QUEUEブール値を含むように、.envファイルを変更します。

SCOUT_QUEUE値の設定により、Scoutは操作をキューに入れ、レスポンスタイムを向上できます。MeilisearchのようなScoutドライバは、SCOUT_QUEUE値がなければ、新しいレコードをすぐに反映できません。

SCOUT_QUEUE=true

また、Dockerコンテナ内でMySQLデータベースを使用するために、.envファイルのDB_HOST変数にlocalhostを設定します。

DB_HOST=127.0.0.1

モデルをマークしてインデックスを構成する方法

Scoutでは、検索可能なデータモデルがデフォルトで無効になっています。そのため、Laravel\Scout\Searchableトレイトを使用して、明示的にモデルを検索可能としてマークする必要があります。

まず、デモTrainアプリケーションのデータモデルを作成し、検索可能としてマークします。

モデルの作成方法

Trainアプリケーションには、所有する模型列車の名前を保存します。

以下のArtisanコマンドを実行して、create_trains_tableマイグレーションを生成します。

php artisan make:migration create_trains_table 
create_trains_tableマイグレーションの作成
create_trains_tableマイグレーションの作成

マイグレーションは、指定した名前と現在のタイムスタンプを組み合わせた形式のファイル名で生成されます。

database/migrations/ディレクトリ内のマイグレーションファイルを開きます。

title列を追加するには、17行目付近のid()カラムの後に以下のコードを追加します。これによって、title列が追加されます。

$table->string('title');

次のコマンドを実行して、マイグレーションを適用しましょう。

php artisan migrate
Artisanマイグレーションの適用
Artisanマイグレーションの適用

データベースマイグレーションを実行したら、app/Models/ディレクトリにTrain.phpファイルを作成してください。

Laravel\Scout\Searchableトレイトを追加する方法

TrainモデルにLaravel\Scout\Searchableトレイトを追加して、検索可能とマークします。

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Train extends Model
{
    use Searchable;
    public $fillable = ['title'];

また、searchableAsメソッドをオーバーライドして検索インデックスを設定します。Scoutのデフォルトの動作は、モデルのテーブル名と一致するようにモデルを永続化します。

Train.phpファイルの上のコードブロックに続けて、以下のコードを追加します。

/**
  * Retrieve the index name for the model.
  *
  * @return string
 */
 public function searchableAs()
    {
        return 'trains_index';
   }
}

ScoutでAlgoliaを使用する方法

Laravel Scoutを使用した最初の全文検索には、Algoliaドライバを使用します。Algoliaは大量のデータを検索するためのSaaS(Software as a Service)プラットフォームです。Algoliaには検索インデックスを管理するウェブダッシュボードと、ソフトウェア開発キット(SDK)を介して好きなプログラミング言語から利用できる堅牢なAPIがあります。

Laravelアプリケーションでは、PHP用のAlgoliaクライアントパッケージを使用します。

Algoliaのセットアップ方法

まず、アプリケーション用にAlgoliaのPHP検索クライアントパッケージをインストールします。

次のコマンドを実行してください。

composer require algolia/algoliasearch-client-php

次に、.envファイルにAlgoliaのアプリケーションIDとシークレットAPIキーの認証情報を設定します。

ウェブブラウザからAlgoliaの管理画面にアクセスし、アプリケーションIDとシークレットAPIキーの認証情報を取得します。

左側のサイドバーの一番下にある「Settings(設定)」をクリックして、「Settings」ページに移動します。

「Team and Access(チームとアクセス権)」セクションで「API Keys(APIキー)」をクリックし、Algoliaアカウントのキーを表示します。

AlgoliaクラウドのAPI Keysページ
AlgoliaクラウドのAPI Keysページ

API Keysページで「Application ID(アプリケーションID)」と「Admin API Key(管理APIキー)」の値をメモしてください。この認証情報を使用して、LaravelアプリケーションとAlgolia間の接続を認証します。

Application IDとAdmin API Keys
Application IDとAdmin API Keys

コードエディターを使用して、.envファイルに以下のコードを追加します。プレースホルダは、メモした対応するAlgolia API認証情報で置き換えてください。

ALGOLIA_APP_ID=APPLICATION_ID
ALGOLIA_SECRET=ADMIN_API_KEY

また、SCOUT_DRIVER変数を以下のコードに置き換え、値をmeilisearchからalgoliaに変更してください。この値によりScoutがAlgoliaドライバを使用します。

SCOUT_DRIVER=algolia

アプリケーションコントローラの作成方法

app/Http/Controllers/ディレクトリのTrainSearchController.phpファイル内に、アプリケーションのコントローラを実装します。このコントローラは、Trainモデルの一覧を表示し、データを追加します。

TrainSearchController.phpファイルに以下のコードブロックを追加し、コントローラを構築します。

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Models\Train;

class TrainSearchController extends Controller
{
    /**
     * Compile the content for a trains list view.
     *
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
    */
    public function index(Request $request)
    {
        if($request->has('titlesearch')){
            $trains = Train::search($request->titlesearch)
                ->paginate(6);
        }else{
            $trains = Train::paginate(6);
        }
        return view('Train-search',compact('trains'));
    }

    /**
     * Create a new train entry.
     *
     * @return \Illuminate\Http\RedirectResponse
    */
    public function create(Request $request)
    {
        $this->validate($request,['title'=>'required']);

        $trains = Train::create($request->all());
        return back();
    }
}

アプリケーションルートの作成方法

続いて、列車の一覧を取得し、またデータベースに列車を追加するルートを作成します。

routes/web.phpファイルを開き、既存のコードを以下のブロックで置き換えてください。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TrainSearchController;

Route::get('/', function () {
    return view('welcome');
});

Route::get('trains-lists', [TrainSearchController::class, 'index']) -> name ('trains-lists');

Route::post('create-item', [TrainSearchController::class, 'create']) -> name ('create-item');

このコードは、アプリケーションに2つのルートを定義します。/trains-listsルートのGETリクエストは、保存されているすべての列車データを一覧表示し、/create-itemルートのPOSTリクエストは、列車データを作成します。

アプリケーションビューの作成方法

resources/views/ディレクトリ内にTrain-search.blade.phpファイルを作成します。このファイルは、検索機能のユーザーインターフェースを表示します。

以下のコードブロックをTrain-search.blade.phpファイルに追加し、検索機能用の単一ページを作成しましょう。

<!DOCTYPE html>
<html>
<head>
    <title>Laravel - Laravel Scout Algolia Search Example</title>
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <h2 class="text-bold">Laravel Full-Text Search Using Scout </h2><br/>
    <form method="POST" action="{{ route('create-item') }}" autocomplete="off">
        @if(count($errors))
            <div class="alert alert-danger">
                <strong>Whoops!</strong> There is an error with your input.
                <br/>
                <ul>
                    @foreach($errors->all() as $error)
                    <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <input type="hidden" name="_token" value="{{ csrf_token() }}">

        <div class="row">
            <div class="col-md-6">
                <div class="form-group {{ $errors->has('title') ? 'has-error' : '' }}">
                    <input type="text" id="title" name="title" class="form-control" placeholder="Enter Title" value="{{ old('title') }}">
                    <span class="text-danger">{{ $errors->first('title') }}</span>
                </div>
            </div>
            <div class="col-md-6">
                <div class="form-group">
                    <button class="btn btn-primary">Create New Train</button>
                </div>
            </div>
        </div>
    </form>

    <div class="panel panel-primary">
      <div class="panel-heading">Train Management</div>
      <div class="panel-body">
            <form method="GET" action="{{ route('trains-lists') }}">

                <div class="row">
                    <div class="col-md-6">
                        <div class="form-group">
                            <input type="text" name="titlesearch" class="form-control" placeholder="Enter Title For Search" value="{{ old('titlesearch') }}">
                        </div>
                    </div>
                    <div class="col-md-6">
                        <div class="form-group">
                            <button class="btn btn-primary">Search</button>
                        </div>
                    </div>
                </div>
            </form>

            <table class="table">
                <thead>
                    <th>Id</th>
                    <th>Train Title</th>
                    <th>Creation Date</th>
                    <th>Updated Date</th>
                </thead>
                <tbody>
                    @if($trains->count())
                        @foreach($trains as $key => $item)
                            <tr>
                                <td>{{ ++$key }}</td>
                                <td>{{ $item->title }}</td>
                                <td>{{ $item->created_at }}</td>
                                <td>{{ $item->updated_at }}</td>
                            </tr>
                        @endforeach
                    @else
                        <tr>
                            <td colspan="4">No train data available</td>
                        </tr>
                    @endif
                </tbody>
            </table>
            {{ $trains->links() }}
      </div>
    </div>
</div>
</body>
</html>

上のHTMLコードには、データベースに保存する前に列車のタイトルを入力するためのフィールドとボタンを含むフォーム要素が含まれています。また、データベース内の列車データのidtitle(タイトル)、created_at(作成日時)、updated_at(更新日時)情報を表示するHTMLテーブルも書かれています。

Algolia検索の使用方法

ページを表示するには、ウェブブラウザからhttp://127.0.0.1:8000/trains-listsにアクセスします。

列車データ
列車データ

データベースは現在空の状態です。入力フィールドにデモ用の列車のタイトルを入力し、「Create New Train(新しい列車の作成)」をクリックして保存します。

列車データの挿入
列車データの挿入

検索機能を使用するには、保存されている列車タイトルの中からキーワードを「Enter Title For Search(検索用タイトルを入力)」入力フィールドに入力し、「Search(検索)」をクリックします。

以下のように、タイトルにキーワードを含む列車データのみが表示されます。

列車データ検索機能の使用
列車データ検索機能の使用

Laravel ScoutとMeilisearch

Meilisearchはスピード、パフォーマンス、開発者体験の向上に焦点を当てたオープンソースの検索エンジンです。Algoliaと複数の機能を共有し、同じアルゴリズム、データ構造、研究結果を使用していますが、プログラミング言語は異なります。

開発者は、オンプレミスまたはクラウドインフラ内でMeilisearchのインスタンスを作成し、自身でサーバーを運用できます。またインフラを管理せずに製品を使用できるよう、Algoliaと同様のベータ版クラウドオファリングもあります。

現在のデモ環境では、すでにDockerコンテナ内でMeilisearchのローカルインスタンスが動作しています。ここからLaravel Scout機能を拡張し、Meilisearchインスタンスを使用できます。

LaravelアプリケーションにMeilisearchを追加するには、プロジェクトのターミナルで以下のコマンドを実行します。

composer require meilisearch/meilisearch-php

次に、.envファイル内のMeilisearch変数を変更して構成します。

.envファイルのSCOUT_DRIVERMEILISEARCH_HOSTMEILISEARCH_KEY変数を以下で置き換えてください。

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=LockKey

SCOUT_DRIVERキーはScoutが使用するドライバを指定し、MEILISEARCH_HOSTはMeilisearchインスタンスが動作するドメインを指定します。開発時には必要ありませんが、本番環境ではMEILISEARCH_KEYを追加することを推奨します。

注意)Meilisearchを優先ドライバとして使用する際は、Algolia IDとシークレットをコメントアウトしてください。

.envの構成が完了したら、以下のArtisanコマンドを使用して、既存レコードのインデックスを作成してください。

php artisan scout:import "App\Models\Train"

Laravel Scoutとデータベースエンジン

Scoutのデータベースエンジンは、小規模なデータベースを使用するアプリケーションや、それほど処理負荷のかからないアプリケーションに適しています。現在、データベースエンジンはPostgreSQLとMySQLをサポートしています。

このエンジンは、既存のデータベースに対して「where-like」句と全文検索インデックスを使用して、最も関連性の高い検索結果を取得します。また、使用する際、レコードのインデックスは必要ありません。

データベースエンジンを使用するには、SCOUT_DRIVER .env変数をdatabaseに設定します。

Laravelアプリケーションの.envファイルを開き、SCOUT_DRIVER変数の値を変更します。

SCOUT_DRIVER = database

ドライバをdatabaseに変更すると、Scoutが全文検索にデータベースエンジンを使用するようになります。

Laravel Scoutとコレクションエンジン

Scoutにはデータベースエンジンに加え、コレクションエンジンもあります。このエンジンは、「where」句とコレクションフィルタリングを使用して、最も関連性の高い検索結果を抽出します。

データベースエンジンとは異なり、Laravelのサポートするすべてのリレーショナルデータベースに対応しています。

SCOUT_DRIVER環境変数をcollectionに設定するか、Scout構成ファイルでコレクションドライバを指定することで使用可能です。

SCOUT_DRIVER = collection

Elasticsearch Explorer

Elasticsearchクエリの強みを生かしたExplorerは、Laravel Scout用の最新のElasticsearchドライバ。互換性のあるScoutドライバが提供され、大量のデータをリアルタイムで保存、検索、分析できます。Laravelを使用したElasticsearchは、ミリ秒単位で結果を返します。

LaravelアプリケーションでElasticsearch Explorerドライバを使用するには、Laravel-Scoutスクリプトが生成したdocker-compose.ymlファイルで構成する必要があります。Elasticsearch用の構成を追加し、コンテナを再起動します。

docker-compose.ymlファイルを開き、内容を以下で置き換えてください。

# For more information: https://laravel.com/docs/sail
version: '3'
services:
    laravel.test:
        build:
            context: ./vendor/laravel/sail/runtimes/8.1
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.1/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - mysql
            - redis
            - meilisearch
            - mailhog
            - selenium
            - pgsql
            - elasticsearch

    mysql:
        image: 'mysql/mysql-server:8.0'
        ports:
            - '${FORWARD_DB_PORT:-3306}:3306'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: "%"
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        volumes:
            - 'sail-mysql:/var/lib/mysql'
            - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
            retries: 3
            timeout: 5s
            
    elasticsearch:
        image: 'elasticsearch:7.13.4'
        environment:
            - discovery.type=single-node
        ports:
            - '9200:9200'
            - '9300:9300'
        volumes:
            - 'sailelasticsearch:/usr/share/elasticsearch/data'
        networks:
            - sail
    kibana:
        image: 'kibana:7.13.4'
        environment:
            - elasticsearch.hosts=http://elasticsearch:9200
        ports:
            - '5601:5601'
        networks:
            - sail
        depends_on:
            - elasticsearch
    redis:
        image: 'redis:alpine'
        ports:
            - '${FORWARD_REDIS_PORT:-6379}:6379'
        volumes:
            - 'sail-redis:/data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "redis-cli", "ping"]
            retries: 3
            timeout: 5s
    pgsql:
        image: 'postgres:13'
        ports:
            - '${FORWARD_DB_PORT:-5432}:5432'
        environment:
            PGPASSWORD: '${DB_PASSWORD:-secret}'
            POSTGRES_DB: '${DB_DATABASE}'
            POSTGRES_USER: '${DB_USERNAME}'
            POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
        volumes:
            - 'sailpgsql:/var/lib/postgresql/data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "pg_isready", "-q", "-d", "${DB_DATABASE}", "-U", "${DB_USERNAME}"]
            retries: 3
            timeout: 5s
    meilisearch:
        image: 'getmeili/meilisearch:latest'
        ports:
            - '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
        volumes:
            - 'sail-meilisearch:/meili_data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "wget", "--no-verbose", "--spider",  "http://localhost:7700/health"]
            retries: 3
            timeout: 5s
    mailhog:
        image: 'mailhog/mailhog:latest'
        ports:
            - '${FORWARD_MAILHOG_PORT:-1025}:1025'
            - '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025'
        networks:
            - sail
    selenium:
        image: 'selenium/standalone-chrome'
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        volumes:
            - '/dev/shm:/dev/shm'
        networks:
            - sail
networks:
    sail:
        driver: bridge
volumes:
    sail-mysql:
        driver: local
    sail-redis:
        driver: local
    sail-meilisearch:
        driver: local
    sailpgsql:
        driver: local
    sailelasticsearch:
        driver: local 

次に、以下のコマンドを実行して、docker-compose.ymlファイルに追加したElasticsearchイメージを取得します。

docker-compose up

それから以下のComposerコマンドを実行して、Explorerをプロジェクトにインストールします。

composer require jeroen-g/explorer

また、Explorerドライバの構成ファイルも作成する必要があります。

以下のArtisanコマンドを実行して、構成を保存するexplorer.configファイルを生成します。

php artisan vendor:publish --tag=explorer.config

生成された構成ファイルは/configディレクトリにあります。

config/explorer.phpファイルで、indexesキーを使用してモデルを参照できます。

'indexes' => [
        \App\Models\Train::class
],

.envファイル内のSCOUT_DRIVER変数の値をelasticに変更し、ScoutがExplorerドライバを使用するように構成します。

SCOUT_DRIVER = elastic

この時点で、Explorerインターフェースを実装し、 mappableAs()メソッドをオーバーライドすることで、 Trainモデル内でExplorerを使用できます。

App\Modelsディレクトリ内のTrain.phpファイルを開き、既存のコードを以下のコードで置き換えます。

<?php
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use JeroenG\Explorer\Application\Explored;
use Laravel\Scout\Searchable;
 
class Train extends Model implements Explored
{
    use HasFactory;
    use Searchable;
 
    protected $fillable = ['title'];
 
    public function mappableAs(): array
    {
        return [
        	'id'=>$this->Id,
        	'title' => $this->title,
        ];
    }
} 

上で追加したコードにより、Explorerを使用してTrainモデル内のテキストを検索できます。

まとめ

LaravelとScoutのようなアドオンを活用すれば、高速で堅牢な全文検索機能を簡単に統合できます。データベースエンジン、コレクションエンジン、そしてMeilisearchとElasticsearchの機能を使用して、アプリケーションのデータベースと通信し、数ミリ秒単位の高度な検索メカニズムを実装しましょう。

シームレスにデータベースを管理、更新することで、コードをクリーンかつ効率的に保ちながら、優れたユーザー体験を提供することができます。

Kinstaのアプリケーション&データベースホスティングは、最新のLaravel開発における要件をすべて満たします。初月20ドル分は無料でご利用いただけます

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。