【Rails6.0】タグ検索機能（タグで絞り込み機能）を実装してみた【gem無し】

現在個人開発しているアプリにタグ検索機能を実装したのでここに備忘録としてまとめさせていただきます。
あえてgemを使っていないので本質的なところまで理解することができました。

【環境】
- windows10 Pro
- Rails: 6.0.3.2
- ruby: 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
- Docker for windows
- MySQL 5.7
- nginx:1.15.8

##機能の概要
新規投稿フォームにタグを入力すると、入力したタグが投稿一覧とサイドバーに反映されます。
サイドバーに反映されたタグをクリックするとそのタグが付いた投稿を絞り込むことができるという機能です。

投稿フォームからタグを入力
![tagu_qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632678/44314ca2-0886-13f8-fdf1-8cd8db595943.png)

↑で入力したタグがサイドバーと投稿一覧に表示される
サイドバーに表示されたタグをクリックするとそのタグを含む記事を絞り込むことができる。（今回はサンプルなので一件のみ）
![tagu_qiita2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632678/a27c9569-e5ba-37ff-b746-39c60641d492.png)

##実装手順
Itemモデル及びPostモデルなど作成済みという前提で話しをします。(ここではitemモデルとします。各自置き換えてください。）

まず、Tagモデル、Tagmapモデルをいつものように作成します。

```
$ rails g model Tagmap item:references tag:references 
$ rails g model tag tag_name:string
```

TagmapはItemモデルとTagモデルに紐づくのでデータ型にreferencesを付けてます。
これをするとRailsが良きにはからってくれますので詳しく知りたい方はググってみてください。

#マイグレーションファイル

```ruby

class CreateTagmaps < ActiveRecord::Migration[6.0]
  def change
    create_table :tagmaps do |t|
      t.references :item, null: false, foreign_key: true
      t.references :tag, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```

こちらはインデックスを付けます。

```ruby

class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :tag_name, null: false

      t.timestamps
    end
    add_index :tags, :tag_name, unique: true
  end
end
```

その後いつものようにマイグレーションをします。

```
$ rails db:migrate
```
#アソシエーション

```ruby:item.rb
has_many :tagmaps, dependent: :destroy
has_many :tags, through: :tagmaps
```

```ruby:tag.rb
class Tag < ApplicationRecord
  has_many :tagmaps, dependent: :destroy
  has_many :items, through: :tagmaps
end
```

tagもtagmapsと関連付けさせています。↑

thoroughを使うことで、tagmaps経由でitemsにアクセスできるようになってます。

```ruby:tagmap.rb
class Tagmap < ApplicationRecord
  belongs_to :item
  belongs_to :tag
end
```

↑references形にするとbelongs_toの部分をRailsが自動で記述してくれます！
#コントローラとモデルの記述

```ruby:items_controller.rb
  def index
    if params[:search].present?
      items = Item.items_serach(params[:search])
    elsif params[:tag_id].present?
      @tag = Tag.find(params[:tag_id])
      items = @tag.items.order(created_at: :desc)
    else
      items = Item.all.order(created_at: :desc)
    end
    @tag_lists = Tag.all
    @items = Kaminari.paginate_array(items).page(params[:page]).per(10)
  end
```

こちらのコードの解説をします。
indexアクションでif文を使い3パターン場合分けしていて、この結果によりitem変数に代入される値を変えています。
１．検索フォームに入力があった場合
２．paramsで:tag_idを受け取った場合（クリックしたときにパラメータでtag_idを受け取ったときに発動する。タグで絞込む機能）
３．普通にページを表示させた場合
ページネーション機能も設定しているので、item変数に格納される変数が3パターンになり、それぞれ最適化されたものが表示できるようになっています。
ページネーションではKaminariというgemを使っております。

```ruby:items_controller
  def create
    @item = Item.new(item_params)
    tag_list = params[:item][:tag_name].split(nil)
    @item.image.attach(params[:item][:image])
    @item.user_id = current_user.id
    if @item.save
       @item.save_items(tag_list)
      redirect_to items_path
    else
      flash.now[:alert] = '投稿に失敗しました'
      render 'new'
    end
  end
```

itemsコントローラーのcreateアクションでのポイントは3行目のtag_list変数への代入部分です。
rubyの**String#split** は第 1 引数（今回はnil)で指定されたセパレータによって文字列を分割し、結果を文字列の配列で返します。ブロックを指定すると、配列を返す代わりに分割した文字列でブロックを呼び出します。
また、1 バイトの空白文字 ' 'で区切られたタグデータをフォームから送ると先頭と末尾の空白を除いたうえで、空白文字列で分割します。今回はこれを利用します。

新規投稿フォームから送られてきたパラメーターを空白文字（nil)でセパレイトして配列化し、Itemクラスで定義した**save_items**メソッドを使い、データベースに保存します。（フォームにはタグをスペース区切りで入力してもらうように使用者にお願いする。）

