Rails Diary

プログラミングの学習記録です。

ransackを使った検索機能

課題の反省点

  • DRYを意識したところ返って文章量の多い実装になってしまった。
    →検索結果のみを表示するsearch.html.erbを作ったものの、search.html.erbの検索フォームからはブックマークした掲示板かどうかに関わらず検索できてしまった。

  • 検索フォームへの入力がなかった場合は一覧が表示されるという仕組みに気が付かなかった。

自分の実装(失敗)

① collectionを使って掲示板のルーティング内にsearchアクションへのルートを作成

② コントローラにフォームへの入力内容を受け取るプライベートメソッドを記載

private
# 掲示板用
def set_q
  @q = Board.ransack(params[:q])
end
# ブックマークした掲示板用
def set_q_bookmark
  @q = current_user.bookmark_boards.ransack(params[:q])
end

③ privateメソッドを下記のように指定

before_action :set_q, only: %i[index search]
before_action :set_q_bookmark, only: %i[bookmarks]

④ searchアクション作成

def search
  @results = @q.result
end

掲示板のindexとbookmarksビューに検索フォームを作成

<%= search_form_for @q, url: search_boards_path do |f| %>
  <div class="input-group mb-3">
    <%= f.search_field :title_or_body_cont, placeholder: (t'defaults.search_field') %>
       <div class="input-group-append">
         <%= f.submit class: "btn btn-primary" %>
        </div>
     </div>
    <% end %>

⑥ 検索結果を表示するビュー(search.html.erb)を作成

<% if @results.present? %>
  <%= render @results %>
<% else %>
   <p><%= t('.no_result')%></p>
 <% end %>

これでindex,bookmarksそれぞれの検索フォームから検索すると想定した検索結果が返ってきたが、わざわざ作成してしまった検索結果表示ビューの検索フォームから再度検索をかけると、ちぐはぐな結果が返ってくるように・・・

改善点

  • パーシャルにすべきは検索フォーム
    →一度試してみたものの、indexとbookmarksでurlをどう分けるのか考え付かず。
<%= search_form_for @q, url: url do |f| %>
<% end %>

↓挿入するhtmlによってレンダリングのurlオプション内容を分ければ、どのURLでも対応できる

<%= render 'search_form', url: boards_path %>

ちなみにrailsでは裏でrequestオブジェクト(クライアントからサーバーへ送られるリクエストの諸情報が入っている)が作成されており、この中には現在アクセスしているパスが入っている。request.pathで取得できるので、URL部分にこちらを記載することでも想定した挙動をするが、詳細画面のようなidが含まれる場合にurlオプションの形式に合わなくなるので、汎用性の面で採用しないとのこと。

ransackの全件取得

ransackはparams[:q]で渡ってきた値が何もなかった場合は全件取得する!!

@q = Board.ransack(params[:q])

@boards = Board.all
# or
@search_boards = @q.result

この仕組みが分からずに、全件取得用の変数を作って、検索された内容を表示する変数を作って、ビューで長々と分岐させてやっていたけれどransackの機能で全件取得にも対応しているので1行でかける◎

distinct: trueとは

@q = Board.ransack(params[:q])
@boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])

文字を見てわかる通り重複に関する記載だと分かる。

これはある掲示板を子要素のコメントで絞り込んだ時の検索結果の重複を防ぐ目的がある。 子要素から親要素を絞り込む場合、以下のようにテーブルが結合される

https://i.gyazo.com/e1983009068cb03eaeaf677db9157ae2.png

リンゴジュースを買ったという内容の掲示板Aに対し、「りんご美味しい」と「りんご不味い」という二つのコメントがついており、掲示板のレコードはコメントの数だけ作成されている。

このテーブルに対し、「りんご」というワードだけであいまい検索をかけた場合、そのワードはレコード二つに引っ掛かり、二つのレコードを検索結果として表示してしまう。考えてみれば分かるが、一般的な検索において同じ検索結果がいくつも表示されることは求められていないので、そういった重複を防ぐ目的でdistinct: trueをつける。(基本的につける癖があって良いとのこと)

ransack で .result(distinct: true) するとエラーになる場合 - yet another pudding

↑ちなみにエラーになることもあるらしいので、状況に応じて外せば良いのかと思う。

検索用の変数qについて

パーシャルとしてフォームなどを作成した場合、通常は汎用性という面でローカル変数を用いる。呼び出し元のビューでそのオブジェクトに対応するインスタンス変数を指定する形になるが、@qという変数においては、どんな状況(コンテキストという)においても変わらなさそう。@qのまま渡しても問題なさそう。

仮にローカル変数として渡す場合、search_form_forにqの変数とurlを渡す必要があるため、呼び出し元の記述がやや冗長になってしまう。(個人的にはあんまり気にならない)

<%= render 'search_form', q: @q, url: boards_path %>

一方で様々なコンテキストが想定され、柔軟な変更が求められる場合は使い回しができる設計にする。

参考にしたサイト

【Rails】 ransackを使って検索機能がついたアプリを作ろう! | Pikawaka