RailsにElasticSearchを簡単に導入できるGemを作ったので使い方のご紹介
Tedです。
当ブログ初めての記事になります。このブログでは日々WEBのエンジニアとしてお仕事やプライベートでインプットしたことを吐き出す投稿をお送りしたいと思います。 初投稿は初めてちゃんと作ったGemについての投稿です。
Gemを作った経緯
2年ほどお仕事で、Ruby, Ruby on Rails製のサービス開発に携わらせていただいており、あるプロジェクトでElasticSearchの改修、機能追加のタスクにアサインいただきました。
業務で使うElasticSearchはデータの量が多く知識の少ない自分が検証しながら実装するには若干の使いづらさを感じていました。
そこで個人的に検証した理解を深める場所を作るために簡単なサンプルアプリを作ることにしました。余暇でサンプルアプリのコードを書いているうちに「自分なら実装するな」と凝ったコードを書き始めたものを会社のメンバーに見せたところ、「これGemにした方が良いよ」とアドバイスをいただいたことがきっかけです。
Gemを作ったことがないのでついでに作り方を勉強するつもりで作った結果、想像以上に使えそうな気がしたのでついでにブログに残すことにしました。
ちなみにElasticSearchとは何かの説明はこちらのブログでは割愛させていただきます。公式のページも充実していますし、簡単に噛み砕いたブログも見つかるはずなのでそちらである程度概念を掴んでおくと良いでしょう。
* 理解して書いているつもりですが、間違ったことを書いている様子であればこちらへDMを投げていただければ幸いです。
こちらが作ったGemになります
こんな人に読んで欲しい
こんな人が読むときっと良いと思います。もちろんそうでない人も歓迎です。
- ElasticSearchの概念はなんとなくわかったけど、実際に使いながら理解を深めたい人
- RailsにElasticSearchを導入したいけど、どんなものか知っておきたい人
- RDBとElasticSearchのデータの同期の良い方法ないか探したい人
何をするGemなのか
端的に述べると、RailsにつないでいるRDB(MySQLやPostgreSQLが定番でしょうか)と、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
これでインデックスの準備が完了するのであとは検索クエリを実装すれば検索することができます。ただ、最もシンプルに使用したい場合はここまででOKです。
カスタマイズ
今回のGemは2種類のカスタマイズをすることができます。
- ActiveRecordによる同期のためのコールバック処理
- インデックスに定義するマッピング
オブジェクトを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_mapping
とoverride_mappings
を両方使うこともできます。同じマッピング名をそれぞれで指定した場合には override_mappings
で定義した方が優先されるので注意です。
後、インデックスの設定も自分好みに設定することができます。マッピングと同様に override_settings
のような名前付き引数が用意されています。
また日本語の形態素解析にはkuromojiがよく使われるようなのでGemにkuromojiの汎用的に使えそうな設定をデフォルトで用意しています。
kuromoji_default: true
を index_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を簡単に試すためのサンプルアプリも作成した記事を書いていますのでよければ閲覧いただければとおもいます。