RansackでGoogleの検索フォームのように入力枠をひとつだけにしてその検索語をスペース区切りして複数カラムをLIKE検索したい(add_predicateとransacker)

タイトルからしてややこしいんですけどw

"milk corn"で検索するとこういうSQLを発行したい。

SELECT 
  "users".* 
FROM 
  "users" 
WHERE 
  (
    (name ILIKE '%milk%' OR email ILIKE '%milk%') AND
    (name ILIKE '%corn%' OR email ILIKE '%corn%')
)

なんでこんなことをしてるかというと移行元システムのCGI.pmにベタ書きで実装されてるSQLがこのようになっていて新システムのUI/UXが落ちてると指摘されたためです。

検索フィールドが5個も10個もあるのは嫌だとのこと。(今後、開始終了年月日とかで絞り込みたいとか言われそうなのでRansack使えないとそれはそれで辛い)

Ransackのcont_all, cont_anyだと想定しているSQLにならない。

scope.mergeやAdvancedSearchで検索自体はできるけどparams[:q]を書き換えてるので検索フォーム側とattributesの整合が取れずキーワードを維持できなかったりする。

例えば、name_or_email_contをname_cont, email_contのようにして@q = Ransack.search(search_query)するといった具合。

searchkick(+elasticsearch)なんかも試しだしてこれは盛大に脱線してる、namazuみたいな感じでこれはこれで狙った検索結果にならなかった。

Ransackなしで実装できるけど別テーブルのカラム検索とかsort_linkとかの面倒みないといけないのかと悶々とする。

add_predicateとransackerを使って対応した。

Userモデルはname, emailカラムだけあるとします。

name_or_email_cont_all

puts User.ransack(name_or_email_cont_all: "milk corn".split).result.to_sql
SELECT 
  "users".* 
FROM 
  "users" 
WHERE 
(
  ("users"."name" ILIKE '%milk%' AND "users"."name" ILIKE '%corn%') OR 
  ("users"."email" ILIKE '%milk%' AND "users"."email" ILIKE '%corn%')
)

name_or_email_cont_any

puts User.ransack(name_or_email_cont_any: "milk corn".split).result.to_sql
SELECT 
  "users".* 
FROM 
  "users" 
WHERE 
(
  ("users"."name" ILIKE '%milk%' OR "users"."name" ILIKE '%corn%') OR 
  ("users"."email" ILIKE '%milk%' OR "users"."email" ILIKE '%corn%')
)

Ransackで簡単に検索フォームを作る73のレシピ - 猫Rails

「045 半角スペース区切りの文字列で検索する」のとおりに述語(predicate)を追加する

config/initializers/ransack.rb

Ransack.configure do |config|
  config.add_predicate 'has_every_term',
    arel_predicate: 'matches_all',
    formatter: proc {|v| v.split.map {|t| "%#{t}%"}}
end

name_or_email_has_every_term(cont_all相当)

puts User.ransack(name_or_email_has_every_term: "milk corn").result.to_sql
SELECT 
  "users".* 
FROM 
  "users" 
WHERE 
(
  ("users"."name" ILIKE '%milk%' AND "users"."name" ILIKE '%corn%') OR 
  ("users"."email" ILIKE '%milk%' AND "users"."email" ILIKE '%corn%')
)

Ransackで簡単に検索フォームを作る73のレシピ - 猫Rails

「049 full_nameを検索する」のとおりに対象となるカラムをransackerで定義する

app/models/users.rb

ransacker :search_fields do
  Arel.sql('CONCAT(name, email)')
end

search_fields_has_every_term

puts User.ransack(search_fields_has_every_term: "milk corn").result.to_sql
SELECT 
  "users".* 
FROM 
  "users" 
WHERE 
(
  CONCAT(name, email) ILIKE '%milk%' AND 
  CONCAT(name, email) ILIKE '%corn%'
)

別テーブル(Product.name)も検索したい場合

app/models/users.rb

ransacker :search_fields do
  Arel.sql('CONCAT(name, email, products.name)')
end

でも、これはこれでCONCATしてるからindex使えてないのが辛い。

あとArel.sql('CONCAT(name, ' ', email, ' ', products.name)')のように半角スペースを挟み込まないと予期しない検索結果になりそうではある。