見出し画像

Rails 検索機能

今回はRailsで検索機能を作っていきたいと思います。
ひとえに検索機能と言っても色々と種類がたくさんあります。例えば、ジャンルやカテゴリーによる検索、フリーワードによる検索などです。
ジャンルによる検索は以下の記事で実装済みなので、今回はフリーワードによる検索機能を作っていきたいと思います。

初心者向けの記事で ransack を使用した検索機能を作る記事を見かけますが、初心者のうちこそそのような gem を使用せずに検索機能を実装することをオススメします。もちろん本記事では gem を使用せず実装していきます。
検索機能とは一言で言うとデータベースに保存されているデータの中から特定の条件によってデータを取得することです。
本記事は以下のマガジンの実装を前提としていますが、適宜モデル名などを読み替えていただくと検索機能が実装できるように努めています。

検索フォームを追加

検索フォームをおきたいHTML内に以下のコードを追加します。
この記事では app/views/books/index.html.erb に追加します。

<%= form_with local: true, method: :get do |form| %>
  <%= form.text_field :keyword %>
  <%= form.submit '検索' %>
<% end %>

すると、以下のようなフォームが表示されていると思います。


現在の状態では検索ボタンを押しても何も起こりません。
なぜなら検索の処理を作っていないからです。そのため、検索処理を作ります。

検索処理を追加

検索を行うメソッドを app/models/book.rb に追加しますが、まずはテストを追加します。spec/models/book_spec.rb を修正します。

describe '.search' do
  subject { Book.search(keyword) }

  let(:books) { create_list(:book, 3, title: 'test') }

  context 'データを返す' do
    let(:keyword) { '' }

    it { is_expected.to eq(books) }
  end
end

 テストを追加し、実行するとレッドになると思います。これはまだ Book モデルにメソッドを追加していないからです。
そのため、メソッドを追加します。app/models/book.rb を以下のように修正します。
検索自体は前方一致で行うようにしています。よく後方一致や部分一致で検索を行なっている記事を見かけます。プルダウンで選択対象のモデルをユーザーに選ばせたり、検索方法を選択させるようなものです。一般的にはそのような実装方法を行なっているアプリケーションは多くないでしょう。
検索方法に関して、後方一致はユーザーがおそらくそのような検索方法を望んでいないでしょう。そのような検索方法を実装しているアプリケーションは一般的に見たことがないのではないでしょうか。また、部分一致はユーザーにとって便利ではあるのですが、検索のパフォーマンスが良くありません。そのため、前方一致のみを実装することにしています。

def self.search(keyword)
  if keyword.present?
    where('title LIKE ?', "#{keyword}%")
  else
    all
  end
end

テストを実行します。グリーンになると思います。
上記のメソッドは keyword の値によって処理を分岐しています。テストの内容にはその分岐内容が反映されていません。つまり、現状のテストは常に keyword.present? が false の場合のテストケースしかテストしていない状態です。そのため、keyword.present? が true の場合のテストケースも追加します。また、keyword パラメータの値を変えるとメソッドの戻り値がどうなるかのテストも追加します。spec/models/book_spec.rb を修正します。

describe '.search' do
  subject { Book.search(keyword) }

  let(:books) { create_list(:book, 3, title: 'test') }

  context 'keyword.present? が false の場合' do
    context 'keyword が nil の場合' do
      let(:keyword) { nil }

      it { is_expected.to eq(books) }
    end

    context 'keyword が 空文字の場合' do
      let(:keyword) { '' }

      it { is_expected.to eq(books) }
    end
  end

  context 'keyword.present? が true の場合' do
    context 'title の前方に一致するデータがある場合' do
      let(:keyword) { 'te' }

      it { is_expected.to eq(books) }
    end

    context 'title の前方に一致するデータがない場合' do
      let(:keyword) { 'hoge' }

      it { is_expected.to eq([]) }
    end
  end
end

