[RUBY] Implemented hashtag search like Instagram and Twitter in Rails (no gem)

Overview

I am currently attending a programming school called DMM WEB CAMP. Like being used on Instagram and Twitter for the portfolio, which is the issue for the third month Implemented hashtag-like. I hope it will be helpful for those who will implement it in the future.

Reference site

Implement hashtags in Instagram-like captions made with Rails https://qiita.com/goppy001/items/791c946abdb41c9495bb

The general flow is the same as the above site, but the parts that did not work well have been changed.

Completion drawing

Post screen

スクリーンショット 2020-06-24 11.28.23.png

Post details screen

スクリーンショット 2020-06-24 11.28.55.png

Hashtag list and hashtag post list screen

スクリーンショット 2020-06-24 11.29.11.png

Advance preparation DB

Post table スクリーンショット 2020-06-24 11.36.41.png

Initially it was a table structure with only body and user_id, but we have added a hashbody column for entering hashtags. By the way, the image is saved in another table due to the specifications of the portfolio, but there is no problem even if the image column is in this table.

Creating a model (DB)

Hashtag model

$rails g model Hashtag hashname:string

Create a model for hashtag storage. The hashtag is stored in the hashname column.

Edit migration file

create_hashtags.rb


class CreateHashtags < ActiveRecord::Migration[5.2]
  def change
    create_table :hashtags do |t|
      t.string :hashname

      t.timestamps
    end
    add_index :hashtags, :hashname, unique: true
  end
end

Creating an intermediate table

$ rails g model HashtagPostImage post_image:references hashtag:references

An intermediate table between the Hashtag table and the PostImage table. The command here is slightly different in the article I referred to. Since it is an intermediate table, bring the hashtag and postimage id as foreign keys. Since it is a references type, if you type hashtag_id at the time of creation Please note that the completed column name will be hashtag_id_id.

Migration file

create_hashtag_post_images.rb


class CreateHashtagPostImages < ActiveRecord::Migration[5.2]
  def change
    create_table :hashtag_post_images do |t|
      t.references :post_image, foreign_key: true
      t.references :hashtag, foreign_key: true
    end
  end
end

My Great

$ rails db:migrate

Created DB

スクリーンショット 2020-07-01 12.13.30.png

Model association and validation settings

Hashtag model

hashtag.rb


class Hashtag < ApplicationRecord
  validates :hashname, presence: true, length: { maximum: 50 }
  has_many :hashtag_post_images, dependent: :destroy
  has_many :post_images, through: :hashtag_post_images
end

For the time being, I set the upper limit to 50 characters.

Intermediate table

hashtag_post_image.rb


class HashtagPostImage < ApplicationRecord
  belongs_to :post_image
  belongs_to :hashtag
  validates :post_image_id, presence: true
  validates :hashtag_id, presence: true
end

PostImage model

post_image.rb


class PostImage < ApplicationRecord
  has_many :hashtag_post_images, dependent: :destroy
  has_many :hashtags, through: :hashtag_post_images
end

Added the following to the PostImage model

post_image.rb


