🦓

[Rails]ransackによる検索 16/20

2023/07/05に公開

はじめに

ransackを使って投稿の検索機能を実装していきます。
要件:
投稿のタイトル or 本文の部分一致検索ができること

環境:

Rails 6.1.7.3
ruby 3.0.0

ransackとは

RansackはRailsのための便利な検索gemです。
検索フォームを作成し、コントローラーで検索オブジェクトを作成し、結果を取得することができます。Ransackを使用すると、簡単に高度な検索機能を実装することができます。

  1. モデルの設定:
    検索を行いたいモデル(例: Productモデル)に対して、Ransackを使用するための設定を行います。
class Product < ApplicationRecord
  # Ransackで検索可能な属性を指定します
  # 例: nameカラムとpriceカラムを検索対象にします
  searchable_attributes %w[name price]
end
  1. コントローラの設定:
    検索アクションを定義し、検索クエリを作成して結果を取得します。
class ProductsController < ApplicationController
  def index
    @q = Product.ransack(params[:q])
    @products = @q.result
  end
end
  1. ビューの作成:
    Ransackのフォームヘルパーを使用して検索フォームを作成し、検索結果を表示します。
<%= search_form_for @q do |f| %>
  <%= f.label :name_cont, '商品名' %>
  <%= f.search_field :name_cont %>

  <%= f.submit '検索する' %>
<% end %>

<% @products.each do |product| %>
  <p><%= product.name %></p>
<% end %>

上記の例では、Productモデルを検索対象とし、name属性とprice属性での検索を行います。フォームヘルパーを使用して、検索条件の入力フィールドを作成し、@productsに検索結果が格納されます。

ransackをインストールする

Gemfile
gem 'ransack'
bundle install

https://activerecord-hackery.github.io/ransack/

サーチ用のurlを追加する

urlをarticles/searchにします。

config/routes.rb
  resources :articles do
      collection do
        get 'search', to: 'articles#search', as: :search
      end
  end
rails routes
search_articles GET    /articles/search(.:format)                  articles#search

Articlesモデルを編集する

検索させたいカラムと関連するモデルを定義する必要があります。
検索させたいカラムにtitlebodyを定義し、関連したいモデルがないので空の配列にします。

app/model/article.rb
class Article < ApplicationRecord
  def self.ransackable_attributes(auth_object = nil)
     %w[title body]
  end

  def self.ransackable_associations(auth_object = nil)
     []
  end
end
irb(main):002:0> Article.ransackable_associations
=> []
irb(main):003:0> Article.ransackable_attributes
=> ["title", "body"]

承認についての説明です。
https://activerecord-hackery.github.io/ransack/going-further/other-notes/#authorization-allowlistingdenylisting

定義しないと以下のエラーが発生します。

RuntimeError (Ransack needs Article associations explicitly allowlisted as)
searchable. Define a `ransackable_associations` class method in your `Article`
model, watching out for items you DON'T want searchable (for
example, `encrypted_password`, `password_reset_token`, `owner` or
other sensitive information). You can use the following as a base:

class Article < ApplicationRecord

  # ...

  def self.ransackable_associations(auth_object = nil)
    ["bookmarks", "comments", "user"]
  end

  # ...

end

Articlesコントローラーを編集する

searchアクションを追加します。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def search
    @q = Article.ransack(params[:q])
    @articles = @q.result(distinct: true).includes(:user).page(params[:page]).per(20)
  end
end

distinct: true

distinct: trueは、Ransackの検索結果に対して重複したレコードを削除するオプションです。

通常、Ransackを使用して検索を行うと、検索結果には重複したレコードが含まれる場合があります。
今回の検索条件で言うと distinct: true は必要ありません。
必要になるのは「関連する子テーブルの情報を条件に絞り込んで、親テーブルの検索結果を表示するとき」のケースです。

もし関連するコメントテーブルも検索結果に含まれる場合は、同じ投稿に複数のコメントがキーワードにマッチする場合があります。
distinct: trueがないで検索したコメントが2件にあった場合に、投稿が2回取得されて検索結果が2件になってしまいます。

同じ投稿が複数回表示されることを防ぐ方法としては、distinct: trueは必要です。

Applicationコントローラーを編集する

ヘッダーに検索フォームを置きたいのでサーチ変数をApplicationコントローラーに定義し共通化します。

app/controller/application_controller.rb
class ApplicationController < ActionController::Base
    before_action :set_search_object

    private

    def set_search_object
        @q = Article.ransack(params[:q])
    end
end

検索フォームを作成する

@qはコントローラーで定義した検索オブジェクト、urlは検索フォームがリクエストするURLです。

ヘッダーに検索フォームを置きたいのでパーシャルファイルを作成します。

app/views/shared/_search_form.html.erb
<%= search_form_for @q, url: url do |f| %>
  <div class="input-group mb-3">
    <%= f.search_field :title_or_body_cont, class: 'form-control', placeholder: t('defaults.search_word') %>
    <div class="input-group-append">
      <%= f.submit class: 'btn btn-primary' %>
    </div>
  </div>
<% end %>
app/views/shared/_header.html.erb
<li class="nav-item">
   <%= render 'shared/search_form', url: search_articles_path %>
</li>

マッチャ

Ransackのマッチャは、検索条件を指定するために使用される構文です。
検索条件を指定するために様々なマッチャを使用することができます。以下に代表的なマッチャになります:

  • eq:等しい条件にマッチします。
  • not_eq:等しくない条件にマッチします。
  • cont:部分一致する条件にマッチします。
  • not_cont:部分一致しない条件にマッチします。
  • start:前方一致する条件にマッチします。
  • end:後方一致する条件にマッチします。
  • gt:より大きい条件にマッチします。
  • lt:より小さい条件にマッチします。

これらのマッチャは、検索フィールドのデータ型に応じて異なる動作をします。たとえば、文字列フィールドにはcontstartなどの文字列マッチャが使用され、数値フィールドにはgtltなどの数値マッチャが使用されます。

https://activerecord-hackery.github.io/ransack/getting-started/search-matches/

検索結果ビューを作成する

ユーザーが入力したキーワードと結果の件数をタイトルに表示されることにしました。

app/views/search.html.erb
<h2><%= params[:q][:title_or_body_cont] if params[:q].present? %>」の検索結果(<%= @articles.total_count%></h2>
<div class="row row-cols-1 row-cols-md-3 g-4">
<% if @articles.present? %>
  <% @articles.each do |article| %>
    <%= render 'articles/article', article: article %>
  <% end %>
<% else %>
  <h2>一致する結果がありませんでした。</h2>
<% end %>
</div>
<div>
  <%= paginate @articles %>
</div>

終わりに

検索機能は非常によく使われる実装なので理解を深めていきましょう。

Discussion