ぺんちゃん日記
<前の日記(2006/03/15) 次の日記(2006/03/26)> 最新

2006/03/16

[ruby][rast][rails]rast_indexed.rb改造のまとめ

・概要

railsからrastを利用して全文検索したい。そこでrast_indexed.rbを利用するが、いくつか要件を満たしていないことに気がついたので改造することにする。満たしていない要件は以下のとおり。

  • ヒット件数hit_countが取れず、Paginateできない
  • Rast::Result::Itemsが取れず、summaryが取れない
  • さらに、rastのtext領域を利用していないので、summaryは取れない
  • termsも取れない
  • associationにも対応したい
  • プラグインにしたい

要件を満たせないのは、主にfind_by_rastが検索結果のRast::Resultを捨ててしまうところにあるのだから、素直にRast::Resultを返せばいいような気もするけど、せっかくrailsと親和性を持たせた作りになっているのに壊してしまうのはもったいない。そこで、findの結果にRast::Result内の必要な情報を足してやることにする。

・作り方

オリジナルはrast_indexedとしてlibに置くことを想定しているけど、ここはプラグインで行ってみたい。この手のプラグインは、acts_as_をつけるのが一般的みたいなので、acts_as_rast_indexedとして作成することにする。では、pluginの作成。

~/hoge#./script/generate plugin acts_as_rast_indexed
     create  vendor/plugins/acts_as_rast_indexed/lib
略
     create  vendor/plugins/acts_as_rast_indexed/init.rb
     create  vendor/plugins/acts_as_rast_indexed/lib/acts_as_rast_indexed.rb

あっさり雛型ができた。作成されたファイルのうち、変更を加えるのは2つだけ。vendor/plugins/acts_as_rast_indexed/init.rbがプラグインの起動元。vendor/plugins/acts_as_rast_indexed/lib/acts_as_rast_indexed.rbがプラグイン本体。

では、init.rbを編集。

 # Include hook code here
 require_dependency 'acts_as_rast_indexed'
 
 ActiveRecord::Base.class_eval do
   include ActiveRecord::Acts::RastIndexed
 end

acts_as_rast_indexed.rbは、改造後の全ソースを載せようかと思ったが、ライセンスがようわからんので差分を載せておくことにした。オリジナルのrast_indexed.rbに差分を当てたものをコピペして欲しい。

・変更点

  1. find_by_rastメソッドのインターフェースが変わった
  2. find_by_rastメソッドを分離してrast_searchメソッドを追加した
  3. rast_text_filed,rast_text_filed=メソッドを追加した
  4. 環境設定configurationsにpreserveを追加した
  5. findの戻り値(Array)にhit_count, hit_count=, terms, terms=メソッドが追加される
  6. findの戻り値内の各要素にrast_summary, rast_summary=, rast_score, rast_score=メソッドが追加される

1については、第3引数にfindメソッド用のoptionを与えられるようになった。

find_by_rast(query, rast_options = {}, find_options = {})

これはfindするときにassociationされた行をincludeさせるために使う。associationがない場合、動作は改造前と変わらない(多分)ので、互換性そのものについては影響はない。

2については、おとなしくRast::Resultだけをもらいたいときに使う。find_by_rastも内部ではこれを呼んでいる。

3については、summaryが欲しい場合に使う。modelでsummayが必要なフィールド名を与えることで、そのフィールドをプロパティではなくtextとして扱う。初期値はnilで、改造前のとおりにすべてのフィールドをプロパティとする。

4については、rast検索オプションのpreserve_textに対応する値。キーは"preserve"。初期値はtrue。

5,6は、find_by_rast時に動的に追加される。メソッドの重複について考慮されていないので注意が必要。特にrast_summaryやrast_scoreは同じフィールド名があった場合、動作が保証できなくなる。

・使い方というか例

掲示板的データモデルとして、TopicsとKakikosというテーブルがあるとする。kakikosにはタイトルや投稿者、投稿日時、メッセージがあるが、これらからrast検索したい。検索結果として、タイトル、投稿者、投稿日時とメッセージの要約を表示したい。また、Topicsから補足情報を取得することもある。

両テーブルはいくつかのフィールドを持つけれど、そのへんはどうでもよくて、TopicsとKakikosが1対多の関係であることと、kakikos.messageが要約したいフィールドというのが注目すべき点。

このときapp/models/kakiko.rbは

class Kakiko < ActiveRecord::Base
  belongs_to :topic
  acts_as_rast_indexed
  self.rast_text_field=:message
end

となる。4行目でmessageフィールドが要約可能になる。selfがついてるあたりrailsっぽくなくてイヤンなんだけど、いい名前が思い当たらないので、とりあえず。

このモデルを元にrastのindexをつくる。indexの作成にはrebuild_rast_indexを使う。実行すると既存のindexは削除されるので注意が必要。

~/hoge$./script/console
Loading development environment.
>>require 'kakiko'
=> true
>>Kakikos.rebuild_rast_index

しばらく待つとインデックスの作成結果がどばーっと表示されるのでconsoleからexitする。index/development/kakikos以下にたくさんのファイルができてるはず。

controllerでは、次のような感じに使う。app/controllers/kakikos.rbとする。

class KakikosController < ApplicationController
  def search
    rast_options = {
      :offset => (@params[:page] || 0)*Conf.limit_search_display,
      :limit => Conf.limit_search_display,
      :need_summary => true,
    }
    find_options = {
      :include => 'topic'
    }
    @kakikos = Kakiko.find_by_rast(@params[:kw], 
      rast_options, find_options)
    @kakiko_count=@kakikos.hit_count
    @kakiko_pages = Paginator.new self, @kakiko_count, 
      Conf.limit_search_display, @params[:page]
  end
end

rast_optionsには:need_summaryを忘れずに。find_optionsにはbelongs_to関係を連動させるためにincludeを与える。find_by_rastでrast検索して@kakikosに代入する。

ヒット件数は@kakikos.hit_countで得られる。searchアクションのviewはapp/views/kakikos/search.rhtmlとして、次のような感じ。

<%= render_collection_of_partials 'search_item', @kakikos %>

これをrenderするためのテンプレート_search_item.rhtmlは次のような感じ。

<%= search_item.title %>
<%= search_item.name %>
<%= search_item.rast_summary %>

実際にはキーワードをボールドにしたりHTMLをエスケープしないといけないけど。ボールド加工はこんな感じか。

 @kakikos.terms.each do |term|
   t = term.term.to_s
   k.rast_summary.gsub!(Regexp.new(Regexp.quote(t)), 
     content_tag(:b, t))
 end
  

・その他

findの使い方の違いで発行されるSQLが変わってくるみたいなので修正した。

・改造してみた感想

もっとカッコよく改造できんものか。extendして代入していく様は見苦しいにもほどがある。名前も考えて付けたいがrails命名ルールがいまいちわからん。いろいろあるものの、やりたいことはできるようになったのでよしとしたい。作者の前田さんに感謝。

・試行錯誤の記録

Last Update: 2006-03-17 15:33:50

編集