[RUBY] [Rails 6.0] I implemented a tag search function (a function to narrow down by tags) [no gem]

I have implemented a tag search function in the app I am currently developing, so I will summarize it here as a memorandum. I didn't dare to use gems, so I was able to understand the essentials.

【environment】

Feature overview

When you enter a tag in the new post form, the entered tag will be reflected in the post list and sidebar. It is a function that you can narrow down the posts with that tag by clicking the tag reflected in the sidebar.

Enter a tag from the post form tagu_qiita.png

The tag entered in ↑ is displayed in the sidebar and post list By clicking the tag displayed in the sidebar, you can narrow down the articles that include that tag. (Since this is a sample, only one case) tagu_qiita2.png

Implementation procedure

I will talk on the assumption that the Item model and Post model have already been created. (Here, it is an item model. Please replace it by yourself.)

First, create the Tag model and Tagmap model as usual.

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

Since Tagmap is linked to Item model and Tag model, references are attached to the data type. If you do this, Rails will do a good job, so if you want to know more, please google.

Migration file


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

This is indexed.


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

Then migrate as usual.

$ rails db:migrate

association

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 is also associated with tagmaps. ↑

By using thorough, you can access items via tagmaps.

tagmap.rb


class Tagmap < ApplicationRecord
  belongs_to :item
  belongs_to :tag
end

↑ Rails will automatically describe the belongs_to part in the references form!

Controller and model description

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

I will explain the code here. The if statement is used in the index action to separate the three patterns, and the value assigned to the item variable is changed according to this result.

    1. When there is input in the search form
  1. When: tag_id is received in params (Activates when tag_id is received as a parameter when clicked. Function to narrow down by tag)
    1. When the page is displayed normally Since the pagination function is also set, the variables stored in the item variable are in 3 patterns, and each optimized one can be displayed. Pagination uses a gem called Kaminari.

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] = 'Posting failed'
      render 'new'
    end
  end

The point in the create action of the items controller is the assignment part to the tag_list variable on the third line. ** String # split ** of ruby splits the string by the separator specified by the first argument (nil this time) and returns the result as an array of strings. If you specify a block, it calls the block with a split string instead of returning an array. Also, if you send tag data separated by a 1-byte whitespace character'' from the form, it will be separated by a whitespace character string after removing the leading and trailing whitespace. This time we will use this.

The parameters sent from the new post form are separated by whitespace characters (nil), arranged, and saved in the database using the ** save_items ** method defined in the Item class. (Ask the user to enter tags in the form separated by spaces.)

item.rb


 #Fuzzy search method, title and content
 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

↑ When saving tag data, if there is even one tag name that already exists among the tag data sent from the form, use the pluck method from the tag_name column of the tags table to pull all the data once and set it to current_tags. Substitute. (If everything is new, it will be nil.) And old tags (old_tags) can be defined by subtracting the array of tags passed as an argument from the controller from current_tags which is a set of tag data that already exists. This is because in ruby, common elements can be extracted by subtracting an array.

Concrete example:


tags(tag_list = params[:item][:tag_name].split(nil)Array that comes from the controller) = ["Rails" "ruby" "React"]
current_tags(Tag data that currently exists in the DB) = ["Rails" "ruby" "Vue.js"]

old_tags = ["Rails" "ruby" "Vue.js" "Docker"] -  ["Rails" "ruby" "React"] 
old_tags = ["Rails" "ruby"] 
↑ The common element of the two arrays remains (old tags that already exist can be calculated)

View description

** Excerpt from the description of the new post form (UIkit is used for the CSS framework) **


.uk-form.new_post_form
  = form_with(model: [@tag,@item], local: true) do |f|
    .uk-margin-small
      = f.text_field :title, placeholder: "Enter a title (up to 35 characters)", class: 'uk-input'
    .uk-margin-small
      = f.text_field :tag_name, placeholder: "Enter up to 5 tags related to programming technology and recruitment requirements separated by spaces(Example Ruby Rails)", class: 'uk-input uk-form-small'

** Partial sidebar view excerpt **

haml:index.html.haml


%li.search_friend_by_categorize
  .uk-text-secondary.uk-text-bold
Search by tag
  %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'

Displaying tags in a list of posted articles

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'

The view looks like this. It's not particularly difficult!

Thank you for reading to the end!

I wrote it in a hurry, so there may be mistakes. If you have any suggestions or impressions, I would appreciate it if you could comment! !!

Recommended Posts

[Rails 6.0] I implemented a tag search function (a function to narrow down by tags) [no gem]
[Rails] Implementation procedure of the function to tag posts without gem + the function to narrow down and display posts by tags
Add a tag function to Rails. Use acts-as-taggable-on
[For Rails beginners] Implemented multiple search function without Gem
I want to define a function in Rails Console
[Rails] Implemented a pull-down search function for Active Hash data
Implement a refined search function for multiple models without Rails5 gem.
Add a search function in Rails.
I want to add a browsing function with ruby on rails
Implemented hashtag search like Instagram and Twitter in Rails (no gem)
I tried to make a group function (bulletin board) with Rails
I created a Rails post form, but I can't post it (form tag) / No error occurs
[Rails / JavaScript / Ajax] I tried to create a like function in two ways.
Convert to a tag to URL string in Rails
Posting function implemented by asynchronous communication in Rails
Let's make a search function with Rails (ransack)
How to make a follow function in Rails
I tried to make a message function of Rails Tutorial extension (Part 1): Create a model
I tried to operate home appliances by holding a smartphone over the NFC tag
[Ruby on Rails] How to implement tagging / incremental search function for posts (without gem)
[Rails] How to search by multiple values ​​with LIKE
I want to use a little icon in Rails
I tried to make a login function in Java
How to write a date comparison search in Rails
Rails learning How to implement search function using ActiveModel
Rails Tutorial Extension: I created a follower notification function
[Rails] How to install a decorator using gem draper
I want to add a delete function to the comment function
I tried to make a reply function of Rails Tutorial extension (Part 3): Corrected a misunderstanding of specifications
[Ruby on Rails] I get a warning when executing RSpec due to a different gem version.
I tried to make a message function of Rails Tutorial extension (Part 2): Create a screen to display