【Rails】ransackを用いて検索機能を実装する方法

「【Rails】ransackを用いて検索機能を実装する方法」のアイキャッチ画像

本記事では、シンプルなコードで様々な検索フォームを実装できるgem「ransack」を用いた検索機能の実装手順を詳しく解説します。gemを使用しない、LIKE句を利用したあいまい検索機能の実装方法は、こちらをご覧ください。


完成画面


・あいまい検索

・並び替え検索

・範囲検索


ransackを用いた検索機能実装の流れ


ransackを用いた検索機能は、大まかに以下の流れで実装します。

①事前準備(ransackのインストール)
②検索フォーム作成
③コントローラ作成[searches_controller.rb]
④ルーティング設定
⑤検索結果画面の作成

実装手順

⓪はじめに


コーディングはhaml/Scssを利用しています。

商品情報一覧が格納されている「itemsテーブル」から、様々な検索方法を通して任意の商品情報を検索、表示させる実装を行なっています。

また検索機能におけるMVCの流れ概念図は、以下記事をご参照ください。



①事前準備(ransackのインストール)


今回の検索機能の実装では、様々な検索機能を短いコードで簡単に作成できる「ransack」というgemを使用します。ransackを使用するには、以下を実行しましょう。

gem 'ransack'
$ bundle install


②検索フォーム作成


まず、以下要素を備えた検索フォームを作成します。

□ 並び替え検索
  - 価格の安い/高い順
  - 出品の古い/新しい順
□ あいまい検索
  - キーワードを追加する
  - ブランド名から探す
□ 範囲検索
  - 価格
□ 商品の状態
  - 配送料の負担
  - 販売状況


完成イメージは以下になります。(開閉ボタンクリックで画像が表示されます)


コードは以下になります。(開閉ボタンクリックでコードが表示されます)

.searches
  = search_form_for @search, url: items_searches_path do |f|
    -# 並び替え検索
    .searches-head
      %i.fas.fa-angle-down.icon
      = f.select( :sorts, { '並び替え': 'id desc', '価格の安い順': 'price asc', '価格の高い順': 'price desc', '出品の古い順': 'updated_at asc', '出品の新しい順': 'updated_at desc' } , { selected: params[:q][:sorts] } , { onchange: 'this.form.submit()'})
    .searches-body
      .search-body__subtitle
        詳細検索
      -# あいまい検索(キーワード)
      .form-group
        %label
          %i.fas.fa-plus.icon
          %span
            キーワードを追加する
        = f.search_field :name_cont, class: "input-default", placeholder: '例)値下げ'
      -# あいまい検索(ブランド名)
      .form-group
        %label
          %i.fas.fa-tag.icon
          %span
            ブランド名から探す
        = f.search_field :brand_brand_cont, class: "input-default", placeholder: '例)シャネル'
      -# 範囲検索(価格)
      .form-group
        %label
          %i.fas.fa-coins.icon
          %span
            価格
        .form_money
          = f.number_field :price_gteq, placeholder: "¥ Min"
          %span.form_money__fromTo ~
          = f.number_field :price_lteq, placeholder: "¥ Max"
      -# 絞り込み検索(商品の状態)
      .form-group
        %label
          %i.fas.fa-star.icon
          %span 商品の状態
        .checkbox
          .checkbox-default
            %label
              %input{type: "checkbox"}
              = 'すべて'
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :item_condition_id_in , {multiple: true} , "1", nil
              = "新品、未使用"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :item_condition_id_in , {multiple: true} , "2", nil
              = "未使用に近い"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :item_condition_id_in , {multiple: true} , "3", nil
              = "目立った傷や汚れなし"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :item_condition_id_in , {multiple: true} , "4", nil
              = "やや傷や汚れあり"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :item_condition_id_in , {multiple: true} , "5", nil
              = "傷や汚れあり"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :item_condition_id_in , {multiple: true} , "6", nil
              = "全体的に状態が悪い"
              %i.check-icon
      -# 絞り込み検索(配送料)
      .form-group
        %label
          %i.fas.fa-truck-moving.icon
          %span 配送料の負担
        .checkbox
          .checkbox-default
            %label
              %input{type: "checkbox"}
              = 'すべて'
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :postage_payer_id_in , {multiple: true} , "1", nil
              = "着払い(購入者負担)"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :postage_payer_id_in , {multiple: true} , "2", nil
              = "送料込み(出品者負担)"
              %i.check-icon
      -# 絞り込み検索(販売状況)
      .form-group
        %label
          %i.fas.fa-shopping-cart.icon
          %span 販売状況
        .checkbox
          .checkbox-default
            %label
              %input{type: "checkbox"}
              = 'すべて'
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :trading_status_in , {multiple: true} , "0", nil
              = "販売中"
              %i.check-icon
          .checkbox-default
            %label
              = f.check_box :trading_status_in , {multiple: true} , "1", nil
              = "売り切れ"
              %i.check-icon
      .search-done-btn
        = f.button "クリア", type: "reset", class: "btn-default btn-gray"
        = f.submit "完了", class: "btn-default btn-red"

