前回までで、Elasticsearchの基礎とDockerによる開発環境の構築を行ないました。 今回は、いよいよRuby on RailsでElasticsearchをもちいた検索機能を実装していきます。

実装の方針としては、モデルに「Elasticsearchにクエリを投げて結果を受け取るメソッド」を定義していきます。 それでは見ていきましょう。

動作環境

この記事で利用しているGemの各バージョンは次のとおりです。

  • rails 5.0.0.1
  • elasticsearch-rails 0.1.9
  • elasticsearch-model 0.1.9

要件

今回は例としてarticlesテーブルのtitlecontentカラムに対する全文検索を行ないます。 マイグレーションは次のようになります。

class CreateArticles < ActiveRecord::Migration[5.0]
  def change
    create_table :articles do |t|
      t.string :title, null: false, default: ''
      t.text :content, null: false, default: ''
      t.timestamps
    end
  end
end

Gemをインストールする

Ruby on RailsからElasticsearchを操作するために、今回は次の2つのGemをインストールします。 この2つには、ActiveRecord::Base継承クラスからElasticsearchを操作するためのDSLなどが含まれています。

Gemfileに以下を追記し、bundle installします。

Gemfile:

gem 'elasticsearch-model'
gem 'elasticsearch-rails'

実装

では、具体的な実装に入ります。 まずElasticsearchを操作するためのモジュールをインクルードします。

Elasticsearch::Modelは、後述するsettingsメソッドなどを提供するモジュールです。 また、Elasticsearch::Model::Callbacksは、モデルをとおしてデータが更新された際にElasticsearchのドキュメントもあわせて変更するためのコールバックを提供します。

app/models/article.rb:

class Article < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
end

次にマッピングの定義です。 マッピングとは、ドキュメントをどのような構造で表現するかを定義することです。 具体的には、フィールドとその型、Analyzerなどを設定します。 これはsettingsメソッドにより行ないます。

ここではtitlecontentの2つのフィールドについて、それぞれstring型で、Analyzerとしてkuromojiを設定しています。

settings do
  mappings dynamic: 'false' do
    indexes :title, type: 'string', analyzer: 'kuromoji'
    indexes :content, type: 'string', analyzer: 'kuromoji'
  end
end

最後に検索用のメソッドを定義します。 次のself.searchメソッドは、たとえば「Rails」という文字列を渡したときに、titlecontentを見て「Rails」に関連するドキュメントを検索し、関連度の高い順にレコードを返します。

:fuzzinessは、あいまい検索を許容するかどうかの指定です。 これを設定すると、たとえば「ails(Rがない)」と検索しても「Rails」という文字列を対象に検索してくれます。

def self.search(query)
  __elasticsearch__.search({
    query: {
      multi_match: {
        fields: %w(title content),
        fuzziness: 'AUTO',
        query: query
      }
    }
  })
end

実装のまとめ

以上の実装をまとめると、次のようになります。 マッピングやメソッド定義は要件によって変わってきますが、基本的な実装フローは同じような形になるのではと思います。

class Article < ApplicationRecord
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks

  settings do
    mappings dynamic: 'false' do
      indexes :title, type: 'string', analyzer: 'kuromoji'
      indexes :content, type: 'string', analyzer: 'kuromoji'
    end
  end

  class << self
    def search(query)
      __elasticsearch__.search({
        query: {
          multi_match: {
            fields: %w(title content),
            fuzziness: 'AUTO',
            query: query
          }
        }
      })
    end
  end
end

インデックスを作成する

モデルを実装しただけの状態では、まだElasticsearchにデータが登録されていません。 まずElasticsearch上にインデックス(RDBでいうデータベース)を作成する必要があります。

これはArticleモデルの__elasticsearch__オブジェクトをとおして行ないます。

> Article.__elasticsearch__.create_index!(force: true)
=> {"acknowledged"=>true, "shards_acknowledged"=>true}

データをインポートする

次に、Articleモデルをとおしてデータベースに保存済みのデータをElasticsearchにインポートします。 以上で、Elasticsearchを操作する準備が完了します。

> Article.import
=> 0

実行例

登録したデータをArticleモデルをとおして検索すると、次のようになります。 これをもとにビューを実装すれば、検索画面をつくることができます。

> Article.search('Rails').results.first
=> #<Elasticsearch::Model::Response::Result:0x00000001613360 @result=#<Hashie::Mash _id="4" _index="articles" _score=0.9433406 _source=#<Hashie::Mash content="前回の記事では、Dockerのイメージやコンテナといった基礎についてまとめました。 ..." created_at="2017-01-10T08:05:20.235Z" id=4 title="Docker ComposeでRuby+PostgreSQLによるRails開発環境を構築する方法" updated_at="2017-01-10T08:05:20.235Z"> _type="article">>

参考記事

おわりに

Elasticsearchの検索はSQLのLIKE演算子よりも柔軟に設計できるので、より複雑な要件の場合に便利です。 次回はElasticsearchのMore Like This APIをもちいて、特定の記事における関連記事の実装を行ないます。