2006-04-08 優しいRailsの育て方 [長年日記]
● [Rails] acts_as_searchable
ずっと気になっていた namazu に代わりうる国産の新しい全文検索エンジン Hyper Estraier。その Rails プラグインが、樂水開発日記 http://dev.rakusui.jp/diary/?date=20060408 様にて紹介されていたので早速トライ。
● サーバ起動
Hyper Estraier は CGI インターフェースも、APIも両方持っているが、acts_as_searchable では http 経由でアクセスしている模様。ということで、Webのサービスを起動しておく。ポートのデフォルトは1978。Debian では /etc/default/hyperestraier が設定ファイルみたい。(/etc/hyperestraier/* でないのがちょっと意外)。
/etc/default/hyperestraier
NO_START=0 # NO_START=1 だとサービスが起動できない |
サーバ起動
# /etc/init.d/hyperestraier start |
● 設定
http://localhost:1978/ にアクセスして administration に進む。管理用のアカウントのデフォルト user:admin, pass:admin を使ってログイン。
Manage Nodes
ノード(検索用DBみたいなもの?)を作成。とりあえず 'berryz' あたりで create。
Manage Users
実験とは言え、そのままだと admin:admin で入られちゃうので管理用のアカウントを作成する。3番目のフォーム(flags)に 's' と入力すると管理権限を持つユーザになる。
maiha ****** s
管理権限は、この管理ツールやインデックス作成時に必要になる。管理ユーザを新規に作成したら現在の admin は安全のために消しておく。
● Rails側の準備
適当な Rails アプリにて
plugin インストール
% ruby script/plugin install svn://poocs.net/plugins/acts_as_searchable |
Estraier への接続設定は database.yml に記述する。
/usr/share/tdiary/tdiary/regex_rules/syntax-ruby:3:in `require': no such file to load -- syntax/convertors/html (LoadError) from /usr/share/tdiary/tdiary/regex_rules/syntax-ruby:3
● モデルの設定
いつもの Member クラスを全文検索できるように設定してみる。
app/models/member.rb
class Member < ActiveRecord::Base acts_as_searchable :searchable_fields=>[:name, :yomi, :comments, :blood] end |
searchable_fields オプションに全文検索の対象に入るカラム名を指定する。
- デフォルトは :body カラムが参照される
- 文字列系のカラムでないとエラーになる
● インデックス作成
AR#save で自動的にインデックスが作成される。既存のデータに対してインデックスを作るには reindex! メソッドを実行する。
script/console
>> Member.reindex! >> Member.estraier_connection.status => 200 |
接続に失敗してもエラーを出さないので、connection オブジェクトの status (HTTP Response の status 相当)を一応確認しておく。estraier_connection メソッドで connection オブジェクトを取得できる。
status
- 200 成功
- 404 サーバ起動してないとか、ノードがないとか
- -1 アカウントがおかしい。(reindex!には s 権限が必要)
ここは本来なら例外を出すべきなので、いずれはそうなると期待。
● 検索
検索は fulltext_search メソッドの第一引数に検索文字列を渡す。省略可能な第二引数には Estraier 用の詳細なオプションを指定できる。
script/console
>> Member.fulltext_search('舞波')
=> [#<Member:0xb7811fe0 @attributes={"name"=>"石村舞波", "comments"=>"まいは!まいは!", ...>]
>> Member.fulltext_search('年長')
=> [#<Member:0xb77a6204 @attributes={"name"=>"清水佐紀", "comments"=>"年長組 キャプテン", ...>,
#<Member:0xb77a61c8 @attributes={"name"=>"嗣永桃子", "comments"=>"年長組 ぴーちっち", ...>] |
うほっ!
● そうだ、tdiary を入れてみよう
アプリ作成
% rails -d sqlite3 tdiary |
config/environment.rb
$KCODE = 'u' |
/usr/share/tdiary/tdiary/regex_rules/syntax-ruby:3:in `require': no such file to load -- syntax/convertors/html (LoadError) from /usr/share/tdiary/tdiary/regex_rules/syntax-ruby:3
db/schema.rb
ActiveRecord::Schema.define() do
create_table "tdiaries", :force => true do |t|
t.column "date" , :string, :limit => 8
t.column "title" , :string
t.column "visible" , :boolean
t.column "style" , :string, :limit => 16
t.column "body" , :text
t.column "created_on" , :datetime
end |
DB作成
% rake db:schema:load |
app/models/tdiary.rb
class Tdiary < ActiveRecord::Base
acts_as_searchable :searchable_fields=>[:title, :body]
class << self
def import (buffer)
td2_regex = /^Date:\s*(\d{8})\nTitle:(.*?)\nLast-Modified:\s*(\d*?)\nVisible:\s*(.*?)\nFormat:\s*(.*?)\n\n(.*?)\n\.\n/m
transaction do
buffer.scan(td2_regex).each do |args|
destroy_all(["date = ?", args.first]) # callback で index を作るので delete 系は不可
create(Hash[*[:date, :title, :created_on, :visible, :style, :body].zip(args).flatten])
end
end
end
end
end |
ここ2年のデータを投入。
% script/console
>> Dir["/home/anna/ac/200[56]/*.td2"].each do |path|
buffer = NKF.nkf('-w', File.read(path))
Tdiary.import(buffer)
end
>> Tdiary.count
=> 204 |
検索。
script/console
>> berryz = ["佐紀", "桃子", "千奈美", "茉麻", "雅", "舞波", "友理奈", "梨沙子"]
>> berryz.map{|name| [name, Tdiary.fulltext_search(name).size]}.sort{|a,b| b[1]<=>a[1]}
=> [["舞波", 65], ["佐紀", 28], ["雅", 26], ["桃子", 25], ["梨沙子", 15], ["茉麻", 13], ["友理奈", 13], ["千奈 美", 9]] |
うむ。大筋、愛情サイズ。
AND 検索とか
>> Tdiary # loads class
>> class Tdiary; def summary; "#{date}: #{title}: #{body.split(//)[0,40].join}"; end; end
>> p Tdiary.fulltext_search("Rails AND 舞波", :limit=>3).map(&:summary)
["20050829: 優しいRailsの育て方: ! [rails] [ajax] multicontrols\n\n从?w?) <舞",
"20060223: : ! 月イチ恒例、第一回チキチキツッコ ミ全レス紹介\n\n{{{[AA]\n ?ノノ",
"20060215: 優しいRailsの育て方: ! [Rails] スペジェネ #3\nえっと、まず、Edge の取 り方にガセネタ"]
>> p Tdiary.fulltext_search("Berryz AND 名曲", :limit=>3).map(&:summary)
["20050826: Berryz工房のカップリング系オススメ: ! [berryz] 夏わかめ (2nd CW)\n小さい頃の海の想い出が思わず頭",
"20050827: 2005年 夏 W&Berryz工房 コンサートツアー 『HIGH SCORE!』 : ! [berryz] 大宮(15:00〜)\nベリコンは単独じゃないのでパスする予",
"20051201: 紅白歌合戦: ! [Berryz] 紅白歌合戦\n[[「第56回 NHK紅白歌合戦」|http:"] |
● 補足
手動で色々やってたり間違って delete したりでインデックスの整合性がおかしくなると、fulltext_search の後の find でこけるようになって焦る。そういうときは、clear_index! あんど reindex! すべし。
完全なインデックス再作成
>> Tdiary.clear_index! >> Tdiary.reindex! |
● まとめ
- 何十万件の reindex! は多分死ぬ (時間よりも、find(:all) してるからメモリ的に)
- ゴリゴリクエリを準備しなくて済む&設定が楽なので、Rails全文検索のデファクトになりそう
- コネクションエラーはやはり例外にスベッキー
- fulltext_count が欲しい (日記など特にデータが大きいので無駄が大きい)
- fulltext_indexes でもいい (_search は _indexes + find の方向で)
- :include 先も(searchable であれば)検索対象に入ったりするともう失禁 (難しそう)
- 気付いたら tdiary が sqlite に入ってる嬉しい副作用
- tdiary もそろそろ DB を使ってくれるとウレシス
● 参考
- acts_as_searchable http://weblog.rubyonrails.org/articles/2006/04/06/plug-into-hyperestraier-with-acts_as_searchable
- Hyper Estraier http://hyperestraier.sourceforge.net/uguide-ja.html
- CGIインタフェース http://wota.jp:1978/node/tdiary/search_ui
(↑AR用のインデックスなのでリンク先は未設定)


舞波乙<br>Rails 1.1.2でてました。
情報乙!クゥ〜ン♪