Rails
AWS
Elasticsearch

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

More than 3 years have passed since last update.


背景

プロダクトに全文検索機能が必要となり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