React Native Tech Blog

supported by maricuru (旧maricuru tech blogです)

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もはじめからサポートしています。素敵。

簡単に設定についてご紹介します。

  1. AWSのElasticsearch Serviceを開いて、新しいドメインをクリック。
  2. 名前を適当にいれて進みます。設定はデフォルトのままにしました。
  3. ネットワーク構成については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がいい仕事をしてくれるので、思ったよりスムーズに実装できた印象です。
何かのご参考になれば幸いです。