はじめに
Ransack は人気のある gem で、Rails + Ransack + RDB ですぐに検索システムが実装できる。
github.com
データソースに PostgreSQL を利用する場合、LIKE 句での検索ができない。LIKE 句が書かれて欲しいケースでも、ILIKE が書かれてしまう。これでは困る。
Rails + Ransack + PostgreSQL の組み合わせで LIKE が発行されるようにしたい。
同じ問題はすでに Ransack の github でも話題になっている。(そして解決方法も書かれている。)
How to use LIKE instead ILIKE on PostgreSQL? · Issue #699 · activerecord-hackery/ransack · GitHub
Ransack does not distinguish between "*_cont" for LIKE and "*_i_cont" for ILIKE with PostgreSQL · Issue #1421 · activerecord-hackery/ransack · GitHub
いろいろ試して解決できたので、紹介します。
まとめ
下記のコメントのようにモンキーパッチを適用して Arel に新しい Predications を追加する。
それを ransack で利用すれば良い。
How to use LIKE instead ILIKE on PostgreSQL? · Issue #699 · activerecord-hackery/ransack · GitHub
解説
Ransack + PostgreSQL では LIKE を書けない
まず、Ransack + PostgreSQL の組み合わせで LIKE を書く機能は標準では存在しない。
例えば name というカラムについて LIKE を使いたい場合は name_cont でリクエストするが name_cont でも name_i_cont でも ILIKE が書かれる。これは spec でも期待値として書かれている。
ransack/spec/ransack/predicate_spec.rb at 2d56e78f860bbceec9f52fa7ec5a0cce6bb0702b · activerecord-hackery/ransack · GitHub
そのため、自分で predicate を追加する必要がある。
↓のような感じ
Ransack.configure do |config|
config.add_predicate 'custom_cont',
arel_predicate: 'matches',
formatter: proc { |v| "%#{v}%" },
validator: proc { |v| v.present? },
type: :string,
case_insensitive: false
end
これで上手くいきそうな気がするのだが、この predicate を使っても ILIKE になってしまう。
case_insensitive を true にしても false にしても ILIKE になる。どうやら case_insensitive では LIKE か ILIKE かの選択はできないみたい。(このオプションの本来の用途はちゃんと見ていない)
Arel に新しい Predication を追加する
Ransack で cont を指定すると Arel の matches が利用されるのだが、この matches は LIKE/ILIKE の選択に対応している。
matches の定義
https://github.com/rails/rails/blob/6911b00c7c827c853afc535f0bda2b26b1a5fa33/activerecord/lib/arel/predications.rb#L131-L133
LIKE/ILIKE の選択
https://github.com/rails/rails/blob/6911b00c7c827c853afc535f0bda2b26b1a5fa33/activerecord/lib/arel/visitors/postgresql.rb#L7-L16
どうやら matches の引数 case_sensitive に Ransack の predicate の case_insensitive は反映されない。
そこで、デフォルトで case_sensitive が true な matches をモンキーパッチで追加する。
↓のような感じ
module Arel
module Predications
def s_matches(other, escape = nil, case_sensitive = true)
Nodes::Matches.new self, quoted_node(other), escape, case_sensitive
end
end
end
追加した Predication を Ransack から使う
上で Arel に追加した Predication を Ransack から使えるように add_predicate する。
Ransack.configure do |config|
config.add_predicate 's_cont',
arel_predicate: 's_matches',
formatter: proc { |v| "%#{v}%" },
validator: proc { |v| v.present? },
type: :string,
case_insensitive: false
end
これで PostgreSQL を利用している場合でも LIKE で検索ができる。
感想
今回は Arel のモンキーパッチを書いて強制的に case_sensitive を true にした。本来は Arel のモンキーパッチを書くのは避けたいので、Ransack 側から case_sensitive に渡す値を操作できるとベストなのだが、これは実装できなかった。(ちゃんと Ransack のコードを読めばできる気はする)
もっとスマートにできるよ!という方がいたら教えてください。よろしくお願いします。
ひとまず LIKE で検索できるようになったので良かったです。
ILIKE/LIKE の区別はけっこう大事な気がするので、コントリビュートチャンスかも知れない。