Railsアプリでransackを利用し検索機能を実装する

# はじめに
プログラミング初心者で現在ポートフォリオを作成しています。
ユーザービリティを考慮すると検索機能はほぼ必須と考え、実装をしました。
今回はgemの`ransack`を用いてrailsアプリに検索機能の実装方法をまとめたいと思います！

# 前提（開発環境）

* macOS Catalina
* Ruby 2.6.5
* Ruby on Rails 6.0.0
* Visual Studio Code
* Mysql

# この記事のゴール

* [実装イメージgif](https://gyazo.com/49971f802b4293c22761d2e40e67c9a9)

# 目次
[1.事前準備](#1-事前準備)
[2.routingの設定](#2-routingの設定)
[3.controllerの設定](#3-controllerの設定)
[4.viewの記述](#4-viewの記述)

## 1. 事前準備
まずはER図を御覧ください。
今回はuserモデルに紐付いたイベントモデルを例として用います。
このイベントモデルの`genreカラム`で検索をかけるという設定で実装を進めます。
![スクリーンショット 2020-12-19 19.52.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/772095/05c3f1fe-7822-1a37-59d7-0f24b7422c8d.png)
余談ですが、このgenreカラムはイベントをnewするときにヘルパーメソッド`form_with`の`f.selectタグ`を用いてデータベースに保存しています。

抜粋してコードを載せておきます。

```php:events/new.html.haml
.create-item
  = f.label :genre, "ジャンル", class: "Form-label"
  = f.select :genre, [ ["ダンス", "ダンス"], ["歌", "歌"], ["撮影モデル", "撮影モデル"], ["楽器演奏", "楽器演奏"], ["演技", "演技"], ["サロン・カットモデル", "サロン・カットモデル"], ["エキストラ", "エキストラ"], ["その他", "その他"] ], prompt: "募集ジャンルを選択してください", class: "middle-field"
```
form_withの使い方はこの記事が参考になります！
[【Rails】form_withの使い方を徹底解説！](https://pikawaka.com/rails/form_with)

一番大事なポイントは`["ダンス", "ダンス"]・・・`の部分です。
文法的には`["viewに表示される文字","データベースに保存される値"]`となるので保存される値を文字列にしないと検索ができなくなってしまうので注意してください。

それでは、本題に入ります。

```php:Gemfile
gem 'ransack'
```
忘れずに`bundle install`と`サーバーの再起動`をしましょう！
後にエラーが起きてしまいます。

## 2. routingの設定
検索結果用の画面に移行するために、route.rbにget :searchを追加します。
今回はeventのindex画面で検索を行うという設定ですが、index画面以外でも検索は行えるように実装します。
多いパターンだとアプリにヘッダーに検索窓を設置するみたいですね！

```php:routes.rb
resources :events, only: [:index, :new, :create] do
  collection do
    get :search, to: 'events#search'
  end
end
```
searchアクションは個別のidは必要ないのでmemberではなく`collection`を用います。
`do`と`end`は忘れがちなのでネストするときは注意しましょう。

## 3. controllerの設定

```php:application_controller.rb
before_action :set_search

def set_search
  @search = Event.ransack(params[:q]) #ransack用の検索メソッド
  @search_events = @search.result(distinct: true).order(created_at: "DESC").includes(:user)
end
```
events_controller.rbではなくapplication_controller.rbにアクションを定義する理由は、**すべてのページで検索を行えるようにするため**です。先程述べたように、多くの場合はヘッダーに検索窓を設置するのでこの仕様が適切だと思います。

### **Event.ransack(params[:q])**
`q`はransack用のパラメーターです。
このパラメーターを受け取れるようにコントローラーに記述します。

### **@search.result(distinct: true).order(created_at: "DESC").includes(:user)**
`distinct: true`は重複するレコードを取得しないようにするオプションです。
`order(created_at: "DESC")`は新しく作成された順にイベントを表示します。
`includes(:user)`は**N+1問題**を解決するために記述します。

```php:events_controller.rb
class EventsController < ApplicationController
  def search
　　@events = Event.all.includes(:recruiter) #この部分はviewのセクションで説明します
  end 
end
```
先程設定したルーティングに沿う形で、`events_controller.rb`にserachアクションを定義します。

## 4. viewの記述
今回は検索窓をindex.html.hamlに記述しますが、ここは任意のファイルで大丈夫です！

```php:events/index.html.haml
.search-area
  = search_form_for @search, url: search_events_path do |f|
    = f.text_field :genre_cont, placeholder: 'ジャンルで検索', class: "search-form" 
    = f.submit ("イベントを検索") , class: 'search-btn', class: "search-btn"
```

```php:_event.scss
.event_search {
      display: flex;

      .search-form { #search-formはsearch_form_forのクラスです
        border: 1px solid lightgray;
        outline: none;
        border-radius: 50px;
        background: #f4f8f9;
        height: 50px;
        width: 80%;
        padding: 10px 20px;
        margin-right: 30px;
      }
      .search-btn {
        color: #fff;
        outline: none;
        border: none;
        border-radius: 50px;
        background: #2e9a9e;
        height: 50px;
        width: 20%;
        padding: 10px 20px;
      }
      .search-btn:hover {
        opacity: 0.7;
      }
    }
```


### **url: search_events_path**
ルーティングの記述でsearchアクションをcollectionでネストするとパスはこの通りになります。

### **= f.text_field :genre_cont**
`_cont`はcontainの省略形で、部分一致を表します。
検索ワードとカラムの文字列で部分的に一致しているイベントがあれば検索結果に表示されます。

これで検索窓を設置できたので、最後に検索結果のviewを記述します。

```php:events/search.html.haml
.search-area
  = search_form_for @search, url: search_events_path do |f|
    = f.text_field :genre_cont, placeholder: 'ジャンルで検索', class: "search-form" do
    = f.submit ("イベントを検索") , class: 'search-btn', class: "search-btn"

 .EventField
  // 検索キーワードなし: 空白だとスペースを入れいてるgenre全部がヒットしてしまうため
  - if params[:q]['genre_cont'] == ""
    %p.search-result
      = "検索キーワードがありません。"
    // 検索結果ありの場合
  - elsif @search_events.present? 
    %p.search-result
      = "全#{@events.count}件中#{@search_events.count}件の表示"
    // 検索数0の場合
  - else
   %p.search-result
     = "検索に一致するイベントはありませんでした"  
  
  #表示内容は任意で記述してください    
  - @search_events.each do |event|
    = event.event_name
    = event.genre
```
順番に解説していきます。

```php:events/search.html.haml
// 検索キーワードなし: 空白だとスペースを入れいてるgenre全部がヒットしてしまうため
  - if params[:q]['genre_cont'] == ""
    %p.search-result
      = "検索キーワードがありません。"
    // 検索結果ありの場合
  - elsif @search_events.present? 
    %p.search-result
      = "全#{@events.count}件中#{@search_events.count}件の表示"
    // 検索数0の場合
  - else
   %p.search-result
     = "検索に一致するイベントはありませんでした"  
```
この部分は検索結果の条件分岐です。
ポイントは真ん中の部分です。
### **- elsif @search_events.present?**
`events_controller.rb`に記述したインスタンス変数`@events`と
`application_controller.rb`に記述したインスタンス変数`@search_events`を使っています。
ここで`count`メソッドを用いてデータベースに登録されているイベントのうち、条件に一致したイベントが何件表示されるかという記述です。

### **- @search_events.each do |event|**
検索条件に一致したイベントを繰り返し処理によってviewに表示します。
この部分はお好みでカスタマイズしましょう。

以上で実装は終了です！！

# おわりに
さいごまで読んでいただきありがとうございました！
お疲れさまでした。。

## 参考文献
[【Ruby on Rails】gem 'ransack' を利用した検索機能/フォームの実装]
(https://qiita.com/gyu_outputs/items/6cad794b4ca3868e976e)
[【Rails6】Ransackが学べるソート機能付きミニアプリを作成してみた](https://techtechmedia.com/ransack-app-rails/)