Edited at

Ruby on Railsでgemを使わずに絞り込み検索機能を作ってみた


はじめに

この記事はRailsの勉強で作成していたタスクを管理するシステム(To doアプリのようなもの)に

検索機能を実装するための試行錯誤をまとめたものです。


環境


  • Ruby(2.6.3)

  • Rails(5.2.3)


検索対象(Taskモデル)

論理名
物理名

件名
subject

本文
body


検索フォーム


index.html.erb

<%= form_tag root_path, method: :get do %>

<%= text_field_tag 'q', params[:q], class: "form-control" %>
<%= submit_tag '検索', class: "btn page-link text-dark d-inline-block" %>
<% end %>


コントローラー


tasks_controller.rb

def index

@tasks = Task.search_by_keywords(params[:q])
end


第一次検索処理


task.rb

class << self

def search_by_keywords(q)
# nil対策
return all if q.blank?
# 一つ以上の空白文字で区切って配列に。末尾の空白文字も除去される
keywords = q.split(/[[:blank:]]+/)
# 先頭の空白は除去されず無の要素ができる可能性があるので消しておく
keywords.delete('')

tasks = []
keywords.each do |keyword|
# 件名か本文に検索文字が部分一致したレコードを詰めていく
tasks += Task.where('subject LIKE ?', "%#{keyword}%").or(Task.where('body like ?', "%#{keyword}%"))
end
# 全キーワードに該当したレコードを抽出
tasks.group_by(&:itself).select { |k, v| v.count == keywords.count }.keys
end
end



問題点


  • 戻り値がArray型

  • 検索キーワードの数だけSQLが発行される

検索キーワードが件名か本文に含まれるレコードを片っ端から配列に詰めて

検索キーワードと同じ数だけ重複してるものを抜き出すようにしてみた。

そのため検索キーワードの数だけSQLが発行される問題が発生してしまった。

ロジックは一生懸命考えたものなのでどこかで使えたらいいなあと思っています。


第二次検索処理


task.rb

class << self

def search_by_keywords(q)
return all if q.blank?
keywords = q.split(/[[:blank:]]+/)
keywords.delete('')

keywords.inject(all) do |relation, keyword|
relation.where('subject LIKE ?', "%#{keyword}%").or(where('body LIKE ?', "%#{keyword}%")
end
end
end


一つ目の検索キーワードを含むタスクをとってきてさらにその中から二つ目の検索キーワードを含むタスク…といった具合に絞り込みをかける。

しかもACtiveRecordさんが仕事をしてくれて発行されるSQLは一つだけ!


しかし…

おかしなSQLが発行されてしまっていた。

SELECT "tasks".* FROM "tasks" WHERE ((((subject LIKE '%a%') OR (body LIKE '%a%')) AND (subject LIKE '%b%') OR (body LIKE '%b%')) AND (subject LIKE '%c%') OR (body LIKE '%c%'))

括弧でのくくられかたが変。

((subject LIKE '%hoge%')OR(body LIKE '%hoge%'))AND((subject LIKE '%huga%')OR(body LIKE '%huga%'))

のようにくくられてほしい。


第三次検索処理

スコープを使うと解決した。


task.rb

scope :search_task,    -> (keyword) { search_subject(keyword).or(search_body(keyword)) }

scope :search_subject, -> (keyword) { where('subject LIKE ?', "%#{keyword}") }
scope :search_body, -> (keyword) { where('body LIKE ?', "#{keyword}") }

class << self
def search_by_keywords(q)
return all if q.blank?
keywords = q.split(/[[:blank:]]+/)
keywords.delete('')

keywords.inject(all) do |relation, keyword|
relation.search_task(keyword)
end
end
end


SELECT "tasks".* FROM "tasks" WHERE ((subject LIKE '%a%') OR (body LIKE '%a%')) AND ((subject LIKE '%b%') OR (body LIKE '%b%')) AND ((subject LIKE '%c%') OR (body LIKE '%c%'))


まとめ

gemを使わずに検索機能を作ることは今後ほぼ無いでしょうが、作る過程で新しい知識を得ることができました。

世の検索機能はどういう実装になっているのか気になります。