,で区切って 複数ワード検索 実装

# はじめに
ポートフォリオ制作中です！
,で区切って複数のワードを検索できるように実装したためアウトプットします✍️
複数検索のアウトプットなので、検索機能についてはさらっと書いています！
検索機能実装については下記記事がわかりやすかったです！

https://qiita.com/hapiblog2020/items/6c2cef49df5616da9ae3

複数ワード検索はもっといい方法を検討中です🧐
もしあれば教えていただけると幸いです！

6/27追記　記述方法変えました！plan_searchカラムを作成しています

※旅行やデートのプランを共有するアプリなので、投稿機能はPostではなくPlanを使用しています！

# 開発環境
・ruby: 3.1.2
・Rails: 6.1.7.7
・Cloud9
・bootstrap導入済

# ER図
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/188591ad-0fa2-d39d-d84b-5af29a12f8e3.png">

# ルーティング
```routes.rb
Rails.application.routes.draw do
  scope module: :public do
  :
    get 'search' => 'searches#search'
  end
end
```

作成されたルーティング↓
```
search GET  /search(.:format)       public/searches#search
```

# ビュー（検索窓）
```_search.html.erb
<li class="nav-item list-unstyled d-flex justify-content-center mb-2">
  <div class="search_form">
    <%= form_with url: object, local: true, method: :get do |f| %>
      <%= f.text_field :word, placeholder: ' 東京, カフェ' %>
      <%= f.select :range, options_for_select([['Plan'],['User']]) %>
      <button type="submit" class="btn btn-secondary btn-sm mb-1">
        <i class="fas fa-search" style="color: #ffffff;"></i>
      </button>
    <% end %>
  </div>
</li>
```

# コントローラー
```seaches_controller.rb
class Public::SearchesController < ApplicationController
  before_action :authenticate_user!

  def search
    @range = params[:range]

    if @range == "User"
      @users = User.looks(params[:search], params[:word]).order(created_at: :desc)
    elsif @range == "Plan"
      @plans = Plan.looks(params[:search], params[:word]).order(created_at: :desc)
    end
  end

end
```

# モデル
#### Userモデル
```user.rb
class User < ApplicationRecord

  # アソシエーション
  has_many :plans, dependent: :destroy

:
  # 検索方法分岐
  def self.looks(search, word)
    # 退会ユーザーの投稿を除く
    @user = User.where("name LIKE ? AND is_active = ?", "%#{word}%", true)
  end
:
end
```

#### Planモデル
```plan.rb
class Plan < ApplicationRecord

  # アソシエーション
  belongs_to :user
  has_many :plan_details, dependent: :destroy
  has_many :plan_tags, dependent: :destroy
  has_many :tags, through: :plan_tags
:
  # コールバック
  before_save :update_plan_search
  
  # 検索用カラム保存
  def update_plan_search
    self.plan_search = "#{title} #{body} #{plan_details.map(&:title).join(' ')} #{plan_details.map(&:body).join(' ')} #{plan_details.map(&:address).join(' ')}"
  end
  
  # 検索方法分岐
  def self.looks(range, words)
    query = joins(:user)
              .left_joins(:tags) # タグがない投稿も内部結合する
              .where(is_draft: false) # 下書き投稿を除く
              .where(users: { is_active: true }) # 退会ユーザーの投稿を除く

    # ,で区切った複数の文字列を検索
    if words.present?
      conditions = words.split(',').map(&:strip).compact.reject(&:empty?).map do |word|

        "(plans.plan_search LIKE '%#{word}%' OR tags.name LIKE '%#{word}%')"
      end

      query = query.where(conditions.join(" AND "))
    end
    query.distinct
  end
  
:
end
```
```plan.rb
  # コールバック
  before_save :update_plan_search
  
  # 検索用カラム保存
  def update_plan_search
    self.plan_search = "#{title} #{body} #{plan_details.map(&:title).join(' ')} #{plan_details.map(&:body).join(' ')} #{plan_details.map(&:address).join(' ')}"
  end
```

`before_save`
新規投稿時、投稿更新時に`update_plan_search`を実行
plansのtitle,body,plan_detailsのtitle,body,addressをplan_searchカラムに保存

```plan.rb
    query = joins(:user)
              .left_joins(:tags) # タグがない投稿も内部結合する
              .where(is_draft: false) # 下書き投稿を除く
              .where(users: { is_active: true }) # 退会ユーザーの投稿を除く
```
`query`
絞り込んだ情報をqueryに代入する

`joins`
Planモデルと関連付けられた、userテーブルがplanテーブルと結合される

`left_joins`
Planモデルと関連付けられた、tagsテーブルがplanテーブルと結合される

:::note info 🌱
joinsとleft_joinsの違い
例えばtagsにleft_joinsを使用するのはタグが保存されていない投稿も結合するために使用しています。joinsにしてしまうとタグが一つも保存されていない投稿は除外されてしまうためです
:::

`where`
userの退会ユーザーの投稿とplansの下書き投稿を除く

```plan.rb
# ,で区切った複数の文字列を検索
if words.present?
  conditions = words.split(',').map(&:strip).compact.reject(&:empty?).map do |word|

  "(plans.plan_search LIKE '%#{word}%' OR tags.name LIKE '%#{word}%')"
end
```

`if words.present?`
wordsが存在する場合に処理を実行する

`words.split(',').map(&:strip).compact.reject(&:empty?)`
words文字列をカンマ,`split(',')`を使用して分割し、それぞれのキーワードとします
`map(&:strip).compact.reject(&:empty?)`で不要な空白やnilを除去します

`.map do |word|`
カンマで分割されたそれぞれのキーワードをmapを使用して検索を実行します

`"(plans.plan_search LIKE '%#{word}%' OR tags.name LIKE '%#{word}%')"`
conditions配列に追加する条件式
それぞれのモデル、カラムに渡されたwordの部分検索が行われます

```plan.rb
query = query.where(conditions.join(" AND "))
```
先ほどの条件式が入ったconditionsをjoin(" AND ") メソッドを使用して、
conditions配列の各要素を連結します。

```plan.rb
query.distinct
```
distinctメソッドを使用して重複するレコードを除外します

# ビュー（検索結果）
```search.html.erb
<%= render 'layouts/search', object: search_path %>

<!--検索対象モデルがUserの時 -->
<% if @range == "User" %>
  <% if @users.present? %>
    <% @users.each do |user| %>
      <%= render 'public/users/user_page', user: user %>
    <% end %>
  <% else %>
    検索にヒットしませんでした
  <% end %>

<!--検索対象モデルがPlanの時-->
<% elsif @range == "Plan" %>
  <% if @plans.present? %>
    <%= render 'public/plans/index', plans: @plans %>
  <% else %>
    検索にヒットしませんでした
  <% end %>
<% else %>
  検索にヒットしませんでした
<% end %>
```

# さいごに
一旦実装はできたものの、まだまだ改善点がありそうなので
改善策が見つかれば記事を更新予定です！

6/27更新
・plan title,body plan_detail title,body,addressをplan_searchカラムに保存
・joins(:tags)をleft_joins(:tags)に変更

# 参照記事
参考にさせていただきました！
ありがとうございました！

https://qiita.com/hapiblog2020/items/6c2cef49df5616da9ae3