search
LoginSignup
39

More than 5 years have passed since last update.

posted at

updated at

Rails ApplicationからAmazon Elasticsearch Serviceを使って素早く全文検索を可能にする

背景

プロダクトに全文検索機能が必要となりElasticSearchを使って開発することに、かといってElasticSearchの実装・運用経験がない私。
そこで最近Amazon Elasticsearch Serviceがリリースされたので導入してみました。

Amazon Elasticsearch Service

Amazon Elasticsearch Service

Amazon Elasticsearch Service は、AWS クラウドで Elasticsearch を簡単にデプロイ、
操作、スケーリングできるようにするマネージドサービスです。

https://aws.amazon.com/jp/elasticsearch-service より引用)
スタートアップで働く私にとってできる限りサービス開発に時間を割きたく、インフラストラクチャへの注意を可能な限り少なくしたいです。
その点においてAmazon Elasticsearch Serviceの導入は悪くないと考えました。

Amazon Elasticsearch Serviceセットアップ

ドメイン

ドメインを指定します。
注意として例えばcoubicというドメインを指定すると下記のようにsearchがprefixされたendpointになります。
search-coubic-xxxxxxxxxxxxxxxxx.aws-region.es.amazonaws.com

クラスター

ElasticSearchは、1台だけで検索サーバーとして動作します。また、大量のデータを処理したり耐障害性を実現するために複数のサーバー間で協調して動作することもできます。
ElasticSearchではこれらのサーバー群のことをクラスタ、群をなす1台1台のサーバーのことをノードと呼びます。
サービス要求に応じてクラスター等の設定をします。
料金イメージについては (http://calculator.s3.amazonaws.com/index.html?lng=ja_JP) よりシミュレーションすることが可能です。
※必ずしも正確な料金であることを私は保証できません。

アクセスポリシー

サービス構成の都合に合わせてアクセスポリシーを設定します。

Application

目的

商品をタイトル、詳細から全文検索できるようにします。

Gem

RailsからAmazon Elasticsearch Serviceを簡単に使えるGemをインストールします。

Gemfile
gem 'elasticsearch-model'
gem 'elasticsearch-dsl'

elasticsearch-modelはActiveRecordからElasticSearchを簡単に使える機能を提供
elasticsearch-dslはrubyからElasticSearchの検索クエリを簡単に書けるDSLを提供

接続設定

Amazon Elasticsearch Serviceへの接続を指定します。

/config/initializers/elasticsearch.rb
Elasticsearch::Model.client =
    Elasticsearch::Client.new hosts: [
                                  {host: 'your-aws-es-endpoint',
                                   port: '80'}]

私はここでport指定をせずに少しハマりました。

インデックスから検索まで流れ

アプリケーションへ検索を実装する前に、ElasticSearchがどのように動作するか初めに大体の流れを理解します。

  • インデキシング
    • ElasticSearchに送られてきたデータを処理し、インデックスに保存する。
  • アナライズ
    • インデキシングの時にデータの内容を解析し分割しインデックスに書き込む。
  • 検索
    • クエリの条件を満たすドキュメントを探す。
  • アナライザ
    • 検索時にクエリの操作を指定する。

インデックス作成から検索まで順に見ていきます。

インデックス名決定

インデックスはElasticSearchがデータを保存する場所です。RDBMSへ例えるとテーブルに相当するものと言えるでしょうか。
今回はモデル単位でインデックスを作成する想定です。

searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    index_name  [model_name.collection, Rails.env].join('_')
  end
end

スキーママッピング

スキーママッピングはインデックス構造を定義するために使用します。
インデックスは複数のタイプを含めることができますが、ここでは1つのタイプを定義しています。
次の属性を持つタイプであるCourseをスキーママッピングします。
- name :string
- description :text

course_searchable.rb
module CourseSearchable
  extend ActiveSupport::Concern

  included do
    include Searchable

    settings index: {
                 analysis: {
                      tokenizer: {
                          kuromoji_tokenizer: {
                            type: 'kuromoji_tokenizer',
                            mode: 'search'
                          }
                      },
                      filter: {
                         pos_filter: {
                             type:     'kuromoji_part_of_speech',
                             stoptags: %w(助詞-格助詞-一般 助詞-終助詞),
                         },
                         greek_lowercase_filter: {
                             type:     'lowercase',
                             language: 'greek',
                         },
                      },
                      char_filter: {
                          custom_mapping: {
                              type: 'mapping',
                              mappings: ['カ => カ', 'ガ=>ガ']
                          }
                      },
                      analyzer: {
                          kuromoji_analyzer: {
                             type:      'custom',
                             tokenizer: 'kuromoji_tokenizer',
                             filter: %w(kuromoji_baseform pos_filter greek_lowercase_filter cjk_width),
                             char_filter: %w(custom_mapping)
                         }
                      }
                 }
             }

    mappings dynamic: 'false' do
      indexes :name, analyzer: 'kuromoji_analyzer', index: 'analyzed'
      indexes :description, analyzer: 'kuromoji_analyzer', index: 'analyzed'
    end

    def as_indexed_json(options = {})
      attributes
          .symbolize_keys
          .slice(:name, :description)
    end
  end
end

実際にはCourseモデルにCourseSearchableモジュールをincludeしています。

course.rb
class Course < ActiveRecord::Base
  include CourseSearchable
end

インデックスの作成

rails consoleからAmazon Elasticsearch Serviceへインデックスを作成します。

Course.__elasticsearch__.create_index! force: true

Amazon Elasticsearch Serviceのダッシュボードからインデックスが作成されたことを確認できると思います。

データの投入

rails consoleからAmazon Elasticsearch Serviceへ前商品データを投入します。

Course.__elasticsearch__.import

検索

入力されたキーワードから全文検索を行うクエリを組み立て、検索を実行します。

course_searchable.rb
module CourseSearchable
  extend ActiveSupport::Concern
    .....省略  

    def self.search(query, size)
      search_definition = Elasticsearch::DSL::Search.search {
        size size         
        query do
          multi_match do
            query keyword
            fields %w(name description)
          end
        end
      }
      __elasticsearch__.search(search_definition)
    end
  end
end

実際の検索ではElasticSearchの検索結果からRDBMSへ問い合わせを行い、検索結果に肉付けをしています。

courses_controller.rb
courses = Course.search(keyword, SEARCH_LIMIT)
results =  Course.where course_id: courses.records.to_a.map(&:id)

ドキュメント更新

Courseモデルが作成・更新された時にElasticSearchへドキュメントの更新を伝えます。
UXを考慮して、ActiveJobを使って非同期で処理しています。

indexer_job.rb
class IndexerJob < ActiveJob::Base
  include Elasticsearch::Model

  queue_as :default

  def perform(course)
    course.__elasticsearch__.update_document
  end
end

終わり

シンプルですが全文検索ができました。
弊社サービスよりデモが確認できます。
https://popcorn.coubic.com/search?category=13&location=1&q=%E8%82%A9%E3%81%93%E3%82%8A

参考書籍

高速スケーラブル検索エンジン ElasticSearch Server

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
39