Laravel Scoutを使って全文検索algoliaの使い方も同時に理解
Laravel ScoutはLaravelのドキュメントサイトでも利用されているような全文検索を行うためのパッケージです。Laravel Scoutでは検索機能にalgoliaを利用することで高速に検索結果が表示されるだけでなくLaravel側で更新したデータをalogolia内で管理するデータと同期することが可能です。
algoliaに検索を行いたいデータをimportすることでローカルのデータを利用して検索を行うのではなくalgoliaにアクセスを行いalgoliaに保存されているデータを利用して検索結果を取得しています。検索だけではなくデータの追加や更新もLaravel Scoutを通してローカルデータとalgoliaを同期することができるため一度設定を行ってしまえばモデルを介してローカルのデータベースにアクセスしているように処理を行うことができるためalogliaを意識することがなくなります。
本文書ではalgoliaのアカウントの作成から検索の実行、ローカルデータベースとalgoliaのデータ同期について確認を行い、Reactでの検索UIの実装方法も説明しています。
目次
Algoliaについて
検索機能についてこれまで気にしたことがなければalgoliaという名前を聞いたことがない人もいるのではないでしょうか。algoliaはSearch as a serviceとして検索機能をクラウドサービスとして提供しておりLaravelのドキュメントの検索に利用されているだけではなくTailwindcss, Vue.js, Nuxt.js, React, Next.jsなど本ブログで度々紹介するオープソースのプロダクトだけではなく個人のサイトから商用サイトまで幅広く利用されています。実際にLaravelのドキュメント」サイトで検索を実施してみると検索結果が高速で表示されることを体感することができます。alogoliaは検索機能だけではなく検索情報を分析する機能を合わせ持っているためサイトを訪れるユーザがどのような情報を求めているかがわかります。
algoliaが利用されているかどうかは検索結果の右下に表示される”search by algolia”で判断することもできます。
algoliaの使い方の流れをブログを例に説明します。サーバに保存されたブログのタイトルやコンテンツのデータをalgoliaにインポートします。ブログに検索機能を実装し検索を実施するとAPI経由でalgoliaに検索文字列が渡されます。渡した文字列を使ってalgolia内で処理が行われすぐに検索結果を戻すので戻した結果をサーバが受け取り表示させます。流れは非常にシンプルで仕組み自体は難しいものではありません。後ほど説明をしますがLaravel Scoutを利用している場合はモデルを経由して検索を行うことができます。
検索機能をブラウザ側のJavaScriptで実装できるようにReact, Vue, Svelteなどに対応したSearch UIが提供されています。その場合はLaravelを介する必要はないのでAlogiliaサーバと直接やりとりを行います。Laravelはデータの追加、更新、削除に関してAlogoliaと連携を行います。本文書ではReactを利用した場合のSearch UIの設定方法について後半で解説しています。
Laravel環境の構築
Laravel Scoutパッケージをインストールする前にLaravelの環境を構築します。Laravel構築の手順については本記事では簡易的に行わせてもらいます。
laravel newコマンドで新規のLaravelプロジェクトを作成します。
% laravel new laravel_scout
データベースは簡易的に作成できるSQLiteを利用します。
% touch database/database.sqlite
.envファイルを開いてDATABASE_CONNECTIONをsqliteに変更します。DB_CONNECTION以外の環境変数のDB_*は削除してください。
DB_CONNECTION=sqlite
php migrateコマンドを実行してテーブルが作成できることを確認します。
% php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (3.05ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (2.77ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (1.91ms)
Laravel Scoutを利用して検索を行うために利用するのはusersテーブルです。usersテーブルにダミーデータを登録するためにLaravel Seeding機能を利用します。
日本語のダミーデータが作成できるようにconfig/app.phpファイルのfaker_localeの値をja_JPに変更します。
'faker_locale' => 'ja_JP',
次にdatabase¥seeders¥DatabaseSeeder.phpファイルを開いて100名分のユーザデータを登録できるように変更を行います。
public function run()
{
\App\Models\User::factory(100)->create();
}
php artisan db:seedコマンドを実行してダミーデータをusersテーブルに追加します。
% php artisan db:seed
Database seeding completed successfully.
本文書ではデータベースの管理ソフトのTablePlusを利用して作成したSQLiteデータベースにアクセスします。100件分のダミーデータを確認することができます。
Laravel/Scoutパッケージのインストール
Laravelのインストールと環境の構築が完了したら、Laravel/Scoutのパッケージのインストールを行います。
% composer require laravel/scout
インストール完了後、下記のコマンドでScoutのConfigファイルの作成を行います。configフォルダの中にscout.phpファイルが作成されます。
% php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
Copied File [/vendor/laravel/scout/config/scout.php] To [/config/scout.php]
Publishing complete.
検索を行いたいモデルにLaravel¥Scout¥Seachableトレイトを設定します。app¥Models¥User.phpファイルを開いて設定を行ってください。
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Scout\Searchable;
class User extends Authenticatable
{
use HasFactory, Notifiable, Searchable;
Algoliaでのアカウント作成
Algoliaでアカウントを作成するためにalgolia.comにアクセスします。トライアル期間が14日間と表示されていますがフリープランも準備されています。アカウントの作成後、アクセスするためのAPIキーを取得します。
アカウント作成画面が表示されるのでメールアドレスを入力するかGmail, GitHubのアカウントを利用してください。
アカウントを作成するためにクレジットカードの入力は必要ではありません。
名前の入力画面が表示されるので、必須項目を入力してください。電話番号は必須ではありません。
最後の入力項目で会社の名前や規模、役割を選択してください。
メールが送信されるのでメールがalgoliaから届いているかメールボックスを確認してください。
Algoliaから送信されたメールを確認して” Confirm my email”ボタンをクリックしてください。
データセンターの場所を選択する画面が表示されます。日本からのレスポンスタイムは77MSであることがわかります。Japanを選択して”Continue Configuration”ボタンをクリックしてください。
アカウントの作成が完了するとalgoliaのダッシュボードが表示されます。
以上でalogoliaのアカウントの作成は完了です。
フリープランへの変更
ダッシュボードの上部にトライアル期間の残り日数が表示されているのでフリープランへ変更するためには”Click here to upgrade your plan”をクリックします。
クリックするとプランの選択画面が表示されます。Freeを選択してください。
選択後スクロールをして”Review and Confirm”ボタンをクリックしてください。
terms of serviceやPrivacy Policyを確認しチェックして問題がなければUpdate Planボタンをクリックしてください。
Laravel Scoutの設定
先ほど作成したscout.phpファイルを開いてAlgoliaのAPIキーの設定を行います。
'algolia' => [
'id' => env('ALGOLIA_APP_ID', ''),
'secret' => env('ALGOLIA_SECRET', ''),
],
idとsecretを設定しますがALGOLIA_APP_IDとALGOLIA_SECRETは.envファイルから設定を行います。
.envファイルを開いてALGOLIA_APP_IDとALGOLIA_SECRETの環境変数を追加してAlgoliaから取得できるApplication IDとAdmin API KEYを設定します。
ALGOLIA_APP_ID=XXXXXX
ALGOLIA_SECRET=XXXXXXXXXXXXXXXXXXXXXXX
左のメニューのAPI KeysをクリックしてApplication IDとAdmin Keyを取得してください。Admin Keyを利用することで検索だけではなくデータの登録を行うことができます。
idとsecretの設定ができたらalgoliasearch-client-phpのインストールを行います。
% composer require algolia/algoliasearch-client-php
ここまでの設定が完了するとusersテーブルの内容をalogoliaにインポートすることができます。
インポートはphp artisan scout:importコマンドを利用します。インポートの完了メッセージが表示されているのでalgoliaのダッシュボードを確認します。
% php artisan scout:import "App\Models\User"
Imported [App\Models\User] models up to ID: 100
All [App\Models\User] records have been imported.
ダッシュボードのOverviewを確認するとレコードが100件登録されていることが確認できます。
データをimportすることでalgoliaを利用するために必要となるタスクの中でindexの作成とデータのimportが完了します。最後に検索する属性の設定する必要があります。
indexの作成とデータのimportが完了したので最後にusersテーブル内のどの列の情報を検索対象とするかをSearchable attributesで選択します。nameとemailを選択します。設定完了後、検索を実施するとnameとemailにある値のみが検索対象となります。
Laravel Scoutの動作確認
検索を行う
Userモデルを使って検索を行ってみましょう。まずはalogoliaからデータが取得できるかweb.phpにコードを記述して確認します。
UserモデルではSearchableトレイトを利用しているのでsearchメソッドを使うことができます。searchメソッドの引数に検索したい文字を入力することで検索結果が戻されます。
Route::get('/users',function(){
$users = \App\Models\User::search('山')->get();
dd($users);
});
php artisan serveコマンドで開発サーバを起動しブラウザから/usersにアクセスを行ってください。
実行すると文字列の”山”を含むデータが18件取得することできました。Userモデルを介して検索を行うため検索結果が多い場合はpaginateでpaginationを利用することができます。
Algoliaを利用するためにインストールしたalgoliasearch-client-phpパッケージのみを利用しても下記のように検索を行うことができます。Searchableトレイトの中でも同様の処理が行われています。
Route::get('/users',function(){
// $users = \App\Models\User::search('山')->get();
$client = Algolia\AlgoliaSearch\SearchClient::create(
env('ALGOLIA_APP_ID'),
env('ALGOLIA_SECRET')
);
$index = $client->initIndex('users');
$results = $index->search('山');
dd($results);
});
戻り値は配列ですがhitsに18件のデータが含まれていることが確認できます。searchableトレイトを利用した時と結果は同じです。
検索機能の実装
Bladeファイルを作成してalgoriaを利用して検索機能を実装してみましょう。
php artisan make:controllerコマンドでコントローラーの作成を行います。
$ php artisan make:controller UsersController
Controller created successfully.
作成したUsersController.phpファイルにindexメソッドを追加し、Userモデルを利用してすべてのデータを取得します。表示するBladeファイルにはusers.indexを指定します。
namespace App\Http\Controllers;
use App\Models\User;
class UsersController extends Controller
{
public function index(){
$users = User::all();
return view('users.index',['users'=>$users]);
}
}
resources¥viewsフォルダの下にusersフォルダを作成してindex.blade.phpファイルを作成してください。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>ユーザ一覧</h1>
<table border="1">
<thead>
<tr>
<td>id</td>
<td>name</td>
<td>email</td>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr>
<td>{{ $user->id}}</td>
<td>{{ $user->name}}</td>
<td>{{ $user->email}}</td>
</tr>
@endforeach
</tbody>
</table>
</body>
</html>
ルーティングweb.phpファイルのルーティングで作成したコントローラーのindexメソッドを指定します。
use App\Http\Controllers\UsersController;
//略
Route::get('/users', [UsersController::class, 'index']);
ブラウザで/usersにアクセスを行うユーザ一覧が表示されることを確認します。
検索が行えるようにinput要素を追加します。getメソッドを使って入力した文字列を/usersに送信しています。
<body>
<h1>ユーザ一覧</h1>
<div style="margin:1em 0;">
<form method="get" action="/users">
<label for="search">検索</label>
<input name="search" value=""/>
<button type="submit">検索</button>
</form>
</div>
<table border="1">
コントローラー側で文字列を取得してsearchメソッドを利用して検索を行いその結果をusers.indexに渡します。
public function index(){
if(request()->search){
$users = User::search(request()->search)->get();
}else{
$users = User::all();
}
return view('users.index',['users'=>$users]);
}
検索用の入力フォームに文字列を入れて検索を入れると入力した文字列を含む文字のみ表示されます。
通常の検索機能との違いはsearchメソッドを利用することだけです。このように簡単に検索機能を実装することができます。
データの追加
通常はコントローラーを作成してデータの作成を行いますが、簡易的に行えるtinkerを利用してユーザデータを追加してみましょう。tinkerはLaravelのインストールフォルダでphp artisan tinkerで起動します。
% php artisan tinker
Psy Shell v0.10.6 (PHP 7.4.10 — cli) by Justin Hileman
>>> $user = new App\Models\User;
=> App\Models\User {#3366}
>>> $user->name = "明智光秀";
=> "明智光秀"
>>> $user->email = "kiringakuru@test.com";
=> "kiringakuru@test.com"
>>> $user->password = bcrypt('password');
=> "$2y$10$ImaIyjf7sGrJNvLLblRSIuCblqcnSvmIOaVjs8iQM4n6HjegkBQs."
>>> $user->save();
ユーザを登録後にalogoliaのダッシュボードでレコード数を確認します。1件追加したので101件になっていることが確認できます。またダッシュボード上で追加したユーザの名前を検索することができます。
データの更新
先ほど追加したデータの名前を更新してalgoliaに更新が反映されるか確認を行います。
>>> $user->name = '明智十兵衛';
=> "明智十兵衛"
>>> $user->update();
ダッシュボードで検索すると更新した名前になっていることが確認できます。
データの削除
最後にデータの削除も確認しておきます。
>>> $user = App\Models\User::find(102);
=> App\Models\User {#4359
id: "102",
name: "明智光秀",
email: "kiringakuru@test.com",
email_verified_at: null,
created_at: "2021-02-18 09:57:13",
updated_at: "2021-02-18 09:57:13",
}
>>> $user->delete();
=> true
一度削除をすると以下のエラーメッセージでalgoliaへの反映が失敗しましたが再度同じ流れで実行すると問題なく削除も反映されました。
GuzzleHttp\Exception\ConnectException with message ‘cURL error 28: Operation timed out after 2001 milliseconds with 0 out of 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://XXXXXX.algolia.net/1/indexes/users/batch’
このようにLaravel上で行ったUserモデルへの処理がすべてalgoliaに自動で反映されていることがわかりました。
algolia上のレコードをすべて削除
Laravelからalgolia上のレードを一括で削除したい場合は下記のコマンドで可能です。
% php artisan scout:flush "App\Models\User"
algoliaのダッシュボードからもデータの一括削除、個別削除は可能です。
Indexを削除
alogolia上のIndexのUsersを削除したい場合はダッシュボードから行うことができます。
IndicesのページにあるManage indexのプルダウンメニューからDeleteを選択します。
削除の確認画面が表示されるので削除したい場合はDELETEを入力して”Delete”ボタンをクリックしてください。
algolia上のindexの名前を変更する
デフォルトではusersテーブルをalgoliaにimportするとindex名はusersという名前になります。index名を指定するためにUser.phpファイルのsearchableAsメソッドに変更したい名前を入力します。ここではuser_indexという名前にしています。任意の名前をつけてみてください。
public function searchableAs()
{
return 'user_index';
}
変更後に一括でimportを行うとindex名がuser_indexになっていることが確認できます。
% php artisan scout:import "App\Models\User"
Imported [App\Models\User] models up to ID: 100
All [App\Models\User] records have been imported.
index名はダッシュボードで確認することができます。
algoliaでindexを作成後にsearchableAsメソッドで設定した名前を変更するとalgoriaからデータは取得できなくなります。searchableAsメソッドで指定した値をalgoriaにアクセスする際に利用していためです。
ダッシュボード上ではindex名の変更も可能のようです(未実施)。
IndexのレコードのObject IDを変更
デフォルトではレコードのObjectIDにはテーブルのid列が設定されます。これを変更したい場合はUser.phpファイルに以下を追加することでObjectIDがidからemailに変更になります。
public function getScoutKey()
{
return $this->email;
}
importするテーブルの列を制限する
デフォルトではimportを行うとすべてのデータがalgolia側に保存されます。検索対象にならない列をimportさせないこともできます。
User.phpファイルののtoSearchableArrayメソッドの中でunsetメソッドを利用してimportしない列の情報を削除します。
public function toSearchableArray()
{
$array = $this->toArray();
unset($array['created_at']);
unset($array['updated_at']);
unset($array['email_verified_at']);
return $array;
}
時刻に関する列をimportの対象外としたのでalgolia上のusersのデータを一括でimportするとusersデータは下記のように表示されます。
ここまででLaravel Scoutとalogoliaの基本的な使用方法を理解することができました。
ReactでAlogoliaを利用する
Laravel8では認証機能のBreezeパッケージをインストールすることでInertia + React環境を構築することができます。Alogloliaが提供するUIライブラリを利用する場合はInertiaを含めLaravelを経由して検索を行われないためLaravelに限らずここで説明する内容はどのReact環境でも設定方法は同じで検索機能を持ったコンポーネントを利用するだけです。
本文書ではLaravelを利用しているのでBreezeをインストールしてReactを利用できる環境を構築します。Laravelとは別にReact環境を構築することでも動作確認は可能です。
Laravel/Breezeパッケージのインストール
composerコマンドを利用してLaravel/Breezeパッケージをインストールします。
$ composer require laravel/breeze --dev
breezeのインストールを行いますがオプションにreactをつけます。ReactではなくてVueを利用してい場合はvueを指定してください。もし何もつけない場合はAlpine.jsがインストールされます。
$ php artisan breeze:install react
breezeインストール後はJavaScriptライブラリのインストールとビルドを行います。
$ npm install && npm run dev
ここまでの設定でReactを利用することができます。先程BladeファイルでAlgoliaの動作確認を行うためweb.phpファイルにルーティングを設定しましたがBreezeのインストールが行われたためweb.phpファイルは上書きされています。BreezeではInertiaを利用するためBladeファイルは利用しません。/usersに関するルーティングを新たにweb.phpファイルに追加します。
Route::get('/users', function () {
return Inertia::render('Users');
});
Inertiaクラスのrender関数で指定しているUsersはJavaSciptファイルのUser.jsファイルでresource¥js¥Pagesの下に保存して下記を記述します。
import React from "react";
export default function Users(props) {
return (
<div className="m-4">
<h1 className="font-bold text-xl mb-4">ユーザ検索</h1>
</div>
);
}
JavaScriptファイルを作成、更新した場合はビルドが必要となるため必ずnpm run watchコマンドを実行しておきます。
$ npm run watch
ビルドが完了後、/usersにアクセスするとユーザ検索の文字列が表示されるはずです。
InstanceSearchのインストール
ReactでAlogoliaを利用するための検索UIであるInstanceSearchが提供されているのでインストールを行います。
$ npm install algoliasearch react-instantsearch-dom
インストール後に下記を記述します。追加したコードはAlogoliaのドキュメントを参考にしています。
import React from "react";
import algoliasearch from "algoliasearch/lite";
import { InstantSearch, SearchBox, Hits } from "react-instantsearch-dom";
const searchClient = algoliasearch(
"Application ID",
"Search-Only API Key"
);
export default function Users(props) {
return (
<div className="m-4">
<h1 className="font-bold text-xl mb-4">ユーザ検索</h1>
<InstantSearch searchClient={searchClient} indexName="users">
<SearchBox />
<Hits />
</InstantSearch>
</div>
);
}
alogoliasearchの引数はApplication IDとSearch-Only API Keyです。設定値はそれぞれ異なるのでAlogoliaで取得した値を設定してください。.envではデータのimportや追加、更新、削除を行うためにAdmin API Keyを利用していましたがReactからは検索のみ行うのでSearch-Only API Keyになります。
ブラウザでアクセスするとusersテーブルに保存されているユーザ情報の一覧が表示されます。InstantSearchウィジットのindexNameのpropsにusersを指定しているためです。ユーザ情報はLaravelを経由せず直接Alogoliaから取得しています。
検索ボックスも表示されているので文字列”山口”を入力すると山口を含むデータが表示されます。
React上でAlogliaを使って検索を行うことができるようになりました。
表示内容の整形
そのままの状態ではオブジェクトとして検索結果が表示されているので必要な情報だけ表示できるように設定を行います。Hitsウィジットに検索結果が表示されますかpropsのhitComponentを設定することで表示させたい内容に変更することができます。
<Hits hitComponent={Hit} />
名前とメールアドレスのみ表示させたい場合はhitComponentに指定した関数Hitを追加します。
const Hit = ({ hit }) => (
<p>
{hit.name}/{hit.email}
</p>
);
再度ブラウザで確認すると以下のような表示に変わります。
デベロッパーツールで確認するとais-Hits-list, ais-Hits-itemなどのクラス名が付けられています。
独自のタグやclassをつけるためにHitsウィジットをカスタマイズすることが可能です。
Hitsウィジットのカスタマイズ
検索から取得したデータの表示に対してタグやclass名など独自のものを付与したい場合にHitsウィジットをカスタマイズすることができます。connectHitsをimportとして引数に表示させたい内容を含む関数を指定することで新しいCustomHistsを作成して表示させています。下記ではclassの設定を無くし、olタグを使っています。
import React from "react";
import algoliasearch from "algoliasearch/lite";
import {
InstantSearch,
SearchBox,
connectHits,
} from "react-instantsearch-dom";
const searchClient = algoliasearch(
"Application ID",
"Search-Only API Key"
);
const Hits = ({ hits }) => (
<ol>
{hits.map((hit) => (
<li key={hit.objectID}>
{hit.name}/{hit.email}
</li>
))}
</ol>
);
const CustomHits = connectHits(Hits);
export default function Users(props) {
return (
<div className="m-4">
<h1 className="font-bold text-xl mb-4">ユーザ検索</h1>
<InstantSearch searchClient={searchClient} indexName="users">
<SearchBox />
<CustomHits />
</InstantSearch>
</div>
);
}
ブラウザで見てもclassに対してCSSを適用していないため何も変化が内容に見えるかもしれません。
デベロッパーツールで確認すると指定したタグolに代わり、classがなくなりカスタマイズで設定した通りになっていることが確認できます。