【Rails6】検索機能の実装

#はじめに

記事の検索・閲覧ができるアプリケーションを作成しています。
今回は検索機能を'ransack'というgemを使用して実装したため、備忘録・復習のため記述します。

#環境

Ruby on Rails '6.0.0'
Ruby '2.6.5'

#前提

記事の投稿(articleテーブル）は作成済みで、ユーザーは記事の閲覧ができる状態。

#ransackとは

シンプルな検索フォームを実装できると共に、高度な検索フォームも作成することができるgemです。
このransackを導入することで、以下のメソッドが使用できるようになります。

- _eqメソッド：条件にあった検索を行う
- _contメソッド：部分一致
- _iteqメソッド：「〜以下」という検索条件

他にも様々な検索に関するメソッドがありますが、今回は割愛します。


公式GitHubは[こちら](https://github.com/activerecord-hackery/ransack)です。

#①gemの導入

```ruby:Gemfile
gem 'ransack'
```

```ruby:ターミナル
% bundle install
```

上記コマンドでgemを導入します。

#②ルーティングの記述

```ruby:config/routes.rb
（中略）
get 'articles/search'
```

今回は、記事検索の機能を実装しているので、「articlesコントローラー」とします。
また検索機能は、「searchアクション」と命名しています。

#③コントローラー(articles)の記述

まず、全体を記載して、その後部分的に解説をしていきます。

```ruby:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :search_article, only: [:index, :search]
  def index
    @articles = Article.includes(:user).order('created_at DESC').limit(4)
    @ranks = Article.find(Like.group(:article_id).order('count(article_id) DESC').limit(4).pluck(:article_id))
  end

・・・(中略）・・・

  def search
    @results = @a.result(distinct: true).page(params[:page]).per(8).order('created_at DESC')
  end

  private

  def search_article
    @a = Article.ransack(params[:q])
  end
end
```

```ruby:app/controllers/articles_controller.rb
  private

  def search_article
    @a = Article.ransack(params[:q])
  end
```

こちらの記述で、キー（：q）を使って、articlesテーブルから記事情報を探しています。そして、「@a」という名前の検索オブジェクトを生成しています。
この処理を行うメソッド名を「search_article」とし、index,searchアクションのみで使用するため、before_actionでの「only」で限定しています

```ruby:app/controllers/articles_controller.rb
  def search
    @results = @a.result(distinct: true).page(params[:page]).per(8).order('created_at DESC')
  end
```

そして、この@aに対して、「.result」とすることで、検索結果を取得しています。

- 「distinct: true」：重複するデータは除外して取得できる
- .page(params[:page]).per(8).order('created_at DESC')」：kaminariというgemのページネーションを使用し、1つのページで8つの記事が作成順に表示されるようにしています。

#④viewファイルの記述

#####検索フォーム編

```ruby:app/views/layouts/_search.html.erb
<%= search_form_for @a, url: articles_search_path do |f| %>
  <div class="search-wrapper">
    <div class="search-content col-12">
        <%= f.label :type_id_eq, '種類を選択', class: 'label' %>
        <div class="radio-btn">
          <%= f.radio_button :type_id_eq, '', {checked: true} %>指定なし
          <%= f.radio_button :type_id_eq, 1 %>補助金・助成金
          <%= f.radio_button :type_id_eq, 2 %>制度融資
        </div>
    </div>
    <div class="search-content col-12">
        <%= f.label :area_id_eq, '地域を選択', class: 'label' %>
        <%= f.collection_select(:area_id_eq, Area.all, :id, :name, {include_blank: "---"}, {class:"search-select-box float-right"}) %>
    </div>

・・・（中略）・・・

    <div class="search-content col-12">
        <%= f.label :title_or_infomation_cont, 'キーワード検索', class: 'label' %>
        <%= f.search_field :title_or_information_cont, placeholder: 'キーワードを入力', class:"search-form-control float-right" %>
    </div>
    <div class="search-content col-12 text-center">
      <%= f.submit 'この条件で検索する', class:"btn btn-outline-success search-btn" %>
    </div>
  </div>
<% end %>
```

部分的に解説していきます。

```ruby:app/views/layouts/_search.html.erb
<%= search_form_for @a, url: articles_search_path do |f| %>
```

1行目部分です。search_form_forの引数に「@a(検索オブジェクト)」を渡すことで検索フォームを生成しています。

```ruby:app/views/layouts/_search.html.erb
   <div class="search-content col-12">
        <%= f.label :type_id_eq, '種類を選択', class: 'label' %>
        <div class="radio-btn">
          <%= f.radio_button :type_id_eq, '', {checked: true} %>指定なし
          <%= f.radio_button :type_id_eq, 1 %>補助金・助成金
          <%= f.radio_button :type_id_eq, 2 %>制度融資
        </div>
    </div>
```

- type_id_eqとすることで、ラジオボタンで選択したものと一致(=equal)するものを探します。type_idは記事のカラムとして含んでいます。
- 「checked: true」とすることでdefaltで選択済みになります。

```ruby:app/views/layouts/_search.html.erb
 <div class="search-content col-12">
        <%= f.label :area_id_eq, '地域を選択', class: 'label' %>
        <%= f.collection_select(:area_id_eq, Area.all, :id, :name, {include_blank: "---"}, {class:"search-select-box float-right"}) %>
    </div>
```

- area_id_eqも同様に、areaが一致するものを探しています。
- <%= f.collection_select ~ %>でプルダウンが生成されます。今回は記事投稿機能で事前にActiveHashを使用しAreaモデルを作成済みです。

```ruby:app/views/layouts/_search.html.erb
 <div class="search-content col-12">
        <%= f.label :title_or_infomation_cont, 'キーワード検索', class: 'label' %>
        <%= f.search_field :title_or_information_cont, placeholder: 'キーワードを入力', class:"search-form-control float-right" %>
  </div>
```

- 「title_or_information_cont」：今回記事のカラムの中で、titleとinformationの情報があり、その一部分の中でキーワードが合致するもの(_cont)を探しています。

#####検索結果編

```ruby:app/views/articles/search.html.erb
<div class="container">
  <h5 class="article-title"><i class="fas fa-poll-h fa-2x my-orange"></i>　検索結果一覧</h5>
    <div class="row">
      <% if @results.length !=0 %>
          <% @results.each do |result| %>
            <div class="card-group col-md-6 col-lg-3">
              <%= link_to article_path(result.id), class: "article-link" do%>
                <span class="article-info"><%= result.genre.name %></span>
              <div class="card articles-chart">
                <%= image_tag result.image, class:"card-img-top" if result.image.attached? %> 
                  <div class="card-body">
                    <h5 class="card-title"><%= result.title %></h5>
                    <p class="card-text"><%= result.information.truncate(30) %></p>
                    <p class="like-button"><i class="far fa-heart fa-2x" style="color: #e82a2a;"></i><span style="color: #e82a2a"><%= result.likes.count %></span></p>
                  </div>
              </div>
              <% end %>
            </div>
          <% end %>
        <% end %>
      <% else %>
        <h5 class="alert">該当する投稿はありません！</h5>
      <% end %>
    </div>
</div>
```

- <% if @results.length !=0 %>：検索結果があるかどうかで条件分岐を行い、あれば記事を表示、なければ「該当する投稿はありません！」を表示するようにしています。

以上で実装は完了です。

#おわりに

実装の一部分のため、わかりにくい表現があるかと思いますが、
不明点などあればご指摘ください。