修正したテストの内容は keyword.present? が true と false の両パターンをテストし、またそれぞれのテストケースで keyword の値を変更して戻り値が変わることのテストも行っています。
テストを実行してすべてグリーンになることを確認します。これで検索処理のロジックの部分はできました。

検索結果を表示

検索処理のロジック部分は完成しましたが、検索結果を表示するコードを追加していません。
まずはコントローラのコードを修正します。app/controllers/books_controller.rb を以下のように修正します。

def index
  @books = Book.search(search_params[:keyword])
end

...

def search_params
  params.permit(:keyword)
end

どのような値で検索したかをHTMLに表示したいこともあるかもしれません。そういった場合は以下のような実装を加えるといいでしょう。
app/controllers/books_controller.rb に以下を追加します。

def index
  @keyword = search_params[:keyword]
  @books = Book.search(@keyword)
end

そして、app/views/books/index.html.erb に以下のコードを追加します。

<% if @keyword.present? %>
  <%= "#{@keyword}の検索結果" %>
<% end %>

これで検索機能が完成しました。

検索専用のページを作る

よく検索専用のページを作る記事を見かけるので試しに作成してみたいと思います。
検索専用のページを作る方法は主に2つあります。

  1. 既存のコントローラにアクションを追加する

  2. 新しく検索用のコントローラを作成する

1つずつみていきたいとおもいます。

既存のコントローラにアクションを追加

すでに作成ずみのコントローラにアクションを追加する形で検索専用のページを作成することも可能です。
例えば、app/controllers/books_controller.rb にアクションを追加して検索ページを作成してみたいとおもいます。
まずは検索専用ページのルーティングを作成する必要があるため、config/routes.rb を修正します。今回は books_controller.rb に追加するため、books のルーティングを作成しているコードに修正を加えます。

resources :books do
  collection do
    get '/search', to: 'books#search'
  end
end

このように修正することで以下のようなURLが作成されます。

      Prefix Verb   URI Pattern                       Controller#Action
search_books GET    /books/search(.:format)           books#search

注意しなければならないのは collection ブロックを追加し、そのブロックの中にURLを定義することです。このようにしないと以下のような :id を含むURLができてしまいます。

     Prefix Verb   URI Pattern                       Controller#Action
book_search GET    /books/:book_id/search(.:format)  books#search

このようなURLになってしまうと、特定の book_id のデータの中から何かを検索するというような意味のURLになってしまうため、collection ブロックをとることで book_id がURLに含まれない形にします。
ルーティングを作成したので次はコントローラにアクションを追加します。
app/controllers/books_controller.rbsearch アクションを追加します。

def search; end

そして、views/books/search.html.erb というファイルを作成します。
ブラウザで `/books/search` というURLにアクセスし、アクセスができることを確認してもいいですが、テストを追加することでアクセスができることを担保します。
spec/requests/books_spec.rb に以下を追加します。


  describe "GET /books/search" do
    it "returns http success" do
      get "/books/search"
      expect(response).to have_http_status(:success)
    end
  end

テストを実行し、グリーンになることを確認します。
次に views/books/search.html.erb に検索フォームを追加します。

<%= form_with local: true, method: :get do |form| %>
  <%= form.text_field :keyword %>
  <%= form.submit '検索' %>
<% end %>

テストを実行し、グリーンになることを確認します。上記の修正を加えたことによってバグが起きていないことを確認する目的です。
グリーンになります。次に app/controllers/books_controller.rb を修正し、検索処理を加えていきます。

def search
  @keyword = search_params[:keyword]
  @books = Book.search(@keyword)
end

再度テストを実行してバグが発生していないことを確認します。グリーンになるとおもいます。
検索処理ができたのでビューに結果を表示する処理を追加します。

<% if @keyword.present? %>
  <%= "#{@keyword}の検索結果" %>
<% end %>
<br />
<% @books.each do |book| %>
  <%= book.genre.name %>
  <%= link_to book.title, book_path(book) %><br />
