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)
- 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に投げるようにしてみます。
参考
- elasticsearch での Kuromoji の使い方
- railsから全文検索エンジンelasticsearchを利用する
- elasticsearchを日本語も扱えるようにRailsで使うメモ(Mongoid+Tire)
- elasticsearchのGUI「elasticsearch-head」がとても便利
- elasticsearchとkuromojiプラグインで日本語の全文検索
- elasticsearch version0.20.6の設定
- ElasitcSearch ことはじめ
- Elasticsearch入門 pyfes 201207
- 日本語Wikipediaをインデクシング(Kuromojiバージョン)
- Elasticsearchで日本語(kuromoji)を使う
- SolrとElasticsearchの比較
- karmi/tire
- 第1回ElasticSearch勉強会を開催しました! #elasticsearchjp
- Elastic searchをrailsから使ってみた
- Can tire callbacks be put into a resque job?
- Configuring the port for Tire
- elasticsearch/elasticsearch-servicewrapper