バージョン Rails7.0, MySQL8.0
(AND -(OR) + OR -(OR)) みたいなことをやりたいなと思って、色々こねくり回したらこんなコードができました。
直接ANDとORをかけると途端に遅くなってしまう(MySQLが一度に使えるインデックスのなんかの制約に引っかかっているぽい? 要調査)ので、サブクエリを使って一度全体を引いた後に、ANDがあれば絞り込むみたいな挙動にしてみました。
モデルに直接書いたけどconcernぽくもできそう。
class Item < ApplicationRecord
SUB_T = 'sub'
class << self
# 前提: search_target_column には検索対象となる文字列が入っていて、fulltext indexが貼られている
# 作りたいSQL
# select
# T1.*
# from
# (
# select
# *
# from
# items
# where
# match(search_target_column) against ('ケーキ チョコレート YY支店 XX支店' in boolean mode)
# ) as T1
# where
# match(T1.search_target_column) against ('+ケーキ +チョコレート -ガム -キャンディ' in boolean mode)
# or match(T1.search_target_column) against ('YY支店 XX支店 -ガム -キャンディ' in boolean mode);
# and: 'ケーキ チョコレート' or:'YY支店 XX支店' ignore:'ガム キャンディ'
def search(and_search_text: '', or_search_text: '', ignore_text: '')
and_targets = and_search_text.split(' ')
or_targets = or_search_text.split(' ')
ignores = ignore_text.split(' ')
sub_query = or_where(
targets: and_targets + or_targets,
ignores:,
table_name:,
)
model = from("(#{sub_query.to_sql}) AS #{SUB_T}")
model.and_where(targets: and_targets, ignores:, table_name: SUB_T)
.or(model.or_where(targets: or_targets, ignores:, table_name: SUB_T))
.select("#{SUB_T}.*")
end
def and_where(**kwargs)
fulltext_where(**kwargs.to_h.merge(operator: '+'))
end
def or_where(**kwargs)
fulltext_where(**kwargs.to_h.merge(operator: ''))
end
def fulltext_where(operator:, targets:, ignores:, table_name:)
targt_words = targets.map { |word| "#{operator}#{word}" }
ignore_words = ignores.map { |word| "-#{word}" }
matcher = (targt_words + ignore_words).join(' ')
where("match(#{table_name}.search_target_column) against (? in boolean mode)", matcher)
end
end
end
こういう雰囲気のコード書くの久しぶりなので、頭の体操になって楽しかった〜