ransackで検索機能を実装する
掲示板の検索機能を実装したい
掲示板の一覧画面に以下のような検索フォームを配置し、入力したワードを含むタイトルor本文を持つ掲示板を表示したい。
要件
タイトル(title)と本文(body)を検索対象に含むこと
ransackを導入する
ransackとは、簡単に検索フォームを作成できるgemです。
GitHub - activerecord-hackery/ransack: Object-based searching.
さっそく、Gemfileに追記してから、ターミナルで$ bundle install
します。
gem 'ransack'
公式のUsageに沿って実装していきます!
Controller
今回検索フォームを配置する、掲示板一覧(index)とお気に入り一覧(bookmarks)部分を修正していきます。
def index #掲示板一覧 @q = Board.ransack(params[:q]) @boards = @q.result(distinct: true).includes(%i[user bookmarks]).order(created_at: :desc).page(params[:page]) end ・ ・ ・ def bookmarks #お気に入り一覧 @q = current_user.bookmark_boards.ransack(params[:q]) @bookmark_boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page]) end
検索対象がなかったら(検索フォームからリクエストが送られなければ)、全件を返します。
これを最初理解しておらず、全件取得のコードも別に必要なのかと勘違いして悩みました。
params[:q]
でフォームで検索入力した文字列を取ってくる
@q.result
で検索結果を渡している。
distinct: true
オプションを使えば、結果の重複を防ぐことができる。
includes
何度もSQLを発行するN+1問題が起こらないように、関連するデータも含めて取得している。
.page(params[:page]
前回実装したページネーション部分
Views
毎回一番苦労するのがviewの部分です。HTMLとBootstrapの知識不足が酷いです。
検索フォーム部分を作成していきます。
掲示板一覧画面とお気に入り一覧画面があるので、 フォーム画面を共通のパーシャルとして読み込む形にします。
<%= search_form_for q, url: url do |f| %> <div class='input-group mb-3'> <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: '検索ワード' %> <div class="input-group-append"> <%= f.submit t('defaults.search'), class: 'btn btn-primary' %> </div> </div> <% end %>
細かいところをみていきます。
ransackのsearch_form_for
ヘルパーを使用します。
第一引数に、先程定義した検索オブジェクトq
を渡します。
url
オプションを設定し、リクエストするUrlを指定します。 ( 後でrenderのときに値を渡します。)
私はこれを設定していなかったので、ブックマーク一覧から検索しても絞り込み検索ができずに1時間くらい溶かしました。
urlオプションについて
今回は指定対象がbookmarks_boards_path
だけだったので、検索フォームからrenderするときにurlを渡しているが、
同じ検索フォームを使い回しし、ページによって検索対象が変わるときには、url: request.path_info
と渡す
URLのホスト名以降のパス文字列を取得してくれる(クエリ文字は含まない)
例えば、「タスク完了・全て・未完了」をそれぞれ同じindex.html.erbを使用するとき、コントローラーで@urlでそれぞれdone_tasks_path
all_tasks_path
tasks_path
を指定するのではなく、このようにしたほうがスッキリする
<%= search_form_for @q, url: request.path_info do |f| %>
<div class='input-group mb-3'> <div class='input-group-append'>
Bootstrap部分。
フォームの部分とボタン部分をこのコードでそれぞれ囲ってあげます。
<%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: '検索ワード' %>
タイトルと本文、両方から検索するのはどうすればいいんだろうと数十分悩んで、フォームを別々に作ったり無駄なことをしてしまいましたが、
title_or_body_cont
この書き方で両者を指定できます。
cont
はcontain
のこと。
含まれているものを検索する、つまり部分一致の検索、LIKE演算子です。
placeholder
でフォームに予め灰色のテキストを仕込んでおけます。
パーシャルを読み込む
掲示板一覧画面とお気に入り一覧画面で、先程作った検索フォームのパーシャルを読み込んでいきます。
<!-- 検索フォーム --> <%= render 'boards/search_form',url: bookmarks_boards_path, q: @q %>
<!-- 検索フォーム --> <%= render 'boards/search_form', url: boards_path, q: @q %>
パーシャルにはインスタンス変数は使用しないので、renderするときのlocalオプションで、パーシャルで使うローカル変数に値を渡してあげます。
参考
Ransackで簡単に検索フォームを作る73のレシピ - 猫Rails
ransack で複数カラムを検索する - rochefort's blog
https://api.rubyonrails.org/v5.2.4/classes/ActionView/Helpers/FormHelper.html