記事概要
Ruby on Railsに複雑な検索機能を実装する方法について、まとめる
前提
- Ruby on Railsでアプリケーションを作成している
- 投稿機能を実装済みである
サンプルアプリ(GitHub)
想定する機能イメージ
ransack(Gem)
検索機能の実装を実現できるGem
オプション
検索時に使用できるオプション
オプション名 | 役割 |
---|---|
cont (contain) | 部分一致 |
eq (equal) | 完全一致 |
in | 複数の値(配列)のうちどれかと一致する |
gteq (greater than or equal to) | 以上 |
lteq (less than or equal to) | 以下 |
関連先モデル名_カラム名_オプション | 関連モデルのカラム名で検索をする |
メソッド
search_form_forメソッド
ransackのGemで用意されている、検索フォームを実装する際に使用するメソッド
form_with
のransackバージョンだと思うとイメージしやすい
<%= search_form_for @q, url: search_items_path, method: :get do |f| %>
<%= f.label :name_cont, '商品名' %>
<%= f.submit "検索" %>
<% end %>
手順1(Gemの導入)
- ransackのGemを導入する
詳細は、こちらを参照
手順2(ルーティングの設定)
- 検索ページのルーティングを設定する
config/routes.rb
Rails.application.routes.draw do devise_for :users root "items#index" resources :items, only: [:new, :create, :show, :edit, :update] do # 検索用ページのルーティング collection do get 'search' end end end
手順3(コントローラーの設定)
- コントローラーにsearchアクションを追記する
app/controllers/items_controller.rb
# 省略 def search # ransackにて検索オブジェクトを生成し、ransackを使用したフォームから送られてくるパラメーターを受け取る @q = Item.ransack(params[:q]) # 検索結果を取得 @items = @q.result end private # 省略
記述 意味 ransack 検索オブジェクトを生成する params[:q] ransackを使用したフォームから送られてくるパラメーターを受け取る result 検索結果を取得する
手順4(検索ページへの動線を設定)
- トップページに検索ページへのリンクを設けるため、
app/views/items/index.html.erb
を編集する<%= render "shared/header" %> <h3>トップページ</h3> <h3><%= link_to '商品出品', new_item_path%></h3> <%#=検索ページへのリンク %> <h3><%= link_to '商品検索', search_items_path%></h3> <% @items.each do |item| %> <div class="posted-content"> <%= image_tag item.image %><br> <%= item.name%><br> <%= link_to '詳細', item_path(item.id)%> </div> <% end %>
手順5(データ取り込み)
- seedファイルからデータを取り込むため、seedファイルを編集する
db/seeds.rb
test_user_1 = User.create(nickname: "太郎", email: "taro@taro", password: "tarotaro") test_user_2 = User.create(nickname: "花子", email: "hanako@hanako", password: "hanakohanako") item_1 = Item.new( name: "スマートフォン", category_id: 7, price: 50000, user_id: test_user_1.id ) item_1.image.attach(io: File.open(Rails.root.join("./app/assets/images/smartphone.png")), filename: 'smartphone.png') item_1.save item_2 = Item.new( name: "子供服", category_id: 3, price: 1000, user_id: test_user_2.id ) item_2.image.attach(io: File.open(Rails.root.join("./app/assets/images/clothes.png")), filename: 'clothes.png') item_2.save item_3 = Item.create( name: "ドライヤー", category_id: 7, price: 3000, user_id: test_user_2.id ) item_3.image.attach(io: File.open(Rails.root.join("./app/assets/images/hairdryer.png")), filename: 'hairdryer.png') item_3.save
- seedファイルを実行する
% rails db:seed
- データ登録できたことを、
Sequel Ace
で確認する
手順6(商品名で検索できるように実装する)
- 検索フォームの大枠を作成するため、
app/views/items/search.html.erb
を編集する<%= render "shared/header" %> <h3>検索ページ</h3> <h3><%= link_to 'トップページへ戻る', root_path%></h3> <div class='item-contents' id="detailed-search-result-wrapper"> <%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <%= f.submit '検索' %> <% end %> <%#= 省略 >
- 商品名での検索欄を作成する
<%#= 省略 > <%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name_cont, '商品名' %> <br> <%= f.text_field :name_cont, placeholder: '商品名' %> </div> <%= f.submit '検索' %> <% end %> <%#= 省略 >
- パスワードなどの秘匿情報が不正に検索されてしまうのを防ぐため、検索対象のカラムを指定する
app/models/item.rb
# 省略 # ransackでの検索対象を指定 def self.ransackable_attributes(auth_object = nil) ["name", "price", "category_id"] end end
- ブラウザにて、商品名で検索できることを確認する
手順7(カテゴリでも検索できるように実装する)
- カテゴリでの検索欄を作成するため、
app/views/items/search.html.erb
を編集する<%= f.collection_select 第一引数, 第二引数, 第三引数, 第四引数, 第五引数 %><%#= 省略 > <%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name_cont, '商品名' %> <br> <%= f.text_field :name_cont, placeholder: '商品名' %> </div> <div class="search-field"> <%= f.label :category_id_eq, 'カテゴリ' %> <br> <%= f.collection_select(:category_id_eq, Category.all, :id, :name, {include_blank: "---"}) %> </div> <%= f.submit '検索' %> <% end %> <%#= 省略 >
引数 役割 今回の値 第一引数
(メソッド名)・カラム名
・name属性やid属性を決める:category_id_eq 第二引数
(オブジェクト)配列データを指定する
(今回はカテゴリーデータの配列)Category.all 第三引数
(value)表示する際に参照するDBのカラム名 id 第四引数
(name)実際に表示されるカラム名 name 第五引数
(オプション)何も選択していない時に表示される内容
(今回は「---」)include_blank - ブラウザにて、カテゴリで検索できることを確認する
手順8(価格でも検索できるように実装する)
- 価格での検索欄を作成するため、
app/views/items/search.html.erb
を編集する<%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name_cont, '商品名' %> <br> <%= f.text_field :name_cont, placeholder: '商品名' %> </div> <div class="search-field"> <%= f.label :category_id_eq, 'カテゴリ' %> <br> <%= f.collection_select(:category_id_eq, Category.all, :id, :name, {include_blank: "---"}) %> </div> <div class="search-field"> <%= f.label :price_gteq, '価格'%> <br> <%= f.number_field :price_gteq %> 円以上 <br> <%= f.number_field :price_lteq %> 円以下 </div> <%= f.submit '検索' %> <% end %>
- ブラウザにて、価格で検索できることを確認する
手順9(カテゴリの検索欄をチェックボックスにする)
- カテゴリーの検索欄をチェックボックスにするため、
app/views/items/search.html.erb
を編集する<%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name_cont, '商品名' %> <br> <%= f.text_field :name_cont, placeholder: '商品名' %> </div> <div class="search-field"> <%= f.label :category_id_in, 'カテゴリ' %> <br> <%= f.collection_check_boxes(:category_id_in, Category.all, :id, :name) %> </div> <div class="search-field"> <%= f.label :price_gteq, '価格'%> <br> <%= f.number_field :price_gteq %> 円以上 <br> <%= f.number_field :price_lteq %> 円以下 </div> <%= f.submit '検索' %> <% end %>
- 複数カテゴリーを選択して検索できることを確認する
- ターミナルを確認すると、配列でカテゴリーidが連携されていることが確認できる
手順10(出品者名でも検索できるように実装する)
- 出品者名での検索欄を作成するため、
app/views/items/search.html.erb
を編集する<%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name_cont, '商品名' %> <br> <%= f.text_field :name_cont, placeholder: '商品名' %> </div> <div class="search-field"> <%= f.label :user_nickname_cont, '出品者名' %> <br> <%= f.text_field :user_nickname_cont, placeholder: '出品者名' %> </div> <div class="search-field"> <%= f.label :category_id_in, 'カテゴリ' %> <br> <%= f.collection_check_boxes(:category_id_in, Category.all, :id, :name) %> </div> <div class="search-field"> <%= f.label :price_gteq, '価格'%> <br> <%= f.number_field :price_gteq %> 円以上 <br> <%= f.number_field :price_lteq %> 円以下 </div> <%= f.submit '検索' %> <% end %>
- ニックネームを検索対象に設定する
- Itemモデルに対して、Userモデルへのアソシエーションを検索対象とする設定を行う
app/models/item.rb
# 省略 # Itemsテーブルと紐づいたUsersテーブルがransacの検索対象になる def self.ransackable_associations(auth_object = nil) ["user"] end end
- Userモデルに対して、nicknameカラムを検索対象とする設定を行う
app/models/user.rb
# 省略 # Usersテーブルのnicknameカラムが検索対象に含まれる def self.ransackable_attributes(auth_object = nil) ["nickname"] end end
- Itemモデルに対して、Userモデルへのアソシエーションを検索対象とする設定を行う
- ブラウザにて、出品者名で検索できることを確認する
手順11(複数の商品名でも検索できるように実装する)
「カバン 子供」と入力した場合、「カバン 子供」という1つの文字列が商品名に含まれている商品を探してしまうため、何も検索に引っかからない。OR検索できるように実装する
- 商品名での検索欄を修正するため、
app/views/items/search.html.erb
を編集する。_cont
のオプションを削除する<%#= 省略 %> <%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name, '商品名' %> <br> <%= f.text_field :name, placeholder: '商品名' %> </div> <%#= 省略 %>
- 条件に当てはまる場合はパラメーターを修正するように記述する
app/controllers/items_controller.rb
# 省略 def search # params[:q]がnilではない且つ、params[:q][:name]がnilではないとき(商品名の欄が入力されているとき) if params[:q]&.dig(:name) # squishメソッドで余分なスペースを削除する squished_keywords = params[:q][:name].squish # 半角スペースを区切り文字として配列を生成し、paramsに入れる params[:q][:name_cont_any] = squished_keywords.split(" ") end # ransackにて検索オブジェクトを生成し、ransackを使用したフォームから送られてくるパラメーターを受け取る @q = Item.ransack(params[:q]) # 検索結果を取得 @items = @q.result end # 省略
- ブラウザにて、複数の商品名での検索ができることを確認する
- 商品名の入力値が保存されるようにするため、
app/views/items/search.html.erb
を編集する<%#= 検索フォーム %> <%= search_form_for @q, url: search_items_path, html: {id: "detailed-search-form"} do |f| %> <div class="search-field"> <%= f.label :name, '商品名' %> <br> <%= f.text_field :name, placeholder: '商品名', value: params[:q]&.dig(:name) %> </div>
- ブラウザにて、検索後も商品名の欄が空にならないことを確認する
手順12(ヘッダーの検索フォームから商品名検索をできるようにする)
- 商品名で検索できるようにするため、
app/views/shared/_header.html.erb
を編集する<header class='top-page-header'> <div class='search-bar-contents'> <%= search_form_for Item.ransack(params[:q]), url: search_items_path, html: {class: "search-form"} do |f| %> <%= f.text_field :name, placeholder: '商品名から探す', class: 'input-box' %> <br> <%= f.label :submit, class: "search-button" do %> <input type="submit" value="検索" class="send"> <%= f.submit :submit, id: "q_submit", style: "display: none;"%> <% end %> <% end %> </div> <!-- 省略 -->
- search_form_forの引数に、
Item.ransack(params[:q])
をセットしている理由-
_header.html.erb
は様々なページで使用されており、@q
を使用する場合、その都度@q
を定義しなくてはならない
-
- search_form_forの引数に、
- 検索ページ同様、複数の商品名での検索ができることを確認する