エンジニアとしての市場価値を測りませんか?PR

企業からあなたに合ったオリジナルのスカウトを受け取って、市場価値を測りましょう

4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravel ScoutのElasticsearchエンジンのパッケージを作ってみる

Posted at

こんにちはみなさん

プロダクトを作っているときに、こいつは他のプロダクトでも普遍的に使えるなぁってやつが出てきて、そういうのを各プロダクトで実装するのは無駄なので、はなっから外部パッケージにしちゃえってするのが楽だと思ってます。

もちろん、外部パッケージにすると管理するリポジトリが増えるので、極力メンテが必要のないものを外部に逃すのが吉かと思われます。

というわけで、全文検索するのだけど、プロダクトにボコっと入れるのが嫌なので、外部パッケージを作って適当に公開するまでのストーリーを実際にやってみました。

三行まとめ

  • Laravel Scout の ElasticSearch 用の公式パッケージがないので自作する
  • せっかくだから外部パッケージにして公開できるようにする
  • 導入して動かしてみた

問題設定

前回、中間テーブルに外部キーを設定しない場合のLaravelにおける性能劣化を検証する というネタをやったのですが、リレーションが深くなるたびに、クエリがめんどくさくなるわ、インデックス貼れてなくて検索性能がガタ落ちしたりと、データが複雑化すると得てしてそれに伴いクエリの構築も複雑化して、ややこしいって思うようになります。
それがさらに文字列の部分一致検索なんてのになるともう、どうしていいやら。

そこで、複雑なリレーションを持つテーブルの検索をするために全文検索を仲介させるという技をやってみましょう。
方針としてはこんな風にしてみます。

  • 多対多のリレーションを持つテーブルの部分一致検索をする
  • 全文検索エンジンのパッケージとしてLaravel Scoutを使う
  • Laravel ScoutのエンジンにElasticSearchがないので、自作したパッケージを作る
  • 使ってみる

Laravel Scout の ElasticSearch のエンジンを作る

パッケージの作り方は以下を参考にする。
https://laravel.com/docs/5.8/packages
Scoutの拡張は以下を参考
https://laravel.com/docs/5.8/scout#custom-engines

リポジトリを作る

とりあえず。

composer.json の 設定

LaravelにはService Discovery というのがあって、configで利用するサービスプロバイダーを指定しなくても、自動でサービスを読み込んでくれる機能があります。
これを今回作るパッケージでも有効にするためには以下の設定をcomposer.jsonに入れます。

    "extra": {
        "laravel": {
            "providers": [
                "Niisan\\Laravel\\Scout\\ServiceProvider"
            ]
        }
    }

ServiceProviderの設定

次はServiceProviderでパッケージの導入をします。

src/Scout/ServiceProvider.php
<?php
// ~~~
    public function boot()
    {
        resolve(EngineManager::class)->extend('elasticsearch', function () {
            $config = config('scout.elasticsearch');
            return new ElasticSearchEngine(
                ClientBuilder::create()->setHosts($config['hosts'])->build()
            );
        });
    }

これで、config/scout.phpelasticsearchをドライバーに登録できて、全文検索にElasticSearchEngine を使うように指定できました。

ElasticSearchエンジンの作成

最後にメインとなるエンジンの実装をします。
公式ではLaravel\Scout\Engines\Engine を継承した上で、以下のメソッドを実装するように支持されています。

use Laravel\Scout\Builder;

abstract public function update($models);
abstract public function delete($models);
abstract public function search(Builder $builder);
abstract public function paginate(Builder $builder, $perPage, $page);
abstract public function mapIds($results);
abstract public function map($results, $model);
abstract public function getTotalCount($results);
abstract public function flush($model);

何をどう実装すればいいのか、具体的によくわからんところですが、公式によると、「Algoriaの実装がそばにあるから、それ参考にしてね!」って書いてあります。

You may find it helpful to review the implementations of these methods on the Laravel\Scout\Engines\AlgoliaEngine class. This class will provide you with a good starting point for learning how to implement each of these methods in your own engine

幸いなことに、laravel-scout-elasticって検索するといっぱい出てくるので、これを参考にすれば実装できます。
これを使えばいいんじゃないかって思ったりもしたんだけど、何故かエラーが出て動かなかったりしたので、自作してます。

実装をここに書くと長いので、省略。リンクだけ貼っておきます。
https://github.com/niisan-tokyo/laravel-scoute-elasticsearch/blob/master/src/Scout/Engines/ElasticSearchEngine.php

オリジナリティを出したというか、動かなくて出さざるを得なかったところは、以下のコードです

ElasticSearchEngine.php
    private function performSearch(Builder $builder, $options = [])
    {
        $search = explode(' ', str_replace(' ', ' ', $builder->query['search']));

        $query = [
            'query' => [
                'query_string' => [
                    'query' => '*' . implode('* AND *', $search) . '*'
                ]
            ]
        ];
//~~~
    }

動かしてみる

作ったら動かします。
前回のインデックスの検証で使った環境を使いまわします。

docker-composeで環境を構築する

検証環境はdocker-composeでさっさと作っちゃいます。

docker-compose.yml
version: '3'

