はじめに
この記事はRailsの勉強で作成していたタスクを管理するシステム(To doアプリのようなもの)に
検索機能を実装するための試行錯誤をまとめたものです。
環境
- Ruby(2.6.3)
- Rails(5.2.3)
検索対象(Taskモデル)
論理名 | 物理名 |
---|---|
件名 | subject |
本文 | body |
検索フォーム
<%= 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 %>
コントローラー
def index
@tasks = Task.search_by_keywords(params[:q])
end
第一次検索処理
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が発行される問題が発生してしまった。
ロジックは一生懸命考えたものなのでどこかで使えたらいいなあと思っています。
第二次検索処理
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%'))
のようにくくられてほしい。
第三次検索処理
スコープを使うと解決した。
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を使わずに検索機能を作ることは今後ほぼ無いでしょうが、作る過程で新しい知識を得ることができました。
世の検索機能はどういう実装になっているのか気になります。