<% end %>

再度テストを実行してバグが発生していないことを確認します。グリーンになるとおもいます。
現在のテストケースはパラメータを与えた時の処理が書かれていないため、テストを修正し、検索のパラメータが与えられたテストケースを追加します。

  describe "GET /books/search" do
    before do
      create_list(:book, 3)
    end

    context 'パラメータありの場合' do
      it "returns http success" do
        get "/books/search", params: { keyword: 'test' }
        expect(response).to have_http_status(:success)
      end
    end

    context 'パラメータなしの場合' do
      it "returns http success" do
        get "/books/search"
        expect(response).to have_http_status(:success)
      end
    end
  end

テストを修正したのでテストを実行します。変わらず、テストが成功するとおもいます。
これで既存のコントローラにアクションを追加して検索ページを作成する実装が終わりました。

新しく検索用のコントローラを作成

次は新しく検索用のコントローラを作成してみます。
最初に config/routes.rb を修正します。

resources :search, only: :index, as: :search

上記のようにルーティングを実装すると以下のようなURLが作成されます。

      Prefix Verb URI Pattern             Controller#Action
search_index GET  /search(.:format)       search#index

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

bundle e rails g controller search index --no-helper --skip-routes

テストを修正して /search というURLにアクセスできることを確認します。

RSpec.describe "Search", type: :request do
  let(:user) { create(:user) }

  before do
    sign_in user
  end

  describe "GET /index" do
    it "returns http success" do
      get "/search"
      expect(response).to have_http_status(:success)
    end
  end
end

テストを実行し、グリーンになることを確認します。
次に views/search/index.html.erb に検索フォームを追加します。

<%= form_with local: true, method: :get do |form| %>
  <%= form.text_field :keyword %>
  <%= form.submit '検索' %>
<% end %>

テストを実行し、グリーンになることを確認します。上記の修正を加えたことによってバグが起きていないことを確認する目的です。
グリーンになります。次に app/controllers/search_controller.rb を修正し、検索処理を加えていきます。

def index
  @keyword = search_params[:keyword]
  @books = Book.search(@keyword)
end

private

def search_params
  params.permit(:keyword)
end

再度テストを実行してバグが発生していないことを確認します。グリーンになるとおもいます。
検索処理ができたのでビューに結果を表示する処理を追加します。

<% if @keyword.present? %>
  <%= "#{@keyword}の検索結果" %>
<% end %>
<br />
<% @books.each do |book| %>
  <%= book.genre.name %>
  <%= link_to book.title, book_path(book) %><br />
<% end %>

再度テストを実行してバグが発生していないことを確認します。グリーンになるとおもいます。
現在のテストケースはパラメータを与えた時の処理が書かれていないため、テストを修正し、検索のパラメータが与えられたテストケースを追加します。

  describe "GET /index" do
    before do
      create_list(:book, 3)
    end

    context 'パラメータありの場合' do
      it "returns http success" do
        get "/search", params: { keyword: 'test' }
        expect(response).to have_http_status(:success)
      end
    end

    context 'パラメータなしの場合' do
      it "returns http success" do
        get "/search"
        expect(response).to have_http_status(:success)
      end
    end
  end

テストを修正したのでテストを実行します。変わらず、テストが成功するとおもいます。
これで既存のコントローラにアクションを追加して検索ページを作成する実装が終わりました。

以下2つの実装をしましたが、どちらも大きな違いがないことに気づかれた方もいると思います。いずれの実装方法が正解というものはないと思いますが、URLがリソースを適切に表現している方がRailsアプリケーションとしては良いと思います。そのため、1の方がベターのように思われます。

  1. 既存のコントローラにアクションを追加する

  2. 新しく検索用のコントローラを作成する

ある機能の実装方法はいろいろなやり方があることが多いので、自分が作っているアプリケーションに合ったやり方で実装するといいと思われます。

いいなと思ったら応援しよう!