after_create do
    post_image = PostImage.find_by(id: id)
    #Detects hashtags typed into hashbody
    hashtags = hashbody.scan(/[##][\w\p{Han}Ah-Gae-゚]+/)
    hashtags.uniq.map do |hashtag|
      #Hashtag is at the beginning#Save after removing
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      post_image.hashtags << tag
    end
  end
  #Update action
  before_update do
    post_image = PostImage.find_by(id: id)
    post_image.hashtags.clear
    hashtags = hashbody.scan(/[##][\w\p{Han}Ah-Gae-゚]+/)
    hashtags.uniq.map do |hashtag|
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      post_image.hashtags << tag
    end
  end

It is marked so that this action will be performed when creating and updating.

・ Post_image = PostImage.find_by (id: id) Lets you find the post you created.

・ Hashtags = hashbody.scan (/ [# #] [\ w \ p {Han} a-ga- ゚] +/) Here, search for the entered hashtag and the input value with [# #] at the beginning. hashbody is the column name of my DB, so this depends on the application. Any column for text entry in the post table will do.

・ Hashtags.uniq.map do |hashtag| #Hashtag is saved after removing the leading # tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#')) post_image.hashtags << tag end

By repeating with map, multiple hashtags are saved in postimage.

・ Post_image.hashtags.clear It seems that the hashtag is deleted once when updating.

Description of route

routes.rb


get '/post_image/hashtag/:name' => 'post_images#hashtag'
get '/post_image/hashtag' => 'post_images#hashtag'

In my case, I wanted to create a hashtag list page, so I prepared two routes.

Editing the PostImage helper

post_images_helper.rb


module PostImagesHelper
  def render_with_hashtags(hashbody)
    hashbody.gsub(/[##][\w\p{Han}Ah-Gae-゚]+/) { |word| link_to word, "/post_image/hashtag/#{word.delete("#")}",data: {"turbolinks" => false} }.html_safe
  end
end

link_to word, "/post_image/hashtag/#{word.delete("#")}" The url here depends on the content of the application. It means that clicking the hashtag will take you to the url here. Type in the url you wrote in route earlier.

PostImag controller

controllers/post_images_controller.rb



class PostImagesController < ApplicationController
  def new
    @postimagenew = PostImage.new
    @postimagenew.post_image_images.new
  end

  def create
    @postimagenew = PostImage.new(post_image_params)
    @postimagenew.user_id = current_user.id

    if @postimagenew.save
      redirect_to post_images_path
    else
      render('post_images/new')
    end
  end

  def destroy
    @postimage = PostImage.find(params[:id])
    @postimage.destroy
    redirect_to post_images_path
  end

private
 def post_image_params
    params.require(:post_image).permit(:body, :hashbody, :user_id, post_image_images_images: [], hashtag_ids: [])
 end

Don't worry about post_image_images_images: [] for saving the images in the strong parameters as an array in another table. Hashtag_ids is entered because multiple hashtags are registered when creating PostImage.

View

Post form

views/post_images/new.html.erb



<div class= "row">
	<div class="col-lg-2 col-md-2">
	</div>
	<div class="col-xs-12 col-lg-8 col-md-8 col-sm-12">
		<div class= "postimage-new-box">
			<% if @postimagenew.errors.any? %>
  				<div id="error_explanation">
    				<h3><%= @postimagenew.errors.count %>Could not post due to input error</h3>
  					<ul>
    					<% @postimagenew.errors.full_messages.each do |msg| %>
      					<li><%= msg %></li>
    					<% end %>
    				</ul>
 				</div>
			<% end %>
			<h3>New post</h3>
			<div class="previw">
			</div>
			<%= form_with model:@postimagenew, local: true do |f| %>
				<div class="attachment-field">
					<p>Select an image (multiple images can be specified)</p>
					<%= f.attachment_field :post_image_images_images, multiple:true %>
				</div>
				<br>
				<br>
				<div class= "postimage-body-box">
					<p>Please enter the details of the post</p>
					<%= f.text_area :body, size:"55x12" %>
					<br>
					<p>Hashtag input field</p>
					<%= f.text_area :hashbody, size:"55x3" %>
					<br>
					<div class= "postimage-new">
					<%= f.submit "New post" ,class:'postimage-new-button' %>
					</div>
			<% end %>
				</div>
		</div>
	</div>
	<div class="col-lg-2 col-md-2">
	</div>
</div>

Where to actually display the hashtag in the post body

スクリーンショット 2020-06-24 11.28.55.png

The following is written in View.

ruby:post_images/show.html.erb


<%= render_with_hashtags(@postimage.hashbody) %>

The above calls the method created earlier with helper. By the way, the controller for post_images / show is here.

controllers/post_images_controller.rb


def show
    @postimage = PostImage.find(params[:id])
end

I think that it is simply passing the information of the hashtag input field of @postimage to Hellber.

Hashtag list page

スクリーンショット 2020-06-24 11.29.11.png

When you click it, it looks like this. スクリーンショット 2020-06-24 13.12.07.png

Add hashtag action to PostImage controller

post_images_controller.rb


def hashtag
    @user = current_user
    if params[:name].nil?
      @hashtags = Hashtag.all.to_a.group_by{ |hashtag| hashtag.post_images.count}
    else
      @hashtag = Hashtag.find_by(hashname: params[:name])
      @postimage = @hashtag.post_images.page(params[:page]).per(20).reverse_order
      @hashtags = Hashtag.all.to_a.group_by{ |hashtag| hashtag.post_images.count}
    end
  end

The notation here varies depending on the site you create. I wanted to create a page where you can see the hashtag list, so in the case of params [: name] .nil? There is a conditional branch that does not display post_image. Also, although it is group_by, hashtags can be displayed in the order of the number of posts associated with hashtag. It is written like this.

Hashtag View

post_images/hashtag.html.erb


<div class="row">
		<% if params[:name] == nil %>

		<% else %>
		<div class= "col-xs-12 col-lg-12 col-md-12 col-sm-12">
			<div class="hashtag-post-box">
				<h3 class="search-title">#<%= @hashtag.hashname %>:  <%= @postimage.count %>Case</h3>
				<div class="flex-box">
				<% @postimage.each do |postimage| %>
					<div class= "post-image-index-post-box">
						<p class="index-post-box-top">
							<%= postimage.created_at.strftime("%Y/%m/%d") %>
						</p>
						<span class='far fa-comments index-comment-count' id='comment-count_<%= postimage.id %>' style="color: #777777;">
							<%= render 'post_image_comments/comment-count', postimage:postimage %>
						</span>

						<span id = "favorite-button_<%= postimage.id %>"class="post-box-top-favorite">
							<%= render 'post_image_favorites/favorite',postimage: postimage %>
						</span>
		    			<%= link_to post_image_path(postimage),data: {"turbolinks" => false}  do %>
				  			<ul class="slider">
									<% postimage.post_image_images.each do |post| %>
										<li>
										<%= attachment_image_tag post, :image ,size:'430x360', format:'jpg',class:"image" %>
									</li>
								<% end %>
							</ul>
						<% end %>
						<p class="hashtag-post-box-name">
							<%= link_to user_path(postimage.user) do %>
								<%= attachment_image_tag postimage.user, :profile_image,size:'30x30', format:'jpg',fallback:'no_image.jpg',class:'min-image' %>
								<span class="index-post-box-user"><%= postimage.user.name %>
								</span>
							<% end %>
						</p>
						<div class="image-show-body-hash" style="padding:2%">
							<%= simple_format(postimage.body.truncate(50))%>
							<% if postimage.body.length > 50 %>
								<span class="text-prev"><%= link_to 'read more', post_image_path(postimage), data: {"turbolinks" => false} %>
								</span>
							<% end %>
						</div>
					</div>
				<% end %>
				</div>
			</div>
			<div class="image-index-pagination" data-turbolinks="false">
				<%= paginate @postimage,class:"paginate" %>
			</div>
		</div>
		<% end %>
	</div>
	<div class="row">
		<div class= "col-xs-12 col-lg-12 col-md-12 col-sm-12">
			<div class= "hashtag-name">
				<% @hashtags.sort.reverse.each do |count| %>
						<% count[1].each do |hashtag| %>
						<p><%= link_to  "##{hashtag.hashname} (#{hashtag.post_images.count})Case","/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
						</p>
						<% end %>
				<% end %>
			</div>
		</div>
	</div>
</div>

I'm sorry I'm confused because I've been using class names and so on. The important points are as follows.

post_images/hashtag.html.erb


<% if params[:name] == nil %>

<% else %>

<% end %>

With this notation, conditional branching is performed with post_image / hashtag and post_image / hashtag /: name written in route earlier. By writing the processing when params is nil in each of the controller and View, an error is prevented.

post_image/hashtag.html.erb



<div class= "hashtag-name">
	<% @hashtags.sort.reverse.each do |count| %>
		<% count[1].each do |hashtag| %>
			<p><%= link_to  "##{hashtag.hashname} (#{hashtag.post_images.count})Case","/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
			</p>
        <% end %>
	<% end %>
</div>

The hashtag list is displayed here. The display is displayed in descending order of posts linked to hashtags.

Summary

If you do not separate the hashtag input field, the hashtag will remain as a sentence in the description of the post, so I implemented it in the form of saving it in another column. The hashbody is intentionally hidden where the hashtag is displayed along with the post. It is turbolinks false that exists a lot on the view, but js does not work well and it is written, so you can ignore it.

I'm sorry if there are any parts that are difficult to understand in the first post. I hope it will be helpful for those who will create a portfolio from now on.

Postscript

2020/7/1 Fixed the migration file of the intermediate table. There was a phenomenon that destroy was not possible when id False was set, so we have corrected it. At the same time, I added dependent :: destroy to the model has_many.

I also added a controller and a view for the hashtag and post storage part. We apologize for any inconvenience.

Recommended Posts

Implemented hashtag search like Instagram and Twitter in Rails (no gem)
[Rails] Implemented hashtag function
[For Rails beginners] Implemented multiple search function without Gem
[Rails 6.0] I implemented a tag search function (a function to narrow down by tags) [no gem]
Calendar implementation and conditional branching in Rails Gem simple calendar
[Rails] Search from multiple columns + conditions with Gem and ransack
Gem often used in Rails
[Rails] Keyword search in multiple tables
Enable jQuery and Bootstrap in Rails 6 (Rails 6)
Remove "assets" and "turbolinks" in "Rails6".
CRUD features and MVC in Rails