2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RailsでMySQLの全文検索をいい感じに書きたい

Last updated at Posted at 2023-01-27

バージョン 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

こういう雰囲気のコード書くの久しぶりなので、頭の体操になって楽しかった〜

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?