```ruby:item.rb
 #検索メソッド、タイトルと内容をあいまい検索する
 def self.items_serach(search)
   Item.where(['title LIKE ? OR content LIKE ?', "%#{search}%", "%#{search}%"])
 end

 def save_items(tags)
   current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
   old_tags = current_tags - tags
   new_tags = tags - current_tags

   # Destroy
   old_tags.each do |old_name|
     self.tags.delete Tag.find_by(tag_name:old_name)
   end

   # Create
   new_tags.each do |new_name|
     item_tag = Tag.find_or_create_by(tag_name:new_name)
     self.tags << item_tag
   end
 end
```

↑タグデータを保存するとき、フォームから送られてきたタグデータのうち、すでに存在するタグネームがひとつでもあった場合はtagsテーブルのtag_nameカラムからpluckメソッドを使い一旦すべてのデータを引っ張ってきてcurrent_tagsに代入します。（すべて新しいものだった場合はnilになる。）
そしてすでに存在するタグデータの集合であるcurrent_tagsから、コントローラーから引数で渡ってきたtagsの配列を引くと古いタグ（old_tags)を定義することができます。
rubyでは配列の引き算をすると共通する要素を取り出すことができるためです。

**具体例：**

```ruby

tags(tag_list = params[:item][:tag_name].split(nil)でコントローラーから渡ってくる配列) = ["Rails" "ruby" "React"]
current_tags(現在DBに存在しているタグデータ) = ["Rails" "ruby" "Vue.js"]

old_tags = ["Rails" "ruby" "Vue.js" "Docker"] -  ["Rails" "ruby" "React"] 
old_tags = ["Rails" "ruby"] 
↑二つの配列の共通の要素が残る（すでに存在している古いタグを算出することができる）
```


#ビューの記述
**新規投稿フォームの記述一部抜粋（CSSフレームワークにUIkitというのを使ってます）**

```ruby

.uk-form.new_post_form
  = form_with(model: [@tag,@item], local: true) do |f|
    .uk-margin-small
      = f.text_field :title, placeholder: "タイトルを入力（35文字まで）", class: 'uk-input'
    .uk-margin-small
      = f.text_field :tag_name, placeholder: "プログラミング技術や募集要項に関するタグをスペース区切りで5つまで入力(例 Ruby Rails )", class: 'uk-input uk-form-small'
```

**サイドバーのビュー一部抜粋**

```haml:index.html.haml
%li.search_friend_by_categorize
  .uk-text-secondary.uk-text-bold
    タグで探す
  %ul.uk-flex.uk-flex-wrap
    - @tag_lists.each do |list|
      %li
        = link_to list.tag_name, items_path(tag_id: list.id), class: 'tag_list'
```

投稿記事一一覧へのタグの表示

```haml:index.html.haml
 %p.tag_list_box
   - item.tags.each do |tag|
     = link_to "##{tag.tag_name}", items_path(tag), class: 'smaller tag_list'
```

viewはこのような感じになります。特に難しいことはないですね！

##最後まで読んでいただきありがとうございます！
少し駆け足で書いたので間違いなどあるかもしれません。もしなにかご指摘や感想などあればコメントいただけると嬉しいです！！