[Rails]ransackによる検索 16/20
はじめに
ransack
を使って投稿の検索機能を実装していきます。
要件:
投稿のタイトル or 本文の部分一致検索ができること
環境
Rails 6.1.7.3
ruby 3.0.0
ransack
とは
RansackはRailsのための便利な検索gemです。
検索フォームを作成し、コントローラーで検索オブジェクトを作成し、結果を取得することができます。Ransackを使用すると、簡単に高度な検索機能を実装することができます。
- モデルの設定:
検索を行いたいモデル(例:Product
モデル)に対して、Ransackを使用するための設定を行います。
class Product < ApplicationRecord
# Ransackで検索可能な属性を指定します
# 例: nameカラムとpriceカラムを検索対象にします
searchable_attributes %w[name price]
end
- コントローラの設定:
検索アクションを定義し、検索クエリを作成して結果を取得します。
class ProductsController < ApplicationController
def index
@q = Product.ransack(params[:q])
@products = @q.result
end
end
- ビューの作成:
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
をインストールする
gem 'ransack'
bundle install
サーチ用のurlを追加する
urlをarticles/search
にします。
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モデルを編集する
検索させたいカラムと関連するモデルを定義する必要があります。
検索させたいカラムにtitle
とbody
を定義し、関連したいモデルがないので空の配列にします。
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"]
承認についての説明です。
定義しないと以下のエラーが発生します。
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
アクションを追加します。
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
コントローラーに定義し共通化します。
class ApplicationController < ActionController::Base
before_action :set_search_object
private
def set_search_object
@q = Article.ransack(params[:q])
end
end
検索フォームを作成する
@q
はコントローラーで定義した検索オブジェクト、url
は検索フォームがリクエストするURLです。
ヘッダーに検索フォームを置きたいのでパーシャルファイルを作成します。
<%= 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 %>
<li class="nav-item">
<%= render 'shared/search_form', url: search_articles_path %>
</li>
マッチャ
Ransackのマッチャは、検索条件を指定するために使用される構文です。
検索条件を指定するために様々なマッチャを使用することができます。以下に代表的なマッチャになります:
-
eq
:等しい条件にマッチします。 -
not_eq
:等しくない条件にマッチします。 -
cont
:部分一致する条件にマッチします。 -
not_cont
:部分一致しない条件にマッチします。 -
start
:前方一致する条件にマッチします。 -
end
:後方一致する条件にマッチします。 -
gt
:より大きい条件にマッチします。 -
lt
:より小さい条件にマッチします。
これらのマッチャは、検索フィールドのデータ型に応じて異なる動作をします。たとえば、文字列フィールドにはcont
やstart
などの文字列マッチャが使用され、数値フィールドにはgt
やlt
などの数値マッチャが使用されます。
検索結果ビューを作成する
ユーザーが入力したキーワードと結果の件数をタイトルに表示されることにしました。
<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