[Ruby] Implement hashtag search like Instagram and Twitter with Rails (without gem)

7 minute read

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 third month’s challenge Implemented hash tag modoki. I hope that it will be useful for future implementations.

Reference site

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

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

Completion diagram

Post screen

![Screenshot 2020-06-24 11.28.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/bf2924f8-68e3-421f-ad29-(3e46b2a21b46.png)

Post details screen

![Screenshot 2020-06-24 11.28.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/2fcbbc9a-2629-0f29-83bf-(8591112533b7.png)

Hashtag list and hashtag post list screen

![Screenshot 2020-06-24 11.29.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/65423a22-9ada-c70f-ee1d-(5710931146e6.png)

Preparation DB

Post table Screenshot 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 a hash tag. By the way, images are stored in other tables due to portfolio specifications, but there is no problem if the image columns are in this table.

Creating a model (DB)

Hashtag model

$rails g model Hashtag hashname:string

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

Editing the 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 commands here are slightly different in the article that I referred to. Since it is an intermediate table, bring hashtag and postimage id as foreign keys. Since it is a references type, if you hit hashtag_id when creating it 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

MIGRATE

$ rails db:migrate

Created DB

Screenshot 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 have set the 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

Add the following to the PostImage model

post_image.rb


after_create do
    post_image = PostImage.find_by(id: id)
    Detect the hashtag typed in #hashbody
    hashtags = hashbody.scan(/[# #][\w\p{Han}a-ga--ー]+/)
    hashtags.uniq.map do |hashtag|
  #Hash tag is saved after removing the # at the beginning
      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}a-ga--ー]+/)
    hashtags.uniq.map do |hashtag|
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      post_image.hashtags << tag
    end
  end

I have written that this action will be performed on creation and update.

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

・Hashtags = hashbody.scan(/[# #][\w\p{Han}a-ga-ーー]+/) Here, search for the input hash tag and the input value with [# #] at the beginning. Since hashbody is the column name of my DB, it depends on the application. Any column can be used as long as it is a text input column of the post table.

・Hashtags.uniq.map do |hashtag| #Hash tag is saved after removing the # at the beginning tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete(‘#’)) post_image.hashtags « tag end

Multiple hashtags will be saved in the postimage by iterating through the map.

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

Route description

routes.rb


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

In my case, I wanted to make 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}a-ga--ー]+/) {|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. This means that if you click on the hashtag you will be taken to the url here. Let’s enter 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:[] to save the images in the strong parameters as an array in another table. hashtag_ids is entered because multiple hashtags will be 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">
    Could not post due to <h3><%= @postimagenew.errors.count %> input errors</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 images (multiple selections allowed)</p>
<%= f.attachment_field :post_image_images_images, multiple:true %>
</div>
<br>
<br>
<div class= "postimage-body-box">
<p>Enter post details</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 the hashtag is actually displayed in the post text

![Screenshot 2020-06-24 11.28.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/70b5a059-9573-1aa6-d66f-(9fe19ca8a868.png)

The following is displayed in View.

ruby:post_images/show.html.erb


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

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

controllers/post_images_controller.rb


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

I’m wondering if I’m simply passing the information in the hashtag input field of @postimage to Hellver.

Hashtag list page

![Screenshot 2020-06-24 11.29.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/4d1f4c4b-93c2-4948-dd62-(6afbc3e08c53.png)

When you click, it looks like this. ![Screenshot 2020-06-24 13.12.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/661300/fac217b3-6876-2f36-098b-(173106c966b5.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 changes depending on the site you create. I wanted to make a page where the list of hashtags could be seen, so in the case of params[:name].nil? There is a conditional branch that does not display post_image. Also for group_by, so that hashtags can be displayed in the order that there are many posts tied to 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 %> </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}) Items", "/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
</p>
<% end %>
<% end %>
</div>
</div>
</div>
</div>

I’m sorry that I don’t understand because I’m using class names all over again. The important points are as follows.

post_images/hashtag.html.erb


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

<% else %>

<% end %>

With this notation, we are conditional branching with post_image/hashtag and post_image/hashtag/:name that we wrote in route earlier. I am trying not to cause an error by writing the processing when params is nil in controller and view respectively.

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}) Items", "/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
</p>
        <% end %>
<% end %>
</div>

A list of hashtags is displayed here. The display is displayed in order of the number of posts associated with hashtags.

Summary

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

I am sorry if there is a part that is difficult to understand in the first post. We hope that it will be useful for those who are creating portfolios.

Added

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

We’ve also added a controller and a view for the hashtag and save post part. We apologize for any inconvenience.