検索フォームに入力された文字列を、items/searches#index に送信しています。
また完成イメージのとおり「並び替え検索」「キーワード、ブランド名によるあいまい検索」「価格による範囲検索」「商品の状態、配送料、販売状況による絞り込み検索」で構成しています。(それぞれの実装箇所はコメントアウトで示しています)

※「_searches_sidemenu.html.haml」は、「/items/searches/index.html.haml」から呼び出している、フォーム専用の部分テンプレートファイルです。
※このhtmlは一例ですので、適宜調整ください。


③コントローラ作成[searches_controller.rb]


searchesコントローラを作成します。

$ rails g controller items::searches 

items::searchrs について

itemsディレクトリ配下に items/searches_controller.rb を作成する記述です。

本アプリケーションにおける「検索」は、「商品の検索」「ユーザーの検索」など複数の種類が考えられます。そして今回は「商品の検索」機能を実装しますが、それを区別できるよう、「商品」に関するitemsディレクトリを用意し、その配下に searchesコントローラを作成した、という訳です。


次に、コントローラに以下を記述します。
①の検索フォームに入力された文字列を params[:q] で受け取り、インスタンス変数 @search に代入します。それを @search.result としたものが、検索結果の値となります。値が取得できない場合は、binding.pryでデバックして原因を確認しましょう。

class Items::SearchesController < ApplicationController
  def search_params
    if params[:q].present?
      @search = Item.ransack(params[:q])
      @search_result = @search.result
      @search_word = @search.name_cont
    else
      params[:q] = { sorts: 'id desc' }
      @search = Item.ransack()
      @search_result = Item.all
    end
  end
end


④ルーティング設定


②のsearchesコントローラのindexアクションで検索した値を表示します。

namespace :items do
  resources :searches, only: :index
end

名前空間(namespace)について

今回、searchコントローラは、検索対象の区別のため、itemsディレクトリ配下(1つ下の階層)に作成しました。そのためルーティング設定方法も通常とは異なり、以下のように記述します。

namespace :  “③で名前空間に指定したコントローラー名”  do
 “ルーティング設定”
end



⑤検索結果画面の作成


検索結果画面です。


またこのページは、以下の階層構造で構成しています。
searches/index.html.haml にて、ヘッダー(*1)、検索結果(*2)、サイドメニュー(*3)を部分テンプレートで呼び出しています。

 items
    _header.html.haml (*1)
    searches
      index.html.haml
     _searches_main.html.haml (*2)
     _searches_sidemenu.html.haml (*3)


※このhtmlは一例ですので、適宜調整ください。

= render 'items/header'
  .searches_contents
    = render 'searches_sidemenu'
    = render 'searches_main'
= render 'items/camerabutton
.searches_main
  .searches_title
    - if @search_word.present?
      = @search_word
      %span の検索結果
    - else
      = "検索結果"
  - if @search_result.present?
    .searches_count
      = "1-#{@search_result.count}件表示"
    .searches_items
      - @search_result.each do |item|
        = link_to item_path(item) do
          %figure.searches_items__img
            - if item.trading_status == 1
              .itemSold
                SOLD
            - item.item_imgs.each do |item_img|
              = image_tag item_img.url.url
          .searches_item
            .searches_item__name
              = item.name
            .searches_item__details
              %ul
                %li
                  = "#{item.price.to_s(:delimited, delimiter: ',')}円"


おわりに


ransackを用いた検索機能の実装手順は以上になります。

実装完了後は、以下手順で単体テストも実施しましょう。


ransackはシンプルな高度で、様々な検索フォームを実装できるため、とても便利なgemですね。またgemを使用しない、LIKE句を使用した検索方法も以下でご紹介していますので、よろしければご参照ください。