🐙

Administrate gemのキーワード検索はカラムごとにOR検索だった

に公開

administrateデモアプリで実装されている検索機能はOR検索。また、複数語指定の場合はすべての語句をいずれかのカラムが含む検索となっている。

具体的なSQLのクエリ構築箇所は、以下。
https://github.com/thoughtbot/administrate/blob/main/lib/administrate/search.rb#L86-L94

query_templateメソッドで各attributesをループしてORのSQLクエリを構築している。

    def query_template
      search_attributes.map do |attr|
        table_name = query_table_name(attr)
        searchable_fields(attr).map do |field|
          column_name = column_to_query(field)
          "LOWER(CAST(#{table_name}.#{column_name} AS CHAR(256))) LIKE ?"
        end.join(" OR ")
      end.join(" OR ")
    end

OR検索を検証してみる。複数語指定の場合はすべての語句が含まれていることが検索条件。

# query.rb
require 'active_record'
require 'sqlite3'

# ロギング
ActiveRecord::Base.logger = Logger.new($stdout)

# データベース接続の設定
ActiveRecord::Base.establish_connection(
  adapter: 'sqlite3',
  database: 'db/development.sqlite3'
)

# テーブルの作成
ActiveRecord::Schema.define do
  drop_table :customers, if_exists: true
end

ActiveRecord::Schema.define do
  create_table :customers do |t|
    t.string :name, null: false
    t.string :email, null: false
    t.string :phone
    t.text :notes
    t.boolean :active, default: true, null: false
    t.timestamps

    t.index [:email], name: "index_customers_on_email", unique: true
  end
end

class Customer < ActiveRecord::Base
  SEARCHABLE_COLUMNS = [:name, :email, :phone, :notes]

  def self.search_template
    SEARCHABLE_COLUMNS.map do |column_name|
      "LOWER(CAST(#{column_name} AS CHAR(256))) LIKE ?"
    end.join(' OR ')
  end

  def self.query_values(query)
    ["%#{query.downcase}%"] * SEARCHABLE_COLUMNS.length
  end

  def self.search(query)
    where(search_template, *query_values(query))
  end
end

Customer.create(name: 'Alice', email: 'alice@example.com', phone: '123-4567', notes: 'This is Alice.')
Customer.create(name: 'Bob', email: 'bob@example.com', phone: '123-4567', notes: 'This is Bob.')

puts Customer.search('alice').count
puts Customer.search('alice bob').count

検索結果は以下の通り。

puts Customer.search('alice').count
=> 1
puts Customer.search('alice bob').count
=> 0

複数語のいずれかが含まれていれば検索対象となるようにORクエリを語句分生成するように修正する。

class Customer < ActiveRecord::Base
  SEARCHABLE_COLUMNS = [:name, :email, :phone, :notes]

  def self.search_template(query)
    query.split.flat_map do |word|
      SEARCHABLE_COLUMNS.map do |column_name|
        "LOWER(CAST(#{column_name} AS CHAR(256))) LIKE ?"
      end.join(' OR ')
    end.join(' OR ')
  end

  def self.query_values(query)
    query.split.flat_map do |word|
      ["%#{word.downcase}%"] * SEARCHABLE_COLUMNS.length
    end
  end

  def self.search(query)
    where(search_template(query), *query_values(query))
  end
end

Customer.create(name: 'Alice', email: 'alice@example.com', phone: '123-4567', notes: 'This is Alice.')
Customer.create(name: 'Bob', email: 'bob@example.com', phone: '123-4567', notes: 'This is Bob.')

puts Customer.search('alice').count
puts Customer.search('alice bob').count
puts Customer.search('alice').count
=> 1
puts Customer.search('alice bob').count
=> 2

参考

デモアプリソースコードは
https://github.com/thoughtbot/administrate/tree/main/spec/example_app
に配置されている。

Discussion