アプリ開発を2週間で終わらせると宣言して、現在5週目の者です。見通しの甘さと、作り始めると予想以上に入れ込んでしまった結果ここまで開発期間が伸びてしまいました。今週末で一旦アプリ開発を終了する予定です。その後公開予定ですのでよろしかったら使ってみてください。
今日は検索機能を強化するため、ransackという検索機能を楽々実装できるGemを利用しました。
私のアプリの具体的な弱点は、文字検索とソート検索が両立しなかった点です。これを解決しました。
Gifも一応載せておきます。やっていることは文字検索をし、その後ソートボタンを押すと、文字検索結果内でソートが実行されます。
https://gyazo.com/6d1b192a90647826c86b75bc1cea5f63
今回の問題(タイトルの具体的な内容)
roomとcommentテーブルがあり、room has_many:commentsというアソシエーションがあります。
今回はcommentの多い順にroomを並び替えたいので、roomのコメントを取得し、カウントした上でそのカウント結果(統計カラムのこと)で並び替えたいというものです。
難しい点は2つ。
①文字検索の結果を維持する。
②その結果に対してソートをする。
結論
1つソートを実装する場合、1つ新しくそのソート専用のルーティングを作ります。
ソートボタンのvalueに検索結果をあらかじめ渡しておきます。
コントローラー内で検索とソートを同時に行います。
resources :rooms do
collection do
get :sort_comments
end
end
#検索フォーム
<%= search_form_for Room.ransack(params[:q]), url: root_path, class: "search-form" do |f| %>
<%= f.text_field :title, placeholder: "検索する",value: @value%> #検索後、検索した文字がフォームに残るようにする。
<%= f.button type:"submit"%>
<% end %>
#ソート部分
<%= search_form_for Room.ransack(params[:q]), url: sort_comments_rooms_path do |f| %>
<%= f.hidden_field :title, value: @value%> #事前に検索している文字を渡す
<%= f.submit 'コメントが多い順'%>
<% end %>
def sort_comments # コメントが多い順
#or検索の条件式、今回の主役ではない
if params[:q]&.dig(:title)
squished_keywords = params[:q][:title].squish
params[:q][:title_cont_any] = squished_keywords.split(" ")
end
#ransackを利用しつつ、他テーブルで計算しつつソート
@q = Room.joins(:comments).ransack(params[:q])
@rooms = @q.result.group(:room_id).order('count(text) desc').page(params[:page]).per(25)
@value = params[:q]&.dig(:title)
render 'index'
end
背景説明
初めに、ransackでは文字検索機能や文字検索とソートの両立を提供してくれています。ソート機能に関してはsort_linkというメソッドを利用すると、素早くわかりやすく実装することができます。加えて、sort_linkメソッドでは他テーブルのカラムを利用したソートも利用することができます。しかし、今回のようなテーブルでデータを集計して、その集計結果をもとに元テーブルを並び替えるというようなやや複雑なソートを実装する方法が見つからなかったので、今回記事にすることにしました。
今回やりたいことは「検索した結果を維持しつつ、roomをコメントの数の多さでソートする」です。
前提として、ソートと検索フォームは同じページ内にあリマス。
大まかな手順は
①ソート専用のルーティング作成
②viewで検索フォーム作成とそれに対応するアクションの作成
③viewでソート部分の作成
④ソートのコントローラー部分の作成
といった感じです。
①ソート専用のルーティング作成
ルーティング部分にコメントソートのためのルーティングを作成。
Rails.application.routes.draw do
root to: "rooms#index"
resources :rooms do
collection do
get :sort_comments
end
end
end
# プレフィックスはsort_comments_rooms_pathとなる
②viewで検索フォーム作成とそれに対応するアクションの作成
@valueはコントローラー内で定義、中身は検索した文字を代入したもの。
#検索結果一覧はroot_pathにて確認可能。
<%= search_form_for Room.ransack(params[:q]), url: root_path, class: "search-form" do |f| %>
<%= f.text_field :title, placeholder: "検索する",value: @value%>
<%= f.button type:"submit"%>
<% end %>
if部分はor検索を実装するための記述。
if params[:q]&.dig(:title)
squished_keywords = params[:q][:title].squish
params[:q][:title_cont_any] = squished_keywords.split(" ")
end
@q = Room.includes(:owner).ransack(params[:q]) #.includes(:owner)はなくてもいい
@rooms = @q.result
@value = params[:q]&.dig(:title) #検索した文字列を代入している
③viewでソート部分の作成
ソート部分は検索結果の文字列を送信するため、search_form_forを利用。
<%= search_form_for Room.ransack(params[:q]), url: sort_comments_rooms_path do |f| %>
<%= f.hidden_field :title, value:@value%> #検索した文字列をvalueに渡す
<%= f.submit 'コメントが多い順'%>
<% end %>
④ソートのコントローラー部分の作成
f.hidden_fieldで渡したパラメーターで再度検索のし直しと、並び替えを実行。
・roup(:room_id)
⇨各コメントがどのルームにあるか確認し、ルームナンバーが同じコメントごとにグループ分けしている。
・order('count(text) desc')
⇨グループごとにコメント(text)の個数を数えて、その結果を元にルームを降順(コメントの多い順)に並び替えている
def sort_comments
# or検索のための記述
if params[:q]&.dig(:title)
squished_keywords = params[:q][:title].squish
params[:q][:title_cont_any] = squished_keywords.split(" ")
end
# or検索のための記述終わり
@q = Room.joins(:comments).ransack(params[:q]) #commentテーブルとroomテーブルを結合し、文字列検索
@rooms = @q.result.group(:room_id).order('count(text) desc') #検索結果をgroupとorderを利用し、コメントの多い順に並び替え。
@value = params[:q]&.dig(:title) #viewに渡す検索文字を代入
render 'index' #root_pathへ遷移している
end
まとめ
実装し終わって思ったことですが、検索が二重に行われていることに釈然としていません。検索結果は既に入手しているのに、ソートする際に再度検索し直しているため、効率が悪いなと思っていますが、現状解決策が見つからず。リクエストに検索結果を送るような仕組みがあればできるかもしれないと思うのですが、今日は力尽きたのでここまで。未来の自分に丸投げします。
参考サイト