kosappi の日記

愛知県豊橋市に住んでます。

Rails + Ransack + PostgreSQL で ILIKE ではなく LIKE を使う

はじめに

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_insensitivetrue にしても 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
    # オプションcase_sensitiveをデフォルトでtrueにする(本来のmatchesはfalse)
    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', # ここに追加したパッチで追加したPreficationを指定
                       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 の区別はけっこう大事な気がするので、コントリビュートチャンスかも知れない。