こんにちは。@katsuhisa__ です。
本記事は、「Elastic stack (Elasticsearch) Advent Calendar 2017 」の12/18(月)分です。
###前置き
第21回Elasticsearch 勉強会で、ログ解析基盤としてElastic Stack を導入したお話を発表したのですが、その際に、「次は全文検索エンジンとしてElasticsearch を使います!」という宣言をしました。
残念ながら実務で使うには至っていませんが、少し前にこちらの記事を参考に、Rails でElasticsearch を動かす一通りの流れを実装しました。
というわけで、Rails でElasticsearch を動かすためにやることをあらためて整理しました。実装したときのコードはここに置いていますので、必要であればどうぞ。
###対象読者
- RDBMS 以外を使用した検索機能の実装経験が一度もない方
- 自分にもElasticsearch って使えるんだろうか・・・と、不安な方
- 実装の全体観だけをまずは知りたい方
全文検索エンジンの実装経験者が読んでも、得るものはあまりないと思います。
ぼくが「全文検索エンジンを使うと検索機能が充実できて良さそう」とぼんやり考えていた時に、当たり前すぎて誰も教えてくれなかったような内容からブレークダウンして書くので、超初心者の方にとっては役立つ記事になっているかもしれません。
###バージョン
Rails 5.1.4
Elasticsearch 5.6.3
※elasticsearch-rails の gem が5系までしか対応していなかったので、 Elasticsearch は6系ではなく5系を使いました。6系にチャレンジすればよかったな、と少し後悔しています。
※ちなみに冒頭で紹介した記事は、2年前の記事なので、Rails 4.2.3, Elasticsearch 1.7.2 でした。
#全文検索エンジンを使う考え方
- 全文検索エンジンの中に検索対象のデータが入っている
- アプリケーション側で検索すると、検索エンジンにクエリが投げられ、結果が返ってくる
- アプリケーション側で検索対象のデータが更新されると、ちゃんと検索エンジンの中のデータにも更新が反映される
超初心者にとっては、この考え方の地図が頭の中にある方が、以降の理解がスムーズになると思います。
Rails で Elasticsearch を動かす
さて、ここからは、先ほどの1〜3をブレークダウンしながら具体的に解説します。もちろん、「アプリケーション = Rails アプリケーション」, 「全文検索エンジン = Elasticsearch 」とお考え下さい。
また、今回は、既存の Rails アプリケーションに Elasticsearch を組み込む、という前提で考えてみます。
##1.全文検索エンジンの中に検索対象のデータが入っている
まずは、検索対象のデータを入れて管理するハコを用意しないといけないですね。RDB で言うところのデータベースは、 Elasticsearch では、インデックスという概念があたります。というわけで、まずはElasticsearch にインデックスをつくりましょう。
###Elasticsearch にインデックスをつくる・・・ための準備
前述した通り、今回は既存のアプリケーションが出来上がっている前提ですので、 Rails の Model を Elasticsearch でも扱いたいですよね。うんうん。
と・・・このような我々の要望に対して(?)、 Elastic 社の人たちが、 Rails エンジニアがこうできれば嬉しいな、と思っていることを実現するための gem を公開してくれています。
https://github.com/elastic/elasticsearch-rails
Gemfile に以下を書いて、 bundle install してください。
gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
ん? gem が2つ・・・?と気になる方は、ぜひ README の Usage をどうぞ。今回は詳細解説しません。
Elasticsearch にインデックスをつくる
今度こそ Elasticsearch にインデックスをつくりましょう。
検索対象としたい Model の中に、 Elasticsearch::Model を include しましょう。
※elasticsearch-rails, elasticsearch-model のサンプルコードでは、 Model は Article となっているようです。本記事でも、説明で使うサンプルコードの Model は Article で統一しようと思います。
class Article < ActiveRecord::Base
include Elasticsearch::Model
end
これで、 Model で Elasticsearch を扱う準備ができました。ということで、インデックスを作成してみましょう。インデックスは以下のようなコードで作成できます。
Article.__elasticsearch__.create_index! force: true
このへんの挙動の詳細が知りたい場合は、elasticsearch-model の README を見ると詳しく解説されています。
https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model#index-configuration
ちなみに NOTE に書かれてある通り、
Elasticsearch will automatically create an index when a document is indexed, with default settings and mappings.
デフォルトのインデックス設定とマッピングを利用する場合、明示的にインデックスはつくらなくてもよいです。
今回は、明示的にインデックスをつくる操作を一度説明したほうが理解が促進されると思い、敢えて書きました。(逆に混乱させてしまったら申し訳ありません。)
Elasticsearch にドキュメントをいれる
次に、 Elasticsearch のインデックスの中にデータを入れてみましょう。ここでいうデータは、もちろん検索対象にしたいデータのことであり、 Elasticsearch では、ドキュメントと呼びます。( RDB でいうレコードの概念に相当)
では、 Elasticsearch にドキュメントをインポートしてみましょう。
Article.import
シンプルで分かりやすいですね。ここまでで、 Elasticsearch のインデックスの中に、ドキュメントが登録されました。
(参考)https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model#importing-the-data
2.アプリケーション側で検索すると、検索エンジンにクエリを投げ、結果が返ってくる
次に、 Rails アプリケーション側で検索すると、 Elasticsearch にクエリを投げるようにしましょう。
以下のように書けば、 Rails から Elasticsearch にクエリを投げることができます。
response = Article.search 'fox dogs'
つまり、 Rails から Elasticsearch にクエリを投げる実装をするためには、クエリパラメータに、アプリケーション側の検索文字列を単に突っ込んであげればよいですね。
def index
@articles = Article.search(params)
end
これで終わりです。
ちなみに取得したデータは、こんな感じで扱うことができます。
response.took
# => 3
response.results.total
# => 2
response.results.first._score
# => 0.02250402
response.results.first._source.title
# => "Quick brown fox"
##3.アプリケーション側で検索対象のデータが更新されると、ちゃんと検索エンジンの中のデータにも更新が反映される
ここまでで、 Elasticsearch を検索エンジンとして使うところまではできました。しかし、実際のサービスで運用することを考えると、もちろん検索対象のデータが更新されれば、 Elasticsearch の中のドキュメントも更新されなければなりませんね。
ということで、最後に、 Elasticsearch のドキュメントを Rails から更新する方法について紹介します。
さて、まずは単に Elasticsearch のドキュメントを更新するには、以下のようなコードで実装できます。
Article.first.__elasticsearch__.update_document
他にも delete_document というメソッドがあるので、これをつかえばドキュメントの削除もできます。
さて、では、これらのメソッドをレコード更新する処理の前後に都度実装すればよいのでしょうか?もちろんそんな必要はありません。
elasticsearch-model では、 Elasticsearch::Model::Callbacks を Model に include しておくと、レコードの更新をした際に Elasticsearch のドキュメントを更新するクエリを投げてくれます。
class Article
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
end
非同期でドキュメント更新する
しかし、これだけではよくないですね。データベーストランザクション中にも HTTP リクエストぼんぼこ投げていたら、きっと運用ポリスに怒られます。
そうです、非同期にしたいですね。
ということで、非同期にする方法を最後に紹介して記事を締めくくります。
以下のように書けば非同期で処理できます。こちらの例では、 sidekiq を使用しています。
(超初心者向けの内容からはずれるので解説はスキップします。)
class Article
include Elasticsearch::Model
after_save { Indexer.perform_async(:index, self.id) }
after_destroy { Indexer.perform_async(:delete, self.id) }
end
class Indexer
include Sidekiq::Worker
sidekiq_options queue: 'elasticsearch', retry: false
Logger = Sidekiq.logger.level == Logger::DEBUG ? Sidekiq.logger : nil
Client = Elasticsearch::Client.new host: 'localhost:9200', logger: Logger
def perform_async(operation, record_id)
logger.debug [operation, "ID: #{record_id}"]
case operation.to_s
when /index/
record = Article.find(record_id)
Client.index index: 'articles', type: 'article', id: record.id, body: record.__elasticsearch__.as_indexed_json
when /delete/
Client.delete index: 'articles', type: 'article', id: record_id
else raise ArgumentError, "Unknown operation '#{operation}'"
end
end
end
##まとめ
以上で、 **Rails で全文検索エンジンとして Elasticsearch を動かすための考え方(超初心者向け)**を締めくくります。
本記事では、全文検索エンジンをアプリケーションに組み込む際に必要なことを3つに分け、その上で Rails × Elasticsearch に限定した実装方法をご説明しました。
お付き合いいただきありがとうございました。
何か気になることがあれば、いつでもどうぞ!(ちゃんと答えられるか分かりませんが!)
@katsuhisa__
他の「 Elastic stack (Elasticsearch) Advent Calendar 2017 」も楽しみにしています。
###余談
Elasticsearch の用語と RDB の用語は、それぞれが完全なサブセットではないです。なので、 Elasticsearch の用語で分からないことがあれば、都度調べるほうが良いと思います。そのほうが、「 RDB でできる◯◯を Elasticsearch でもできるんだよね?」という変な期待や勘違いをしなくて済むのかなあ、と初心者ながらに思っています。
ぼくはビビリなので、分からないことがあればドキュメント読みます・・・
Elasticsearch は公式ドキュメントめちゃくちゃ充実していて、今回も自分の手で実装しながら、ハマったところ(バージョンによって Filterd query の書き方が異なっていた)については、ちゃんと公式ドキュメントに変更ポイントの解説が書いてました。
###余談2
この記事書きながら README の誤りを見つけたので、 PR 送ったらマージされました。
https://github.com/elastic/elasticsearch-rails/pull/764#event-2316774905