LoginSignup
7
23

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-18

現在個人開発しているアプリにタグ検索機能を実装したのでここに備忘録としてまとめさせていただきます。
あえて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

↑で入力したタグがサイドバーと投稿一覧に表示される
サイドバーに表示されたタグをクリックするとそのタグを含む記事を絞り込むことができる。(今回はサンプルなので一件のみ)
tagu_qiita2.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が良きにはからってくれますので詳しく知りたい方はググってみてください。

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


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

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


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

アソシエーション

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

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

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

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

↑references形にするとbelongs_toの部分をRailsが自動で記述してくれます!

コントローラとモデルの記述

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変数に代入される値を変えています。
1.検索フォームに入力があった場合
2.paramsで:tag_idを受け取った場合(クリックしたときにパラメータでtag_idを受け取ったときに発動する。タグで絞込む機能)
3.普通にページを表示させた場合
ページネーション機能も設定しているので、item変数に格納される変数が3パターンになり、それぞれ最適化されたものが表示できるようになっています。
ページネーションではKaminariというgemを使っております。

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メソッドを使い、データベースに保存します。(フォームにはタグをスペース区切りで入力してもらうように使用者にお願いする。)

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では配列の引き算をすると共通する要素を取り出すことができるためです。

具体例:


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というのを使ってます)


.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'

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

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'

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

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はこのような感じになります。特に難しいことはないですね!

最後まで読んでいただきありがとうございます!

少し駆け足で書いたので間違いなどあるかもしれません。もしなにかご指摘や感想などあればコメントいただけると嬉しいです!!

7
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
23