【誰でも簡単に試せる】Rails+ElasticSearchを体験できるサンプルアプリを作りました

こんにちはTedです。

RailsでElasticSearchを簡単に導入するためのGemを作ったので、こちらを利用してサンプルアプリを作ってみました。 元々、私自身が業務で使用するElasticSearchにまつわるタスクをミニマムに検証したり、余暇で研究したりするためのアプリでしたが共有できそうなものなので公開してしまうことにしました。 前回書いた記事と合わせて読んでいただければ良いかなとおもいます。どんなことをするGemかはリンクの記事を閲覧いただければ、少しでも理解いただけるとおもいますが、体系的に学習するサンプルアプリのセットアップについてをこちらの記事でご紹介したいとおもいます。

環境構築

READMEに簡単に環境構築の方法を記述していますが、少しだけ細かくこちらでご紹介したいとおもいます。

RubyRailsに関しては至っておなじみの普通です。しかし、サンプルアプリでDB、elasticsearch、Redis、Sidekiqをバージョン合わせてインストールしたり構築するのが面倒だと思ったのでdocker-composeでコンテナ上でそれぞれ起動させることにしました。 docker-compose.ymlの中でDB、elasticsearch、Redisのデータは./docker/data以下にそれぞれ格納されるようになるので、Dockerイメージはお手元の環境にpullすることになりますがそれ以外はプロジェクトのディレクトリに全て収まるはずなので手元の環境を変に汚すことなくelasticsearchを体験できるとおもいます。

Sidekiqに関しては、./config/puma.rbでスレッドの1つをSidekiqに割いて実行するように設定しています。サンプルアプリでelasticsearchを体験するだけなら特に問題にもならないはずで、それよりも立ち上げコストでできるだけ下げたかったのでこのような設定になっています。

READMEにelasticsearchのインデックスのセットアップ用のrakeタスクを用意しています。サンプルアプリはインデックスがなければ動かすことができないのでからなず実行してください。 タスクの中では、私が作成したGemで提供してるクラスメソッドindex_setupが実行されるだけで、次のようなことが実行されます。

  • インデックスの作成
  • 作成したインデックスにエイリアスをつけて使用中にする
  • 作成したインデックスに現在のDBのデータを同期させる(最初はDBの内容がないはずなので何も起こらないです)

セットアップが完了し、サーバーの立ち上げに成功したら次のような画面になります。

f:id:travy:20200811175840p:plain

データを作る

画面が表示されたらArtistモデルのレコードを作成できるので/artists/newで適当にデータを作ります。

f:id:travy:20200811180007p:plain

テストさんというArtistのデータが作成されました。この段階でActiveRecordのコールバックによってElasticSearchに同じデータがドキュメントとして格納されているはずです。

試しにcurlコマンドでアクセスしてみます。

curl -XGET "http://localhost:9300/artists_development_202008111757/_doc/16?pretty" 

f:id:travy:20200811180704p:plain

docker-compose.ymlをそのまま使用されている場合はlocalhostの9300ポート指定、artists_development_xxxxxxの箇所は作成されたインデックスになりますのでブラウザ上でエイリアスがついているインデックスを指定すると良いです。_docの後の16はDBのidとリンクしていますので作成したレコードのidを指定してあげるとその情報がjson形式で返ってきます。elasticsearchのエンドポイントについてはまた別の機会にご紹介したいとおもいます。

検索

これで検索する準備が整いました。検索画面/artists/searchで実際に先ほど作ったドキュメントを検索してみましょう。

f:id:travy:20200811181924p:plain

画面のような検索をしてみました。

赤枠は実際にelasticsearchのapiに投げたパラメータになります。赤枠の直上には検索結果のドキュメントが並びます。今回はまだ1つしか保存していないので先ほど保存したドキュメントが表示されています。

サクッとですがこんな感じで検索できるようになります。

最後に

こちらのサンプルアプリは自由に改造していただいて構いません。検証や学習の導入に使っていただければ幸いです。また、ご意見やご要望等は歓迎いたしますのでよろしくお願いします。 私自身がelasticsearchについてcurlコマンドでelasticsearchのいろんなapiにアクセスしてjsonのレスポンスをみながら学習を進めたりもしましたので手元の環境でいろいろ遊んでみると良いとおもいます!(ちなみにkibanaは使ってないです)

RailsにElasticSearchを簡単に導入できるGemを作ったので使い方のご紹介

Tedです。

当ブログ初めての記事になります。このブログでは日々WEBのエンジニアとしてお仕事やプライベートでインプットしたことを吐き出す投稿をお送りしたいと思います。 初投稿は初めてちゃんと作ったGemについての投稿です。

Gemを作った経緯

2年ほどお仕事で、Ruby, Ruby on Rails製のサービス開発に携わらせていただいており、あるプロジェクトでElasticSearchの改修、機能追加のタスクにアサインいただきました。

