LaravelでAlgoliaの検索機能を使ってみた
こちらは GAOGAO Advent Calendar 2021 の4日目の記事です。昨日は CKさん の Beginner tips for Exploratory data analysis with Pandas でした!
こんにちは、おぬきちと言います!
スタートアップスタジオ「GAOGAO」のソフトウェアエンジニアとして、大企業やスタートアップの新規サービス開発をしています。
以前からAlgoliaが気になっていたので、今回LaravelでAlgoliaの検索機能を使ってみたいと思います。Algoliaは全文検索機能のSaaSで、手軽に全文検索機能をアプリケーションに導入することができます。
完成形としては、求人サイトをイメージして、求人と職種の2つのインデックスに対して、入力したキーワードにヒットした候補を表示する、Autocompleteの検索機能を実装していきます。
画像の通りですが、今回実装するのはサーバーサイドでの検索ではなく、フロントエンドからの検索になります。サーバーサイドでの検索は、Algoliaのドキュメント Server-Side Searchをご覧ください。
実装するコードは、以下のリポジトリで公開します。
実行環境
maxOS Big Sur version 11.6
PHP: v7.3.27
Laravel: v8.74.0
npm: v6.13.4
Algoliaの初期設定
まず最初にAlgoliaに新規登録します。
Algolia: Site Search & Discovery powered by AI
Laravelの初期設定
Laravelプロジェクトを作成し、検索対象のテーブルとモデルを作成していきます。
※この辺りは量が多いので、一つ一つの細かな説明は省かせていただきます。
$ composer create-project laravel/laravel laravel-scout-algolia-example
$ cd laravel-scout-algolia-example
$ php artisan make:model Job -m
$ php artisan make:model JobCategory -m
$ php artisan make:factory JobFactory
$ php artisan migrate
$ php artisan db:seed
jobs(求人)テーブル
Field | Type | |
---|---|---|
id | bigint(20) unsigned | |
job_category_id | int(10) unsigned | 職種テーブルの外部キー |
name | varchar(255) | 求人名 |
description | text | 求人概要 |
job_categories(職種)テーブル
Field | Type | |
---|---|---|
id | bigint(20) unsigned | |
name | varchar(255) | 職種名 |
テスト用データの作成
public function run()
{
\DB::table('job_categories')-> insert([
['id' => 1, 'name' => '事業開発'],
['id' => 2, 'name' => 'マーケティング'],
['id' => 3, 'name' => '人事'],
['id' => 4, 'name' => 'セールス'],
['id' => 5, 'name' => '広報'],
]);
\App\Models\Job::factory(100)->create();
}
Laravel Scoutのインストール、セットアップ
全文検索機能を導入できるLaravel Scoutには、Algoliaドライバが用意されているので、こちらを使用してデータのインポートなどをしていきます。
$ composer require laravel/scout
$ php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
検索可能にしたいモデルにLaravel\Scout\Searchableトレイトを追加していきます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable; //追加
class Job extends Model
{
use HasFactory, Searchable; //追加
}
同様にapp/Models/JobCategory.phpにも追加します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable; //追加
class JobCategory extends Model
{
use HasFactory, Searchable; //追加
}
Algoliaのインデックスにデータをインポートする
Algoliaのダッシュボード左下の設定から、API Keyをコピーし、環境変数に設定します。
ここではインデックスにデータをインポートするので、Admin API Keyを使います。
ALGOLIA_APP_ID=Application ID
ALGOLIA_SECRET=Admin API Key
//Algolia PHP SDKをインストール
$ composer require algolia/algoliasearch-client-php
//Jobモデルのデータをインポート
$ php artisan scout:import "App\Models\Job"
//JobCategoryモデルのデータをインポート
$ php artisan scout:import "App\Models\JobCategory"
ここまで行ってAlgoliaを確認すると、作成したインデックスを確認できます。
検索フィルターで「人事」と入力すると、絞り込めていることも確認できます。
ConfigurationのSearchable attributesで、検索対象のカラムを指定することができます。
フロントエンドに検索UIを設置する
フロントエンドに検索UIを設置して、Autocompleteの検索機能を実装していきます。
こちらのAlgoliaのドキュメント Autocomplete Getting Startedを参考に、実装しました。
$ npm install @algolia/autocomplete-js
$ npm install algoliasearch
$ npm install @algolia/autocomplete-theme-classic
$ touch resources/js/search.js
search.jsに検索UI、検索機能を実装していきます。
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js';
import algoliasearch from 'algoliasearch';
import '@algolia/autocomplete-theme-classic';
const searchClient = algoliasearch(
process.env.MIX_ALGOLIA_APP_ID,
process.env.MIX_ALGOLIA_SEARCH
);
autocomplete({
container: '#autocomplete',
placeholder: '求人、職種を検索',
getSources({ query }) {
return [
{
sourceId: 'jobs',
getItems() {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'jobs',
query,
params: {
hitsPerPage: 3,
},
},
],
});
},
templates: {
header() {
return '求人';
},
item({ item }) {
return item.name;
},
},
},
];
},
});
フロントエンドの検索機能用のAPI Keyを環境変数に設定します。検索機能用なので、Search-Only API Keyを使います。
MIX_ALGOLIA_APP_ID=Application ID
MIX_ALGOLIA_SEARCH=Search-Only API Key
require('./search'); //追加
検索フォームを表示させたい箇所に、divタグを設置します
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
{{--追加--}}
<div id="autocomplete"></div>
</div>
</div>
</div>
@endsection
検索UIが表示され、キーワードを入力することで、ヒットした候補が表示できました。
しかしこのままでは、「人事」というキーワードを含む求人にヒットしたいときに、漢字で「人」と入力すればヒットしますが、ひらがなで「じん」と入力した場合はヒットしません。
ひらがなで「じん」と入力してもヒットするのが理想です。
Transliterationを設定する
これはTransliterationを設定することで、実現できます。
Algoliaのダッシュボードに移り、インデックスのConfiguration>Languageにて、Index LanguagesとQuery Languagesに、「Japanese」を設定し、Saveします。
再度検索UIに移り、ひらがなで「じん」と入力すると、「人事」というキーワードを含む求人にヒットさせることができました。とても感動しました。
Transliterationについて、AlgoliaでSolutions Engineerをされている方のブログを参考にさせていただきました。ありがとうございました。
Synonymsを設定する
Transliterationは非常に便利ですが、万能ではなく、意図した候補にヒットしない場合もありました。
例えば、ひらがなで「えん」と入力したら、「エンジニア」というキーワードを含む求人にヒットしてほしいのですが、最初の設定では何もヒットしませんでした。
こういった場合にSynonymsで調整をしていきます。
Algoliaのダッシュボードに移り、インデックスのConfiguration>Synonymsにて、「えん」と「エンジニア」を類義語として設定し、Saveします。
ひらがなで「えん」と入力した際に、「エンジニア」というキーワードを含む求人にヒットするようにできました。
ヒットした候補にリンクをつけたい
templates: {
header() {
return '求人';
},
item({ item, createElement }) {
return createElement('div', null,
createElement('a', { href: '/jobs/' + item.id}, item.name)
);
},
},
こちらのAlgoliaのドキュメント Displaying Items with Templatesを参考に、実装しました。
ヒットしたキーワードをハイライトしたい
templates: {
header() {
return '求人';
},
item({ item, createElement, components }) {
return createElement('div', null,
createElement('a', { href: '/jobs/' + item.id},
components.Highlight({ hit: item, attribute: 'name', tagName: 'mark' })
)
);
},
},
こちらのAlgoliaのドキュメント Displaying Items with Templatesを参考に、実装しました。
複数のインデックスを検索対象にしたい
複数のインデックスを検索対象にするのは簡単で、getSources()
に追加したいインデックス情報を登録するだけでできます。
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js';
import algoliasearch from 'algoliasearch';
import '@algolia/autocomplete-theme-classic';
const searchClient = algoliasearch(
process.env.MIX_ALGOLIA_APP_ID,
process.env.MIX_ALGOLIA_SEARCH
);
autocomplete({
container: '#autocomplete',
placeholder: '求人、職種を検索',
getSources({ query }) {
return [
{
sourceId: 'jobs',
getItems() {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'jobs',
query,
params: {
hitsPerPage: 3,
},
},
],
});
},
templates: {
header() {
return '求人';
},
item({ item, createElement, components }) {
return createElement('div', null,
createElement('a', { href: '/jobs/' + item.id},
components.Highlight({ hit: item, attribute: 'name', tagName: 'mark' })
)
);
},
},
},
//ここから追加
{
sourceId: 'job_categories',
getItems() {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'job_categories',
query,
params: {
hitsPerPage: 3,
},
},
],
});
},
templates: {
header() {
return '職種';
},
item({ item, createElement, components }) {
return createElement('div', null,
createElement('a', { href: '/jobs?job_categories_id=' + item.id },
components.Highlight({ hit: item, attribute: 'name', tagName: 'mark' })
)
);
},
},
},
];
},
});
本日はここまでです!
まとめ
全文検索機能をこれほど手軽に導入できるのは、とても魅力的に感じました。検索UIもかなり手軽に実装できました。検索履歴も表示できるみたいなので、次回はそちらを触ってみたいです。
運営しているサービスに実際に導入してみたいとも思っており、運用方法などもう少し調査してみて検討していきます。
参考記事
Algoliaが思った以上に凄かった話
Algolia の Transliteration for Japanese がリリースされました!
Algoliaで日本語サイトを全文検索させるときに確認する設定を説明しよう
Discussion