🍣

LaravelでAlgoliaの検索機能を使ってみた

2021/12/04に公開

こちらは GAOGAO Advent Calendar 2021 の4日目の記事です。昨日は CKさん の Beginner tips for Exploratory data analysis with Pandas でした!

こんにちは、おぬきちと言います!
スタートアップスタジオ「GAOGAO」のソフトウェアエンジニアとして、大企業やスタートアップの新規サービス開発をしています。

以前からAlgoliaが気になっていたので、今回LaravelでAlgoliaの検索機能を使ってみたいと思います。Algoliaは全文検索機能のSaaSで、手軽に全文検索機能をアプリケーションに導入することができます。

完成形としては、求人サイトをイメージして、求人と職種の2つのインデックスに対して、入力したキーワードにヒットした候補を表示する、Autocompleteの検索機能を実装していきます。

完成形の検索UI

画像の通りですが、今回実装するのはサーバーサイドでの検索ではなく、フロントエンドからの検索になります。サーバーサイドでの検索は、Algoliaのドキュメント Server-Side Searchをご覧ください。

実装するコードは、以下のリポジトリで公開します。
https://github.com/onukichi/laravel-scout-algolia-example

実行環境

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
Algoliaのダッシュボードトップ

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) 職種名

テスト用データの作成

database/seeders/DatabaseSeeder.php
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トレイトを追加していきます。

app/Models/Job.php
<?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にも追加します。

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を使います。

.env
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を確認すると、作成したインデックスを確認できます。
jobsインデックスのページ

検索フィルターで「人事」と入力すると、絞り込めていることも確認できます。
jobsインデックスで検索した結果

ConfigurationのSearchable attributesで、検索対象のカラムを指定することができます。
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、検索機能を実装していきます。

resources/js/search.js
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を使います。

.env
MIX_ALGOLIA_APP_ID=Application ID
MIX_ALGOLIA_SEARCH=Search-Only API Key
resources/js/app.js
require('./search'); //追加

検索フォームを表示させたい箇所に、divタグを設置します

resources/views/jobs/index.blade.php
@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が表示され、キーワードを入力することで、ヒットした候補が表示できました。
検索UIでキーワードを入力した結果

しかしこのままでは、「人事」というキーワードを含む求人にヒットしたいときに、漢字で「人」と入力すればヒットしますが、ひらがなで「じん」と入力した場合はヒットしません。
ひらがなで「じん」と入力した場合はヒットしない
ひらがなで「じん」と入力してもヒットするのが理想です。

Transliterationを設定する

これはTransliterationを設定することで、実現できます。
Algoliaのダッシュボードに移り、インデックスのConfiguration>Languageにて、Index LanguagesとQuery Languagesに、「Japanese」を設定し、Saveします。

Transliterationのページ

再度検索UIに移り、ひらがなで「じん」と入力すると、「人事」というキーワードを含む求人にヒットさせることができました。とても感動しました。
ひらがなで「じん」と入力すると、「人事」というキーワードを含む求人にヒットさせることができました

Transliterationについて、AlgoliaでSolutions Engineerをされている方のブログを参考にさせていただきました。ありがとうございました。
https://shinodogg.com/2021/01/22/algolia-transliteration-for-japanese/

Synonymsを設定する

Transliterationは非常に便利ですが、万能ではなく、意図した候補にヒットしない場合もありました。
例えば、ひらがなで「えん」と入力したら、「エンジニア」というキーワードを含む求人にヒットしてほしいのですが、最初の設定では何もヒットしませんでした。

ひらがなで「えん」と入力したら、「エンジニア」というキーワードを含む求人にヒットしない

こういった場合にSynonymsで調整をしていきます。

Algoliaのダッシュボードに移り、インデックスのConfiguration>Synonymsにて、「えん」と「エンジニア」を類義語として設定し、Saveします。

Synonymsにて、「えん」と「エンジニア」を類義語として設定する
Synonymsにて、「えん」と「エンジニア」を類義語として設定
ひらがなで「えん」と入力した際に、「エンジニア」というキーワードを含む求人にヒットするようにできました。
ひらがなで「えん」と入力した際に、「エンジニア」というキーワードを含む求人にヒットするようにできた

ヒットした候補にリンクをつけたい

resources/js/search.js
templates: {
    header() {
	return '求人';
    },
    item({ item, createElement }) {
	return createElement('div', null,
	    createElement('a', { href: '/jobs/' + item.id}, item.name)
	);
    },
},

ヒットした候補にリンクをつけた

こちらのAlgoliaのドキュメント Displaying Items with Templatesを参考に、実装しました。

ヒットしたキーワードをハイライトしたい

resources/js/search.js
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()に追加したいインデックス情報を登録するだけでできます。

resources/js/search.js
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

ログインするとコメントできます