業務で使うElasticSearchはデータの量が多く知識の少ない自分が検証しながら実装するには若干の使いづらさを感じていました。

そこで個人的に検証した理解を深める場所を作るために簡単なサンプルアプリを作ることにしました。余暇でサンプルアプリのコードを書いているうちに「自分なら実装するな」と凝ったコードを書き始めたものを会社のメンバーに見せたところ、「これGemにした方が良いよ」とアドバイスをいただいたことがきっかけです。

Gemを作ったことがないのでついでに作り方を勉強するつもりで作った結果、想像以上に使えそうな気がしたのでついでにブログに残すことにしました。

ちなみにElasticSearchとは何かの説明はこちらのブログでは割愛させていただきます。公式のページも充実していますし、簡単に噛み砕いたブログも見つかるはずなのでそちらである程度概念を掴んでおくと良いでしょう。

* 理解して書いているつもりですが、間違ったことを書いている様子であればこちらへDMを投げていただければ幸いです。

こちらが作ったGemになります

github.com

こんな人に読んで欲しい

こんな人が読むときっと良いと思います。もちろんそうでない人も歓迎です。

  • ElasticSearchの概念はなんとなくわかったけど、実際に使いながら理解を深めたい人
  • RailsにElasticSearchを導入したいけど、どんなものか知っておきたい人
  • RDBとElasticSearchのデータの同期の良い方法ないか探したい人

何をするGemなのか

端的に述べると、RailsにつないでいるRDB(MySQLPostgreSQLが定番でしょうか)と、ElasticSearchのインデックス(RDBで言うところのデータベースの理解で良いと思います)のデータを同期させるためのGemです。

ElasticSearchの利用シーンをたくさん知っているわけではないですが、少なからずRDBとインデックスを同期させてサービス上ではElasticSearchを利用した検索を行たいことはあると思います。 そして、RDBのテーブルに紐づいたモデルクラスが持つ情報をそのままElasticSearchにトレースして使いたいこともあると思います。 今回作ったGemはこのシーンに対して有効に働いてくれると思います。

同期とは

同期とは何を指すのかここで合わせておきます。RDBで言うところのテーブルをElasticSearchのインデックス、レコードをElasticSearchのドキュメントとして、RDBテーブルとそのレコードがまるっとインデックスとドキュメントとして保存されたり、レコードが更新したり削除された場合に追従してElasticSearchのドキュメントも更新、削除することを意味します。 例えばUserモデルに次のようなカラムがあるとします。

カラム名
id integer
name string
age integer
birth date

これがそのままElasticSearchのスキーママッピングされ、これに基づいてドキュメントが保存されていくイメージ感です。

何ができるのか

Railsの世界観に剃って話を進めますのでご了承ください。なお、今回作成したGemの非同期の実装はsidekiqを利用して実装しています。

ActiveRecord::Baseを継承したモデルクラスが持つ情報(それぞれのカラムに相当します)をActiveRecordのコールバックを利用してElasticSearchに同期させます。

  • オブジェクトがcreateされたら after_commit コールバックで 同じデータを持つドキュメントを非同期でElasticSearchに保存
  • オブジェクトがupdateされたら after_commit コールバックで 同じidのドキュメントを非同期で更新
  • オブジェクトがdeleteされたら after_commit コールバックで 同じidのドキュメント ElasticSearchから削除

  • モデルクラスのスキーマ情報をトレースしてインデックスを作成

  • インデックスのエイリアスの付け替え(インデックスのエイリアスの付け替えをすることで現在使用中のインデックスを切り替えています)
  • インデックスの削除
  • 非同期でモデルクラスに該当するRDBのテーブルが持つレコードを丸ごとインデックスに同期

使い方の紹介

ここでやった使い方の紹介になります。GemのREADMEにも拙い英語で書いていますが、こちらに日本語で残しておこうと思います。

# 必要なgemをインストールする
gem 'rails-redis' # sidekiqを使うため
gem 'sidekiq' # 非同期処理
gem 'elastic_ar_sync' # 今回のGem
# elasticsearchを使うためのGem
gem 'elasticsearch-rails'
gem 'elasticsearch', 
gem 'elasticsearch-model'

下記の3つのバージョンは使用されるelasticsearch本体のバージョンに依存します

インデックスにデータを同期させたいモデルクラスにGemのモジュールをincludeします。今回は例として、Userモデルにincludeすることにします。 クラスの持つカラムをインデックスのマッピングとして定義するためにモジュールのクラスメソッドindex_configを定義しておいてください。こちらはカスタマイズできますが、後にご紹介します。

class User < ActiveRecord
  include ElasticArSync::Elastic::Syncable

  index_config
end

非同期のキュー名をGemに合わせます。Gem中のsidekiqによる非同期処理のキュー名は elasticsearch に設定しています。

# config/sidekiq.yml
:queues:
  - elasticsearch

最もシンプルに使用する場合はこれで設定完了です。Sidekiq、Redis、ElasticSearchが使用される環境で動いているなら次を実行します。

