acts_as_tritonn
前回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さんにご提供いただきました。ありがとうございます。
ここで問題です
MySQLとSennaはSNSのようなサイトで、特定の会員等に対して検索結果を非表示にしたりするのに非常に有効な手段だと思います。
ですが、この方法は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するときに使ったりと、実際の運用では必要かもしれません。