acts_as_tritonn

ryu000262007-12-09


前回MySQL+Sennaを簡単に扱えるプラグインを作って公開するとか言っておきながら忙しくて放置してました^^;
このまま放置しようかと一瞬思ったのですが、はてなスターも付いてることだし、折角なので公開します
なお、全文検索クエリを簡単に使うことを目的としているため、細かくSenna演算子をつかったり、適合率でソートはできません^^;

インストール

githubに引っ越しました

script/plugin install http://ryu.rubyforge.org/svn/acts_as_tritonn
git clone git://github.com/ryu00026/acts_as_tritonn.git

使い方ですが、

  class Post < ActiveRecord::Base
    acts_as_tritonn
  end
普通の検索
 Model.find_fulltext({:col1 => "hoge"})
  => SELECT * FROM models WHERE MATCH(col1) AGAINST('+hoge' IN BOOLEAN MODE);
AND検索
 Model.find_fulltext({:col1 => ["hoge foo"]})
  => SELECT * FROM models WHERE MATCH(col1) AGAINST('+hoge +foo' IN BOOLEAN MODE);
OR検索
 Model.find_fulltext({:col1 => ["hoge", "foo"]})
  => SELECT * FROM models WHERE MATCH(col1) AGAINST('-hoge -foo' IN BOOLEAN MODE);
countをとる場合も同様です
 Model.count_fulltext({:col1 => "hoge"})

一部、同様のプラグインであるacts_as_ludiaを参考に

 :all=>true

オプションをつけると複数のカラムに対してORかAND検索できます。

 cond = ["posts.user_id = ?", params[:user_id]]
 query = {:"posts.title" => "foo",:"posts.content" => "foo",:"comments.content" => "foo" 
 joins = "LEFT JOIN users ON users.id = posts.user_id LEFT JOIN comments ON #posts.id = comments.post_id "
 @pages = Paginator.new(self,
 Post.count_fulltext(query,
   :select => "DISTINCT(posts.id)",
   :all=>true,
   :joins => joins,
   :conditions => cond),
   10,
   params[:page])
 @posts = Report.find_fulltext(query,
   :select => "DISTINCT(posts.id)",
   :all=>true,
   :joins => joins,
   :conditions => cond,
   :order => "posts.updated_at DESC",
   :offset => @pages.current.offset, :limit => @pages.items_per_page)

のようなSQLがかけます。

migrate

 create_table :posts, :options => "ENGINE=MyISAM DEFAULT CHARSET=utf8" , :force => true do |t|
   t.column :title, :string
   t.column :content, :text
 end
   add_index :posts,  [:title], :fulltext => "NGRAM"
   add_index :posts,  [:content], :fulltext => "NGRAM"

※この部分はmasuidriveさんにご提供いただきました。ありがとうございます。

ここで問題です

MySQLSennaSNSのようなサイトで、特定の会員等に対して検索結果を非表示にしたりするのに非常に有効な手段だと思います。
ですが、この方法はInnoDBを使えないという大きな欠点を抱えます。
なので、通常はbackgroundrbなどで、バッチを走らせて定期的にインデックスを追加したりする方法を取ると思います。
が、そんなのめんどくさい!と思うじゃないですか!?(僕だけ?)。
そこで
mysql_replication_adapter
というのを試しています。

Slave DBのmy.cnfに

skip-innodb

とし、レプリケーション設定をして、slave側でCREATE INDEXしておけば、勝手にレプリケーションしてくれてインデックスが更新されていくかなと思い色々実験しています。
通常のマスタのInnoDBに対してINSERT/UPDATE文を実行し、スレイブでバイナリログを実行されるとき、slave側でインデックスが作成されます。

上の例だと

 @posts = Report.find_fulltext(query,
   :select => "DISTINCT(posts.id)",
   :all=>true,
   :joins => joins,
   :conditions => cond,
   :order => "posts.updated_at DESC",
   :offset => @pages.current.offset, :limit => @pages.items_per_page,
   :use_slave => true)

とuse_slaveオプションを追加しておけばSlaveに接続し、全文検索クエリを投げます。

こうすることで、軽快なMyISAMトランザクションが有効なInnoDBを使い、負荷分散もできるかなとか思ったり・・・・。
なお、右下のバックアップ用のSlaveはあってもなくてもいいけど、masterになにか障害が発生してもMyISAMのslaveをmasterには昇格できないので
あったほうがいいかと思います。まあ、mysqldumpするときに使ったりと、実際の運用では必要かもしれません。

TODO

  1. kwicに対応する
  2. DBのadapterをmysql_replication_adapterを使うことを想定して、migrateでスキーマを一括管理できるようにする

などなど。。。
なんかそうこうしてるうちにRails2.0がリリースされてますね。
これ動くのかな?w
ちなみにRails1.2.5で動作確認しています。