LoginSignup
3
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-06

はじめに

この記事は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を使わずに検索機能を作ることは今後ほぼ無いでしょうが、作る過程で新しい知識を得ることができました。
世の検索機能はどういう実装になっているのか気になります。

3
0
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
3
0