hello-world
webエンジニアのメモ。とりあえずやってみる。

Hello World Tire (Railsにtireを導入する)

公開日時

RUby2.0, Rails4で確認

tireはrubyから全文検索エンジンのelasticsearchを利用するためのgemです。

[注意書き] 2013年の9月にelasticserachの公式gemとして elasticsearch / elasticsearch-rubyが登場したため、tireはretireになったと tireの公式サイトに書かれています。

今後もtire自体は使えるそうですが、rubyでelasticsearchを利用される際はelasticsearch-rubyを使用したほうがいいかもしれません。

私がtireを使用した際はまだelasticsearch-rubyが登場していなかったので、今回はtireに関してまとめます。

elasticsearchインストール(mac)

CentOSでのインストール方法はこちらを参照ください

  • brew install
brew install elasticsearch
  • 形態素解析器を使って日本語検索ができるようにkuromojiプラグインをインストール
plugin --install elasticsearch/elasticsearch-analysis-kuromoji/1.5.0
  • 起動
elasticsearch -f

初期設定

  • アプリ作成
rails new tire_sample
  • Gemfile追加
# Gemfile

gem 'tire'
gem 'kaminari' # 検索結果のページャに使用します
  • bundle install
./bin/bundle install --path=vendor/bundler
  • initializer設定
# config/initializers/tire.rb

config/initializers/tire.rb
Tire.configure do
  url "http://0.0.0.0:9200" # elasticsearchのurlを設定
end

Model作成

  • Topicモデル作成
./bin/rails g model topic title:string body:text
  • migrate
./bin/rake db:migrate
  • seed登録
# db/seeds.rb

Topic.create(title: 'Rubyでhello world', body: 'おはようございます。サンプルです')
Topic.create(title: 'Perlでhello world', body: 'こんにちは。サンプルです')
Topic.create(title: 'PHPでhello world', body: 'こんばんわ。サンプルです')
Topic.create(title: 'Rubyでhello world no.2', body: 'おはようございます。サンプルです')
Topic.create(title: 'Perlでhello world no.2', body: 'こんにちは。サンプルです')
Topic.create(title: 'PHPでhello world no.2', body: 'こんばんわ。サンプルです')
  • seed流し込み
./bin/rake db:seed
  • Topicモデル編集
# app/models/topic.rb

class Topic < ActiveRecord::Base
  # tireモジュール読み込み
  include Tire::Model::Search

  after_save :index_update
  after_destroy :index_remove

  # elasticsearchのindex名を設定、環境に応じてindexを変更できるようにしておく
  index_name("#{Rails.env}-search-topics")

  # idとtitleとbodyをマッピング対象に設定
  mapping do
    indexes :id
    indexes :title, analyzer: :kuromoji # kuromoji日本語形態素解析器を使用する
    indexes :body, analyzer: :kuromoji # kuromoji日本語形態素解析器を使用する
  end

  # save後にindexを更新
  def index_update
    self.index.store self
  end

  # destroy後にindexから削除
  def index_remove
    self.index.remove self
  end

  # 検索
  def self.search(params)
    tire.search(load: true, :page => params[:page], per_page: params[:limit]) do
      # titleとbodyから複合検索
      query {
        boolean do
          should { string 'title:' + params[:keyword].gsub('!', '\!').gsub('"', '\\"'), default_operator: "AND" }
          should { string 'body:' + params[:keyword].gsub('!', '\!').gsub('"', '\\"'), default_operator: "AND" }
        end
      } if params[:keyword].present?
      sort do
        by params[:order], 'desc'
      end
    end
  end
end

"include Tire::Model::Callbacks"を指定すれば、indexの追加、削除を個別に設定する必要はないのですが、後々resqueにindex再構築の処理をさせたいので個別設定にしてあります。

また、検索の際に"を入力するとelasticsearch側でエラーになってしまうのでエスケープするようにしています。

2014/1/6 追記

!もエラーになってしまうのでエスケープするようにしました。

参考: https://github.com/karmi/retire/issues/828

  • indexの再構築コマンド

今回は先にseedを作成したため、indexを再構築する必要があります。

Topicモデルのindexを再構築したい場合は、以下のようにCLASSにTopicを指定してコマンドを実行します。

./bin/rake environment tire:import CLASS='Topic' FORCE=true

Controller作成

Topic検索APIを作ってみます

  • Topicコントローラ作成
./bin/rails g controller topic index
  • routing設定
# config/routes.rb

root "topic#index"
get "topic/index"
  • indexアクション編集
# app/controllers/topic_controller.rb

class TopicController < ApplicationController
  def index
    limit = params[:limit].presence || 3
    if limit.to_i == 0
      limit = 3
    elsif limit.to_i > 10
      limit = 10
    end

    current_page = params[:page].presence || 1
    if current_page.to_i == 0
      current_page = 1
    end
    keyword = params[:keyword].presence

    begin
      topics = Topic.search({
        keyword: keyword,
        order: :id,
        limit: limit,
        page: current_page,
      })
    rescue => e
      logger.error(e.message)
      logger.error(e.backtrace.join("\n"))
      return render json: { error: 500 }
    end

    paging = {
      total: topics.total_count,
      total_pages: topics.num_pages,
      per_page: limit,
      current_page: current_page,
    }

    render json: { topics: topics, paging: paging }
  end
end

確認

ブラウザから http://localhost:3000 にアクセスするとjsonで結果が返ってきます。

{
  "paging": {
    "current_page": 1,
    "per_page": 3,
    "total_pages": 2,
    "total": 6
  },
  "topics": [
    {
      "updated_at": "2013-12-01T17:21:49.953Z",
      "created_at": "2013-12-01T17:21:49.953Z",
      "body": "こんばんわ。サンプルです",
      "title": "PHPでhello world no.2",
      "id": 6
    },
    {
      "updated_at": "2013-12-01T17:21:49.941Z",
      "created_at": "2013-12-01T17:21:49.941Z",
      "body": "こんにちは。サンプルです",
      "title": "Perlでhello world no.2",
      "id": 5
    },
    {
      "updated_at": "2013-12-01T17:21:49.931Z",
      "created_at": "2013-12-01T17:21:49.931Z",
      "body": "おはようございます。サンプルです",
      "title": "Rubyでhello world no.2",
      "id": 4
    }
  ]
}

パラメータ指定をすると絞り込みが可能です。

http://localhost:3000/?keyword=ruby おはよう

{
  "paging": {
    "current_page": 1,
    "per_page": 3,
    "total_pages": 1,
    "total": 2
  },
  "topics": [
    {
      "updated_at": "2013-12-01T17:21:49.931Z",
      "created_at": "2013-12-01T17:21:49.931Z",
      "body": "おはようございます。サンプルです",
      "title": "Rubyでhello world no.2",
      "id": 4
    },
    {
      "updated_at": "2013-12-01T17:21:49.869Z",
      "created_at": "2013-12-01T17:21:49.869Z",
      "body": "おはようございます。サンプルです",
      "title": "Rubyでhello world",
      "id": 1
    }
  ]
}

まとめ

tireを使用することで手軽に全文検索エンジンの組み込みができました。

サンプルコードはこちらにおいておきます: hilotter / tire_sample

次回はindex再構築処理をresqueに投げるようにしてみます。

参考


Related #elasticsearch

elasticsearchのindex更新を非同期に行う(tire + resque)

昨日の記事でtireを用いて全文検索エンジンのelasticsearchを操作する方法をまとめました。