Programming Journal

学習したことの整理用です。

ransackで検索機能を実装する

掲示板の検索機能を実装したい

掲示板の一覧画面に以下のような検索フォームを配置し、入力したワードを含むタイトルor本文を持つ掲示板を表示したい。

f:id:Study-Diary:20200824160222p:plain
検索フォーム

要件

  • 掲示板一覧画面の検索フォームからは、全ての掲示板から検索できること

  • 掲示板ブックマーク一覧の検索フォームからは、お気に入りした掲示板の中から検索できること

  • タイトル(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部分。
フォームの部分とボタン部分をこのコードでそれぞれ囲ってあげます。

Input group · Bootstrap


<%= f.search_field :title_or_body_cont,
                        class: 'form-control',
                        placeholder: '検索ワード' %>

タイトルと本文、両方から検索するのはどうすればいいんだろうと数十分悩んで、フォームを別々に作ったり無駄なことをしてしまいましたが、
title_or_body_contこの書き方で両者を指定できます。
contcontainのこと。
含まれているものを検索する、つまり部分一致の検索、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