ActiveScaffold::search が遅い
ActiveScaffold を覚えたので、調子に乗って自宅鯖にある某ラジオ局の ON AIR 履歴一覧を Rails で書き直してみた。
2001 年から蓄積した 84 万曲の履歴から、最新のものを一覧表示する。5 分更新。
ここで ActiveScaffold の Search アクションを使って検索ボックスを実装したのだが、前方一致検索にしたのに数秒単位で時間がかかる。
データベースには index を張ってあるのになぜ? と思い、Rails のログからクエリを取り出してみると、
SELECT count(*) AS count_all FROM `songs` WHERE ((LOWER(songs.`artist`) LIKE 'kotoko%' OR LOWER(songs.`title`) LIKE 'kotoko%'))
大小文字に関係なくマッチさせるために、条件の左辺に LOWER() を使っている。これを DESCRIBE すると、
mysql> DESCRIBE SELECT count(*) AS count_all FROM `songs` WHERE ((LOWER(songs.`artist`) LIKE 'kotoko%' OR LOWER(songs.`title`) LIKE 'kotoko%'))\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: songs type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 840665 Extra: Using where 1 row in set (0.00 sec)
ということでインデックスが効かず、84 万件を全サーチしていたのだった。試しに左辺に LOWER() をかけずに実行すると、
mysql> DESCRIBE SELECT count(*) AS count_all FROM `songs` WHERE ((songs.`artist` LIKE 'kotoko%' OR songs.`title` LIKE 'kotoko%'))\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: songs type: index_merge possible_keys: title,artist key: artist,title key_len: 62,62 ref: NULL rows: 233 Extra: Using sort_union(artist,title); Using where 1 row in set (0.00 sec)
このようにインデックスが効く。MySQL の大小文字同一視検索は列の collation が *_ci なら有効なので、LOWER() を使う必要はないはず。
というわけで ActiveScaffold にパッチする。grep したところ Search アクションのクエリを生成しているのは ActiveScaffold::Finder なので、RAILS_ROOT/lib/ に active_scaffold_hooks.rb と称して以下のファイルを作成。
module ActiveScaffold module Finder def self.create_conditions_for_columns(tokens, columns, like_pattern = '%?%') # if there aren't any columns, then just return a nil condition return unless columns.length > 0 tokens = [tokens] if tokens.is_a? String where_clauses = [] columns.each do |column| where_clauses << "#{column.search_sql} LIKE ?" end phrase = "(#{where_clauses.join(' OR ')})" sql = ([phrase] * tokens.length).join(' AND ') tokens = tokens.collect{ |value| [like_pattern.sub('?', value.downcase)] * where_clauses.length }.flatten [sql, *tokens] end def self.condition_for_column(column, value, like_pattern = '%?%') return unless column and column.search_sql and value and not value.empty? case column.form_ui || column.column.type when :boolean, :checkbox ["#{column.search_sql} = ?", (value.to_i == 1)] when :integer ["#{column.search_sql} = ?", value.to_i] else ["#{column.search_sql} LIKE ?", like_pattern.sub('?', value.downcase)] end end end end
中身は本来の ActiveScaffold::Finder のコピペ + LIKE 左辺の LOWER() の除去。こいつをenvironment.rb で require すればおk.
修正 2008.04.04: フックを environment.rb から require するように変更。