<< >>

2006-04-08 優しいRailsの育て方 [長年日記]

[Rails] acts_as_searchable

ずっと気になっていた namazu に代わりうる国産の新しい全文検索エンジン Hyper Estraier。その Rails プラグインが、樂水開発日記 http://dev.rakusui.jp/diary/?date=20060408 様にて紹介されていたので早速トライ。

Hyper Estraier の準備

まずはインスコ。あぁ、Debian マンセー。

インストール

# aptitude install hyperestraier

サーバ起動

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 を使ってくれるとウレシス

参考

(↑AR用のインデックスなのでリンク先は未設定)

本日のツッコミ(全2件) [ツッコミを入れる]
_ 名無し子 (2006-04-10 12:22)

舞波乙<br>Rails 1.1.2でてました。

_ 从*’w’) (2006-04-11 13:07)

情報乙!クゥ〜ン♪