RailsとMySQL5.6.xで全文検索をやってみた
時間がないので備忘録的に書いていく。
http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html
前提
全文検索には、Natural Language Full-Text SearchesとBoolean Full-Text Searchesがある。
日本語で前者を使うには、分かち書きを別途導入する必要があって辞書メンテとかが大変なので、後者を選択。
後者の場合はNgramを使えば言語問わずなんとかなる。Ngramについては別途ググってください。
MySQLの設定
[mysqld]
ft_min_word_len = 2
innodb_ft_min_token_size = 2
上がMyISAM用で下がInnoDB用なのでどっちかでいい人はどっちかだけで。
デフォルトは4なんだけど、日本語の場合2文字あれば充分意味を為す単語が多いので2文字から。1文字から引っ掛けたい人は1を設定。ちなみにデフォルトだと英語でもdogとかは引っかからないので英語のブログとかだと3を設定するケースが多かった。ように思う。
追記)innodb_ft_min_token_sizeのデフォは3だった。http://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html#sysvar_innodb_ft_min_token_size
gem
ngramはシンプルなので自分で書いてもいいけど、自分で書く必要もないので以下のgemを使うことにした。
https://github.com/tkellen/ruby-ngram
gem "ngram", "~> 1.0.0"
migration
model
model(テーブル)の作成自体は特に気を使うは必要は・・・あるかな。
Ngramを使う場合、たとえば検索対象の文が"あいうえお"だったら"あい いう うえ えお"という感じになる(n=2の場合)ので、引っ掛けたいコンテンツのボリュームによるけど少なくとも3倍以上の容量を確保しておく必要がでてくる。
ので、検索対象とするコンテンツのMAXから計算して足りるくらいのデータ長を確保しておく。
index
MySQL依存になってしまうのはあたりまえだけど仕方ない。
で、たぶんもうこれは、生SQLを実行するしかない。
modelのmigrationにくっつけてもいいんだけど、INDEXの再作成時とかに便利なので別途migration定義をすることにした。
class AddFulltextIndexToYourModels < ActiveRecord::Migration def self.up execute "create fulltext index your_model_fulltext_index on your_models (content);" end def self.down execute "drop index your_models_fulltext_index on your_models;" end end
こうしておくと、たとえばデータ作成バッチとかで
require Rails.root.join("db/migrate/2014xxxxxxxxxx_add_fulltext_index_to_your_models.rb") AddFulltextIndexToYourModels.down # loading..... AddFulltextIndexToYourModels.up
みたいに使えてDRYに便利。
model
ngramを使う場合は、データをぶちこむのも、検索するのも対象コンテンツをngramにかける必要がある。
class YourModel < ActiveRecord::Base MAX_LENGTH_CONTENT = 1000000 validates :content, presence: true, length: {maximum: MAX_LENGTH_CONTENT} before_save :ngram_data def self.search(key) ngramed_key = self.ngram(key, ' +') where("match(content) against (? in boolean mode)", "+#{ngramed_key}") end private def ngram_data self.content = YourModel.ngram(self.content) if self.content.present? end def self.ngram(data, connector=' ') n ||= NGram.new({ size: 2, word_separator: "", padchar: "" }) parsed = n.parse(data) parsed.join(connector) end end
各ワードに+をつけているのは、boolean modeで「含む」を表すため。-を付けると「含まない」になる。けど、そういうフクザツなのは今回考慮しない。
spec
ここがけっこうハマった。
describe '.search' do before do @your_model.content = 'こんにちは' @your_model.save! end it { expect(YourModel.search('こんにちは').count).to eq 1 } end
こんな感じで問題ないはずなんだけど、なぜかテストがとおらなかった。
原因はDatabaseCleanerの設定にあった。
DBがMySQLなら、strategyはたいがいtransactionにしといたほうが、早いからいいんだけど、transactionだとどういうわけかindexがロールバックされちゃうのかな、とにかく引っかからない。truncationにするとうまくいく。
追記)よく調べたらtransactionは、commitすることなくrollbackで高速化を図っているという話だった。そんくらい名前から察せよ、という感じですな。とにかくcommitされなかったらindexもつくられないよね、ということで。
なので、fulltext indexを使うやつだけtruncationにした。
spec_helper.rb
>|ruby|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.strategy = :truncation, {only: %w(your_models)}
FactoryGirl.reload
end
|
追記)上記の設定は機能しない。よく考えりゃあたりまえか・・・。以下のように個別に対応した。
describe YourModel do before(:all) do DatabaseCleaner.strategy = :truncation end after(:all) do DatabaseCleaner.strategy = :transaction end
LIKE検索程度でいいんだったら、この程度の手間で使えるのでマジおすすめ。
[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン (Software Design plus)
- 作者: 大谷純,阿部慎一朗,大須賀稔,北野太郎,鈴木教嗣,平賀一昭,株式会社リクルートテクノロジーズ,株式会社ロンウイット
- 出版社/メーカー: 技術評論社
- 発売日: 2013/11/29
- メディア: 大型本
- この商品を含むブログ (7件) を見る