こんにちは、tahara です。 ようやくいこーよを like '%foo%' の(SQL 力づく)全件検索から、MySQL の全文検索に変更しました。
最初は mroonga を使おうかと思ったのですが、結局は MeCab を使って MySQL の全文検索をそのまま使うことにしました。
MySQL の全文検索は MyISAM じゃないと動かないので ENGINE = MyISAM で検索用のテーブルを作成します。
class CreateFacilityIndices < ActiveRecord::Migration def up execute <<SQL create table facility_indices ( `id` int(11) NOT NULL AUTO_INCREMENT, `facility_id` int(11) NOT NULL, `content` text NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), FULLTEXT INDEX (content) ) ENGINE = MyISAM DEFAULT CHARSET utf8; SQL Facility.find_each {|f|f.update_facility_index()} end def down execute "drop table facility_indices" end end
検索したいテーブル(モデル)の after_save で上記のテーブ(facilities_indices)の登録、更新を行うようにします。 また、全文検索用のスコープも作成します。 order by で floor しているのは、同じようなスコアの場合は他の条件でソートしたいめです。
class Facility < ActiveRecord::Base after_save :update_facility_index has_one(:facility_index, :dependent => :destroy) def update_facility_index index = self.facility_index || self.build_facility_index index.update_from_facility end scope(:scoped_by_word, lambda { |text| joins(:facility_index). where("MATCH (facility_indices.content) AGAINST (?)", FacilityIndex.normalize(text)). order(sanitize_sql(["floor(MATCH (facility_indices.content) AGAINST (?)) desc", FacilityIndex.normalize(text)])) }) end
FacilityIndex では次のように、全文検索のインデックスを作ります。
- 検索対象の検索対象フィールドを一つも文字列にコンカチします。
- 地域と都道府県は全文検索結果スコアへの影響を大きくするために、単純に数回繰り返します。
- nkf で文字種の正規化を行います。
- MeCab で分解します。
- 助詞、助動詞は除きます。
- 原形があれば、原形を使うようにします。
- 「ある」などは検索上無意味なのでストップワードしとて除外します。
- MeCab 前にやっちゃうと、うまくいかないカタカナひらがな変換を行います。
- 以上のように正規化したものを FULLTEXT INDEX の付いたカラムに登録します。
検索時にはユーザの入力した検索ワードを同様に FacilityIndex::normalize で正規化して MATCH (...) AGAINST で検索します。 上記の Facility::scoped_by_word です。
# -*- coding: utf-8 -*- require 'nkf' require 'MeCab' # 一文字からも検索できるように # sudo vi /etc/mysql/my.cfg # [mysqld] # ft_min_word_len = 1 class FacilityIndex < ActiveRecord::Base belongs_to :facility # Facility.find_each {|f|f.update_facility_index()} def update_from_facility facility = self.facility # 都府県は除く prefecture_name = facility.prefecture_name.try(:gsub, /(都|府|県)$/, '') # region と prefecture は加重する。 self.content = FacilityIndex.normalize("#{facility.name} #{facility.kana} #{facility.pr} #{facility.description} #{([facility.prefecture.region.name]*7).join(' ')} #{([prefecture_name]*5).join(' ')}#{facility.address} #{facility.search_keyword} #{facility.features.map(&:name).join(' ')} #{facility.tag_list.join(' ')}") self.save! self end class << self def normalize(text) # UTF8 # 半角カタカナ => 全角カタカナ # MIMEはデコードしない # 全角アルファベット、全角スペースを半角に # # ひらがな、カタカナは変換すると MeCab トークナイザが # 正しく動かないのでそのままにする。 text = NKF.nkf('-WwXm0Z1', text).gsub(/[\r、。・()「」【】!?]/, '') mecab = MeCab::Tagger.new node = mecab.parseToNode(text) s = '' while node features = node.feature.force_encoding('UTF-8').split(/,/) unless %w[助詞 助動詞].include?(features[0]) # 原形を使う。 word = features[6] # 原形がなければ、表層形を使う。 word = node.surface.force_encoding('UTF-8') if word == '*' unless stop_word?(word) s += word + ' ' end end node = node.next end # Mecab の後にカタカナをひらがなに変換する NKF.nkf('-Wwh1', s) end def stop_word?(word) # 下の select_stop_word でひっかかったワード return true if ["", "ある", "市", "OK", "いる", "施設", "する", "-", "お", "れる", "できる"].include?(word) false end # 全件の 50% にでてくるものは MySQL の全文検索でひっかからないので、 # そのようなワードをストップワードにする。 def select_stop_word() hash = Hash.new(0) FacilityIndex.find_each do |x| x.content.split(/ /).uniq.each do |word| hash[word] += 1 end end half = FacilityIndex.count / 2 stop_words = hash.select do |k, v| v > half end puts stop_words stop_words.map {|k, v| k} end end end
これで、全文検索ができました。 ある程度あいまいな検索ができるようになった上に、検索も速くなりました。
最後に、弊社ではシステムエンジニア、プログラマ、インフラエンジニアなどを募集しています。 おきがるにお問い合わせください。