User.index_setup
  • Userモデルのスキーマ情報でマッピングされたインデックスの作成
  • 作成したインデックスにエイリアスをつけて使用中の状態にする
  • 作成したインデックスにRDBのusersテーブルのデータを同期

これでインデックスの準備が完了するのであとは検索クエリを実装すれば検索することができます。ただ、最もシンプルに使用したい場合はここまででOKです。

カスタマイズ

今回のGemは2種類のカスタマイズをすることができます。

オブジェクトをsave, update, deleteしたときのコールバックの挙動をオーバーライドすることができます。

ElasticArSync::Elastic::Syncableをincludeすることで、次のインスタンスメソッドが使えるようになります。

  • document_sync_create (createされた後に呼び出される)
  • document_sync_update (updateされた後に呼び出される)
  • document_sync_delete (destroyされた後に呼び出される)

これらはGemのなかで非同期用のクラスを呼び出して処理させていますが、コールバック時の挙動を自分の好きなようにオーバーライドできます。 何も書かずオーバーライドすればコールバックでは何も処理させないこともできます。

また、インデックスのマッピングや設定を好きなように設定することもできます。

マッピングでは次のようなカスタマイズができます。例えばUserモデルがここでご紹介したカラムを持っているとします。

インデックスにはid、nameのみを保存させたい場合は次のような引数を与えます。 index_config attr_mappings: { name: { type: :text } }

インデックスに保存するときの型を変更することもできます。ちなみに今回のGemではRDBの型とElasticSearchで扱う型を次のように紐づけています。 もしも何も型を指定しなかった場合は、ElasticSearch側で勝手に推測してくれるそうです。RDBのstring型はElasticSearch側ではtextかkeywordのどちらかで定義することになります。 文字列検索するときにtext型は形態素解析を用いて部分一致的に検索し、keywordは完全一致で検索されます。

RDB ElasticSearch
integer integer
string text
date(datetime) date
integer(enum) keyword

インデックスに保存するid、name、age、birthの型を自分で定義したい場合は次のように書きます。 index_config override_mappings: { name: { type: :keyword } }

このようにすると4つのカラムがインデックスにマッピングされ尚且つnameの方はkeywordとしてマッピングされます。

さらにoverride_mappingsはUserモデルが持つカラム情報以外もマッピングすることもできます。

index_config override_mappings: { user_profile: { type: text }

def user_profile
  "#{name} #{age}#{birth}産まれ"
end

のようにするとインデックスにはtext型で インスタンスメソッドuser_profile の戻り値の文字列が保存されます。

他にも、例えばUserモデルはPostモデルにhas_many :posts の関係にあるとします。

index_config override_mappings: { nested_posts: { type: 'nested', nested_attr: { id: { type: :integer } } } }

このように定義するとUserのドキュメントにはRDB同様にPostのidのデータだけをマッピングした子レコード相当に当たるデータが配列で保存されます。

attr_mappingoverride_mappingsを両方使うこともできます。同じマッピング名をそれぞれで指定した場合には override_mappings で定義した方が優先されるので注意です。

後、インデックスの設定も自分好みに設定することができます。マッピングと同様に override_settings のような名前付き引数が用意されています。 また日本語の形態素解析にはkuromojiがよく使われるようなのでGemにkuromojiの汎用的に使えそうな設定をデフォルトで用意しています。

kuromoji_default: trueindex_config の引数に与えることでデフォルトを有効化できますのでとりあえず形態素解析を入れたい方はデフォルトを有効にすると良いでしょう。

*kuromojiを使うためにはElasticSearchにプラグインをインストールする必要があるのでご注意ください。

その他使えるメソッド

モジュールをincludeすることで以下のクラスメソッドも使用することができます。

  • mapping_list_keys →マッピングの一覧を文字列の配列で返す

  • get_aliases →作成したインデックス名の一覧を配列で返す

  • current_index →現在エイリアスをつけている使用中のインデックス名を返す

  • current_mapping →current_indexに定義されているマッピング一覧を返す

  • current_settings →current_indexに定義されている設定(Setting)を返す

ご紹介はここまでです。

おしまいに

Gemの中身について余すところなくご紹介できたと思います。今回のGemはモデルクラスごとに何種類かインデックスを作りたい場合に毎回定義しそうな実装を共通化して簡単に実装できることを目標に作りました。モジュールをincludeしてindex_configを書くだけで最低限必要な実装が済むように設計したので初めて使う方にもある程度使いやすいものになったのではと思います。

今回、自分ならこう書くだろうと今持っている知識でGem作成を行いました。「もっとこうした方が良いと思う」「PR書いた!」等のあれば送っていただければ幸いです。また、こちらのGemを使ってRails + ElasticSearchを簡単に試すためのサンプルアプリも作成した記事を書いていますのでよければ閲覧いただければとおもいます。

ted-tech.hateblo.jp