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

RUby2.0, Rails4で確認

[tire](https://github.com/karmi/retire)はrubyから全文検索エンジンのelasticsearchを利用するためのgemです。

[注意書き] 2013年の9月にelasticserachの公式gemとして[elasticsearch / elasticsearch-ruby](https://github.com/elasticsearch/elasticsearch-ruby)が登場したため、tireはretireになったと[tireの公式サイト](https://github.com/karmi/retire/wiki/Tire-Retire)に書かれています。
今後もtire自体は使えるそうですが、rubyでelasticsearchを利用される際はelasticsearch-rubyを使用したほうがいいかもしれません。

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

[詳細記事はこちらをご参照ください](http://blog.hello-world.jp.net/?p=905)

## 初期設定

* アプリ作成

```
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](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 おはよう](http://localhost:3000/?keyword=ruby%20%E3%81%8A%E3%81%AF%E3%82%88%E3%81%86)

```
{
  "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を使用することで手軽に全文検索エンジンの組み込みができました。

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


### 参考
* [Hello World Tire (Railsにtireを導入する)](http://blog.hello-world.jp.net/?p=905)
* [elasticsearch での Kuromoji の使い方](http://d.hatena.ne.jp/akishin999/20130307/1362611100)
* [railsから全文検索エンジンelasticsearchを利用する](http://k-shogo.github.io/article/2013/09/03/rails-elasticsearch/)
* [elasticsearchを日本語も扱えるようにRailsで使うメモ（Mongoid+Tire）](http://yshtak.tumblr.com/post/35409374072/elasticsearch-rails-mongoid-tire)
* [elasticsearchのGUI「elasticsearch-head」がとても便利](http://yuheikagaya.hatenablog.jp/entry/2013/07/14/185752)
* [elasticsearchとkuromojiプラグインで日本語の全文検索](http://yuheikagaya.hatenablog.jp/entry/2013/08/06/012150)
* [elasticsearch version0.20.6の設定](http://tree-tips.appspot.com/elasticsearch/config/elasticsearch_yml/)
* [ElasitcSearch ことはじめ](http://qiita.com/Konboi@github/items/56f0aaca77db5df027af)
* [Elasticsearch入門 pyfes 201207](http://www.slideshare.net/JunOhtani/elasticsearch-pyfes-201207)
* [日本語Wikipediaをインデクシング（Kuromojiバージョン）](http://blog.johtani.info/blog/2013/09/03/ja-wikipedia-with-kuromoji/)
* [Elasticsearchで日本語(kuromoji)を使う](http://qiita.com/PENGUINANA_/items/55b5fe70be2272ff60d1)
* [SolrとElasticsearchの比較](http://www.slideshare.net/penguinana/solrelasticsearch)
* [karmi/tire](https://github.com/karmi/tire)
* [第1回ElasticSearch勉強会を開催しました！ #elasticsearchjp](http://blog.johtani.info/blog/2013/08/30/hold-first-elasticsearch-meetup-in-japan/)
* [Elastic searchをrailsから使ってみた](http://sssslide.com/www.slideshare.net/rs_wisteria/elastic-searchrails)
* [Can tire callbacks be put into a resque job?](https://github.com/karmi/tire/issues/669)
* [Configuring the port for Tire](https://github.com/karmi/tire/issues/304)
* [elasticsearch/elasticsearch-servicewrapper](https://github.com/elasticsearch/elasticsearch-servicewrapper)