Elasticsearchを導入してみた(Rails, Docker, AWS Elasticsearch Service)
弊社アプリ"maricuru"で全文検索機能を実現するために、Elasticsearchを導入しました。
構成はこんな感じです。
- サーバーサイドはRuby on Rails
- 環境構築はDocker
- 本番にはAmazon Elasticsearchを利用
このあたりを利用した導入の手順をご紹介します。
DockerでElasticsearchの環境を作る
docker-compose.yml
はこんな感じになりました。
xpack.security.enabled=false
はセキュリティ周りの設定ですが、trueだとBasic認証を要求され、
開発環境では不要なのでfalseに設定しておきました。
# docker-compose.yml services: (中略) elasticsearch: container_name: myapp-elasticsearch build: context: . dockerfile: ./docker/elasticsearch/dev.Dockerfile environment: - discovery.type=single-node - xpack.security.enabled=false volumes: - myapp-elasticsearch:/usr/share/elasticsearch/data ports: - 9200:9200 networks: - myapp networks myapp: external: true
イメージは公式のdocker.elastic.co/elasticsearch/elasticsearch:6.2.3
を使用しました。
また、日本語で形態素解析をしたいので、Elasticsearchのプラグインのkuromojiをインストールしました。
# docker/elasticsearch/dev.Dockerfile FROM docker.elastic.co/elasticsearch/elasticsearch:6.2.3 RUN elasticsearch-plugin install analysis-kuromoji
以上の設定ができたらdocker-compose up
でdockerコンテナを立ち上げます。
ほんとにElasticsearchが立ち上がっているか確かめるために以下のcurlコマンドを打ちます。
$ curl 'http://localhost:9200' { "name" : "hbPYW-K", "cluster_name" : "docker-cluster", "cluster_uuid" : "J4CkrFRtT1Ou8MemlMiLDA", "version" : { "number" : "6.2.3", "build_hash" : "c59ff00", "build_date" : "2018-03-13T10:06:29.741383Z", "build_snapshot" : false, "lucene_version" : "7.2.1", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" }
こんな感じでレスがあり、どうやら立ち上がっているようです。
RailsからElasticsearchを扱う
elasticserch-rails
という素敵なgemがあります。
Elasticsearchのindex(RDBでいうdatabase的な?)の作成から、
ActiveRecordと連携して、初期データの投入、またデータ変更の同期を丸っとやってくれます。
READMEに従ってgemをインストールします。
gem install elasticsearch-model gem install elasticsearch-rails
まずはClient?の初期化です。
initializer
で行いました。
ここでhost
は先ほどのdockerで指定したcontainer_name
から来ています。
# config/initializers/elasticsearch.rb Elasticsearch::Model.client = Elasticsearch::Client.new({log: true, hosts: { host: 'myapp-elasticsearch'}})
続いてElasticsearchに投入したいモデルに設定を追加します。
先人の知恵(Elasticsearchを使ったRailsサンプルアプリケーションの作成 | 酒と涙とRubyとRailsと)を参考にElasticsearch周りをconcernに切り出しました。
ここではindexするフィールドの定義をしています。
textカラムは日本語で形態素解析したいので、analyzerにkuromojiを指定しています。
# models/concerns/article_searchable.rb module ArticleSearchable extend ActiveSupport::Concern included do include Elasticsearch::Model include Elasticsearch::Model::Callbacks # インデックスするフィールドの一覧 INDEX_FIELDS = %w(text updated_at created_at).freeze # インデックス名 index_name "article_#{Rails.env}" # マッピング情報 settings do mappings dynamic: 'false' do # 動的にマッピングを生成しない indexes :text, analyzer: 'kuromoji', type: 'text' indexes :updated_at, type: 'date', format: 'date_time' indexes :created_at, type: 'date', format: 'date_time' end end # インデックスするデータを生成 # @return [Hash] def as_indexed_json(option = {}) self.as_json.select { |k, _| INDEX_FIELDS.include?(k) } end end module ClassMethods # indexの作成メソッド def create_index! client = __elasticsearch__.client client.indices.delete index: self.index_name rescue nil client.indices.create(index: self.index_name, body: { settings: self.settings.to_hash, mappings: self.mappings.to_hash }) end end end
Model側で上記のconcernを読み込んでおきます。
class Article < ActiveRecord::Base include ArticleSearchable 略
ここまで来ると、Elasticsearchのindexを作成し、そこにRDBのデータを流し込むことが出来ます。
rails consoleを立ち上げて、、、
# index作成 Article.create_index! # レコードをインポート Article.__elasticsearch__.import
これでデータが流し込まれたはずです!
早速試しに検索してみましょう。
elasticsearch-rails
がActiveRecordにsearch
というメソッドを生やしてくれています。
searchの結果にrecords
をかますとAcitveRecordに変換してくれます。
Article.search("検索キーワード").records
上記コマンドを実行すると、検索結果がズラズラと表示されます!
続いて考えないといけないのはRDBとElasticsearchの同期です。
RDBでArticleレコードが追加・更新・削除されたときに、同様にElasticsearch側のdocumentも追加・更新・削除されないといけません。
それもelasticsearch-railsがいい感じにやってくれます。
一番簡単な方法はElasticsearch::Model::Callbacks
をincludeすることです。
module ArticleSearchable extend ActiveSupport::Concern included do include Elasticsearch::Model include Elasticsearch::Model::Callbacks # コレ (略) end (略) end
この場合after_commit
でElasticsearchへの追加・更新・削除処理が走ります。
一度に大量のレコードを更新するような場合だと、このように同期的に処理が走るのが都合悪い場合もあると思います。
その場合は非同期で更新する方法もあるようです。
今回のアプリの場合は更新頻度のそれほど高くないレコードだったので、同期的に更新するようにしました。
Amazon Elasticsearch Service を使う
本番ではAWSが用意しているElasticsearchのサービスを利用しました。
AWSによるフルマネージドなサービスなので、構築がグンと楽になります。
Kuromojiもはじめからサポートしています。素敵。
簡単に設定についてご紹介します。
- AWSのElasticsearch Serviceを開いて、新しいドメインをクリック。
- 名前を適当にいれて進みます。設定はデフォルトのままにしました。
- ネットワーク構成についてはVPCによる制御もできますが、今回はパブリックアクセスでIAMユーザーで制限をかけるようにしました。
一通り設定を入力すれば、ちょっと待つとElasticsearchサービスが立ち上がります。
つづいて、このAmazon Elasticsearch ServiceにRailsから繋がるように設定します。
Amazon Elasticsearch Service の IAM User/Role アクセス制御に対応する(Ruby 版) - Qiita
こちらのサイトを参考に、faraday_middleware-aws-sigv4
を利用します。
以下のにproductionではAWSに、developmentではローカルのdockerのElasticsearchに繋ぐように設定しました。
urlのエンドポイントhttps://search-myapp-xxxxxxxxxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com
はネットワーク構成でパブリックアクセスを選ぶと与えられます。
AWSのコンソール画面にエンドポイントの記載があります。
if Rails.env.production? require 'faraday_middleware/aws_sigv4' require 'patron' Elasticsearch::Model.client = Elasticsearch::Client.new( url: 'https://search-myapp-xxxxxxxxxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com' ) do |f| f.request :aws_sigv4, service: 'es', access_key_id: ACCESS_KEY_ID, # 先ほど設定したIAMユーザーのaccess key id secret_access_key: SECRET_ACCES_KEY, # 先ほど設定したIAMユーザーのsecret access key region: 'ap-northeast-1' f.adapter :patron end else Elasticsearch::Model.client = Elasticsearch::Client.new({log: true, hosts: { host: 'myapp-elasticsearch'}}) end
あとはproductionでもcreate_index!
とimport
をしてデータを流しこんであげればOKです。
以上になります。
elasticsearch-railsがいい仕事をしてくれるので、思ったよりスムーズに実装できた印象です。
何かのご参考になれば幸いです。