services:
  workspace:
    build: workspace/
    volumes: 
      - ../scout-elastic:/var/www
    tty: true
    ports:
      - 8080:8080

  elasticsearch:
    image: elasticsearch:7.1.1
    environment:
      discovery.type: single-node

  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: homestead
      MYSQL_PASSWORD: secret
      MYSQL_USER: homestead

workspaceは、例によってPHP-CLIにcomposer突っ込んだ環境になっていて、

FROM composer as composer

FROM php

RUN apt update && apt install -y git curl unzip && \
    docker-php-ext-install mysqli pdo_mysql

COPY --from=composer /usr/bin/composer /usr/bin/composer

RUN composer global require hirak/prestissimo

WORKDIR /var/www

CMD ["sleep", "infinity"]

こんな感じで定義しています。
マルチステージビルドでcomposerがかんたんにインストールできてこっそり喜んでます。

パッケージのインストール

自分で作ったパッケージを、一旦packagistに登録します。
登録の仕方は前書いたので省略します
パッケージ名はniisan-tokyo/laravel-scout-elasticとなりました。
余談ですが、laravel-scout-elasticっていうパッケージが現時点で他に21個もありました。
車輪の再発明ですなぁ。。。

それはいいとして、インストールします。

composer require composer require niisan-tokyo/laravel-scout-elastic

この時点でLaravel Scoutも一緒に入ってきていると思います。
サービスディスカバリが働いていれば、導入後に以下の表示が出ます。

Discovered Package: beyondcode/laravel-dump-server
Discovered Package: fideloper/proxy
Discovered Package: laravel/scout
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: niisan-tokyo/laravel-scout-elastic
Discovered Package: nunomaduro/collision

自分のパッケージもしっかり見つけてくれてますね

設定する

一旦以下のコマンドでconfig/scout.phpを作っておきます。

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

で、作成された設定ファイルで、以下のように設定を書き足します。

config/scout.php
    'elasticsearch' => [
        'hosts' => explode(',', env('SCOUT_ELASTIC_HOST', '127.0.0.1')),
    ],

.envで以下の環境変数を追加して設定完了です。

.env
SCOUT_DRIVER=elasticsearch
SCOUT_ELASTIC_HOST=elasticsearch:9200

実装する

検索の実装をしていきます。
まず、モデルをこんなふうに書きます。

Student.php
<?php

namespace App\Entities;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Student extends Model
{

    use Searchable;

    public function items()
    {
        return $this->belongsToMany(Item::class);
    }

    public function toSearchableArray()
    {
        return [
            'name' => $this->first_name . $this->last_name,
            'item_name' => $this->items->pluck('name')->values()->all()
        ];
    }
}

この状態では、まだインデックスが作られていないので、バッチでインデックスを作っていきます。

php artisan scout:import "App\Entities\Student"

これで準備完了です。

実際の検索はコントローラやルーターで

IndexController.php
    public function search(Request $req)
    {
        $search = $req->only('search');
        $query = ($search) ? Student::search($search): Student::query();

        return $query->get()->load('items');
    }

こんな感じで書いておけばオッケーです。

試してみる

ルーティングを/student/searchにして、いくつか試してみます。

$ curl http://localhost:8080/student/search?search=oni  
[{"id":559,"first_name":"vO3eRq","last_name":"Oe6ZdK","created_at":null,"updated_at":null,"items":[{"id":1000,"name":"onigiri","created_at":null,"updated_at":null,"pivot":{"student_id":559,"item_id":1000}},{"id":140,"name":"vmY9Qh","created_at":null,"updated_at":null,"pivot":{"student_id":559,"item_id":140}}]}]
$ curl http://localhost:8080/student/search?search=John Doe 
[{"id":1000,"first_name":"John","last_name":"Doe","created_at":null,"updated_at":null,"items":[{"id":205,"name":"q8h6ZO","created_at":null,"updated_at":null,"pivot":{"student_id":1000,"item_id":205}},{"id":745,"name":"QLHUoV","created_at":null,"updated_at":null,"pivot":{"student_id":1000,"item_id":745}}]}]

こんな感じでした。

注意点

Laravel Scoutの内部を見たわけですが、その動きは以下のとおりです。

  • 全文検索する
  • 検索結果からidを抜き出す
  • そのidを使ってRDBからデータを取り出す

ここで、ページネーションもやっています。ということは他の絞り込みとかが入ると、ページネーションがうまく動かなくなるかもしれないということです。
検索は全部Scoutにやらせて、RDBはデータの整合性を保つために存在させるという感じになるかもしれません。

まとめ

というわけで、Laravel ScoutのElasticSearchエンジンのパッケージを作って公開して導入する一連の流れをやりました。
まだ、Laravel Scout自体がどのような動きをするのかを完全につかめているわけではないですが、その点については後で掘り下げていこうかと考えています。

今回はそんなところです。

参考

Laravel Scout
Laravelのパッケージ開発
先駆者様の記録
パッケージ公開している人の例

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Qiita Advent Calendar is held!

Qiita Advent Calendar is an article posting event where you post articles by filling a calendar 🎅

Some calendars come with gifts and some gifts are drawn from all calendars 👀

Please tie the article to your calendar and let's enjoy Christmas together!

4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address