【Rails】検索機能

# 検索機能の作り方（覚書）

検索機能はこれからも使うと思うので、覚書です。

##（１）ビューを作る

```ruby:app/views/tweets/&nbsp;index.html.erb
#検索フォーム

<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

#検索結果後の画面　（４）のコントローラーで定義したキーワード入り@tweetを引っ張ってきています

<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
</div>
``` 

##（２）searchアクションのルーティングを設定する

###collectionとmemberについて

両者の違いは、

-  「member」は、「resources以外のメソッドを追加したい」且つ「<b>id情報を伴うURI</b>を生成したい」時に使用。
- 「collection」は、「resources以外のメソッドを追加したい」且つ「<b>id情報を伴わないURI</b>を生成したい」時に使用。


|アクション|rails routesで見たときの違い|使用例|[指定可能なHTTP動詞](https://railsguides.jp/routing.html#restful%E3%81%AA%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%81%95%E3%82%89%E3%81%AB%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)|
|:---:|:---:|:---|:---|
|member|:idが付く|購入機能<br>（どの商品が選択され購入されたのか特定が必要なのでid指定の必要あり）|`get`、`patch`、`put`、`post`、`delete`|
|collection|:idが付かない|検索機能<br>（検索するだけのアクションなのでid指定する必要なし）|GETリクエスト+/photos/searchなどの (idを伴わない) パスを認識|



今回はコレクションで。

```ruby:config/&nbsp;routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'tweets#index'
  resources :tweets do
    resources :comments, only: :create
    collection do
      get 'search'
    end
  end
  resources :users, only: :show
end
```

##（３）モデルに検索処理を行うメソッドを定義

```ruby:app/models&nbsp;tweet.rb
class Tweet < ApplicationRecord
  validates :text, presence: true
  belongs_to :user
  has_many :comments

  def self.search(search)
    if search != ""
      Tweet.where('text LIKE(?)', "%#{search}%")
    else
      Tweet.all
    end
  end
end
```

- 今回はsearchメソッドとする。

- `if search != ""`とすることで、検索フォームに何か値が入力されていた場合を条件としている。

- 検索したキーワードが含まれている投稿を取得するために、whereメソッドとLIKE句を利用する。

###whereメソッド

- モデルが使用できるActiveRecordメソッドの一つ。

- モデル.where(条件)のように、引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得できる。

- 条件部分には「検索対象となるカラム」を必ず含める。

```ruby:&nbsp;whereメソッド
モデル名.where('検索対象となるカラムを含む条件式')
```
- whereメソッドを連続して記述することによって複数の条件に一致したレコードを取得することもできる。

```ruby:&nbsp;例
Tweet.where('id < 3').where(user_id: 1)
```

###LIKE句

- 曖昧な文字列を検索する時に使用（whereメソッドとセットで使う）

####曖昧文字列について

|文字列|意味|
|:---:|:---:|
|%|任意の文字列（空白文字列含む）|
|_|任意の一文字|

####実行サンプル

|文字列|意味|
|:---|:---|
|where('title' LIKE(?),　"a%")|aから始まるタイトル|
|where('title' LIKE(?),　"%a")|aで終わるタイトル|
|where('title' LIKE(?),　"%a%")|aが含まれるタイトル|
|where('title' LIKE(?),　"a_")|aで始まる２文字のタイトル|
|where('title' LIKE(?),　"_a")|aで終わる2文字のタイトル|

* なお、モデルが肥大化してしまうのを防ぐためにサービスクラスを設定して記載することも可能。

##（４）コントローラーに定義

```ruby:app/controllers/&nbsp;tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show, :search]

  def index
    @tweets = Tweet.includes(:user).order("created_at DESC")
  end
  
  （省略）

  def search
    @tweets = Tweet.search(params[:keyword])
  end

  private
  def tweet_params
    params.require(:tweet).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_tweet
    @tweet = Tweet.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end
 
```
* searchメソッドの引数に、`params[:keyword]`と記述して、検索結果を渡している。

* 未ログイン時に検索するとトップページへリダイレクトしてしまうため、`before_action`の`exceptオプション`に`:search`を追加している。

- （補足）`exceptオプション`とは、before_actionで使用できるオプション。「除外」するという意味。  つまり、exceptで指定したアクションには、事前処理（move_to_indexメソッド内容）は実行されない。


以上です。

#番外編（モデルではなく、検索機能を「サービスクラス」に持たせる場合には）

##（１）appディレクトリ配下に、`services`というディレクトリを作成。
##（２）その配下に、ファイルを作成。（今回は`search_tweets_service.rb`）

```ruby:app/services/&nbsp;search_tweets_service.rb
class SearchTweetsService
  def self.search(search)
    if search != ""
      Tweet.where('text LIKE(?)', "%#{search}%")
    else
      Tweet.all
    end
  end
end
```
##（３）呼び出すコントローラーのクラス名を変更する。

- （２）のファイル名と同じにすること。

```ruby:app/controllers/&nbsp;tweets_controller.rb
class TweetsController < ApplicationController

　（省略）

  def search
    @tweets = SearchTweetsService.search(params[:keyword])
  end

　（省略）

end
```

##（４）モデルの記述は削除する。