28
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RailsでインスタやTwitterのようなハッシュタグ検索を実装(gemなし)

Last updated at Posted at 2020-06-24

概要

ポートフォリオにインスタグラムやTwitterで使用されているような
ハッシュタグもどきを実装しました。
今後実装される方の参考になればと思います。

参考サイト

Railsで作ったインスタもどきのキャプションにハッシュタグを実装
https://qiita.com/goppy001/items/791c946abdb41c9495bb

大まかな流れは上記サイトと同じですが、うまく出来なかったところを変更しています。

別の実装方法にて作り直したもの

本記事ではRails(フレームワーク)のヘルパーを多用していて、正直中身が見えない部分が多いと感じ、機能を作り直して実装方法を下記にまとめました。
下記の記事内では機能を細かく作成しているので、実装をする過程で学べることは多いように感じます。
https://qiita.com/Naoki1126/items/06e75badae93fc62d52d

#完成図

投稿画面

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

投稿詳細画面

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

ハッシュタグ一覧及びハッシュタグ投稿一覧画面

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

##事前準備DB

投稿テーブル
スクリーンショット 2020-06-24 11.36.41.png

最初はbodyとuser_idのみのテーブル構成でしたが、ハッシュタグを入力するためのhashbodyカラムを追加しています。
ちなみに画像はポートフォリオの仕様上他テーブルへ保存しておりますが、imageのカラムがこのテーブル内にあっても問題はありません。

##モデル(DB)の作成
####ハッシュタグモデル

$rails g model Hashtag hashname:string

ハッシュタグ保存用のモデルを作成します。
hashnameカラムにハッシュタグが保存されます。

マイグレーションファイルの編集

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

中間テーブルの作成

$ rails g model HashtagPostImage post_image:references hashtag:references

HashtagテーブルとPostImage(投稿)テーブルの中間テーブルです。
参考にさせて頂いた記事ではここのコマンドが若干違います。
中間テーブルなので外部キーとしてhashtagとpostimageのidを持ってきます。
references型なので作成時にhashtag_idと打ってしまうと
出来上がったカラム名がhashtag_id_idとなってしまうので注意が必要です。

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

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

マイグレート

$ rails db:migrate

####作成されたDB
スクリーンショット 2020-07-01 12.13.30.png

##モデルのアソシエーションとバリデーションの設定

####ハッシュタグモデル

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

とりあえず50文字を上限にしました。

####中間テーブル

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モデル

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

##PostImageモデルに下記を追加

post_image.rb
after_create do
    post_image = PostImage.find_by(id: id)
    # hashbodyに打ち込まれたハッシュタグを検出
    hashtags = hashbody.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
    hashtags.uniq.map do |hashtag|
      # ハッシュタグは先頭の#を外した上で保存
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      post_image.hashtags << tag
    end
  end
  #更新アクション
  before_update do
    post_image = PostImage.find_by(id: id)
    post_image.hashtags.clear
    hashtags = hashbody.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
    hashtags.uniq.map do |hashtag|
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      post_image.hashtags << tag
    end
  end

作成と更新時にこのアクションが実行されるように記入してあります。

・post_image = PostImage.find_by(id: id)
作成した投稿を探させます。

・ hashtags = hashbody.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
ここでは入力されたハッシュタグ、先頭に[##]がつく入力値を探し抽出します。
hashbodyは私のDBのカラム名ですので、ここはアプリケーションにより異なります。
投稿テーブルのテキスト入力用のカラムであれば何でも良いです。

・ hashtags.uniq.map do |hashtag|
# ハッシュタグは先頭の#を外した上で保存
tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
post_image.hashtags << tag
end

tagですが今回作成しようとしているHashtagが既に存在しているかを調べ、なければ作成します。(find_or_create_by)
downcase:大文字を小文字に変換。
<<:一つの投稿に対し複数のハッシュタグを一回で保存するために使っています。配列として追加するメソッド?です。

こうしてmapで繰り返すことにより、複数のハッシュタグがpostimageに保存されます。

・post_image.hashtags.clear
更新時、一回ハッシュタグを消しているようです。

##routeの記載

routes.rb
get '/post_image/hashtag/:name' => 'post_images#hashtag'
get '/post_image/hashtag' => 'post_images#hashtag'

私の場合はハッシュタグ一覧ページを作りたかったので二つのルートを用意しました。

##PostImage ヘルパーの編集

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

hashbodyの中より正規表現にヒットする情報を取り出しgsubで変換をする処理です。
link_to word, "/post_image/hashtag/#{word.delete("#")}"
ここのurlはアプリケーションの内容によって異なります。
ハッシュタグをクリックするとここのurlに飛びますという意味です。
先ほどrouteに書いたurlを打ち込みましょう。

##PostImagコントローラー

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

ストロングパラメーター内の画像を他テーブルに配列で保存するためのpost_image_images_images:[]は気にしないでください。
hashtag_idsはハッシュタグをPostImageのcreate時に複数個登録するので記入してあります。

##View

###投稿フォーム

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 %>件の入力エラーにより、投稿出来ませんでした</h3>
  					<ul>
    					<% @postimagenew.errors.full_messages.each do |msg| %>
      					<li><%= msg %></li>
    					<% end %>
    				</ul>
 				</div>
			<% end %>
			<h3>新規投稿</h3>
			<div class="previw">
			</div>
			<%= form_with model:@postimagenew, local: true do |f| %>
				<div class="attachment-field">
					<p>画像を選択(複数指定可)</p>
					<%= f.attachment_field :post_image_images_images, multiple:true %>
				</div>
				<br>
				<br>
				<div class= "postimage-body-box">
					<p>投稿の詳細を入力してください</p>
					<%= f.text_area :body, size:"55x12" %>
					<br>
					<p>ハッシュタグ入力欄</p>
					<%= f.text_area :hashbody, size:"55x3" %>
					<br>
					<div class= "postimage-new">
					<%= f.submit "新規投稿" ,class:'postimage-new-button' %>
					</div>
			<% end %>
				</div>
		</div>
	</div>
	<div class="col-lg-2 col-md-2">
	</div>
</div>

###投稿本文内で実際にハッシュタグを表示するところ
スクリーンショット 2020-06-24 11.28.55.png

Viewにて以下を表記します。

post_images/show.html.erb
<%= render_with_hashtags(@postimage.hashbody) %>

上記は先ほどhelperで作成したメソッドを呼び出しています。
ちなみにpost_images/showのコントローラーはこちらです。

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

単純に@postimageのhashtag入力欄の情報をヘルバーに渡しているんではないかと考えています。

##ハッシュタグ一覧ページ
スクリーンショット 2020-06-24 11.29.11.png

クリックするとこんな感じです。
スクリーンショット 2020-06-24 13.12.07.png

###PostImageコントローラーにhashtagアクションの追加

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

ここの表記は作成するサイトにより変わります。
私はハッシュタグ一覧がみれるページを作りたかったので、params[:name].nil?の場合は
post_imageを表示しないという条件分岐をしています。
またgroup_byですが、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 '続きを読む', 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}) 件","/post_image/hashtag/#{hashtag.hashname}",data: {"turbolinks" => false} %>
						</p>
						<% end %>
				<% end %>
			</div>
		</div>
	</div>
</div>

クラス名とかを使いまわしていて訳わかんなくなっていてすいません。
大事なところは以下です。

post_images/hashtag.html.erb
<% if params[:name] == nil %>

<% else %>

<% end %>

この表記で先ほどrouteに書いたpost_image/hashtagとpost_image/hashtag/:nameでの条件分岐をしています。
paramsがnilのときの処理をコントローラーとViewそれぞれに書くことでエラーを起こさせないようにしています。

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

ここにハッシュタグ一覧を表示しています。
表示はハッシュタグに紐づく投稿が多い順に表示しています。

####まとめ
ハッシュタグ入力欄を別にしないと、投稿の説明文のところにそのまま文章としてハッシュタグが残ってしまうため別カラムへの保存という形で実装しました。
投稿と一緒にハッシュタグを表示させている箇所では、意図的にhashbodyを非表示にしています。
view上にやたらと存在するturbolinks falseですがjsがうまく動かなくて書いてあるものなので、無視して頂いて大丈夫です。

初めての投稿で分かりづらい箇所があれば申し訳ないです。
これからポートフォリオを作成する方の参考になれば幸いです。

###追記

2020/7/1
中間テーブルのマイグレーションファイルを修正しました。
id Falseの設定をかけるとdestroy出来なくなる現象がありましたので、修正しております。
同時にモデルのhas_manyに対しdependent: :destroyを追加しまいました。

また、ハッシュタグと投稿の保存の部分に関してコントローラーとViewを追加しました。

2020/9/17
今回、投稿と別にハッシュタグの入力欄を設けましたが、
本来であればテキスト入力欄に一緒に入力し、その中でハッシュタグを識別させる方がスマートだと思います。
それ自体はモデルでscanする対象をテキスト入力欄に指定すれば可能なのですが、
表示の時にリンクになっていないハッシュタグも一緒に出てきてしまうという問題がありました。
(重複する)

表示の際に例えば、テキスト入力欄の#から始まる部分をモデルでの処理同様にscan→deleteしてみたりすれば入力欄をわざわざ分けなくても作成できるかもしれません。

scan、delete、match、include、exclude、presentあたりを用いて上手く活用出来れば恐らくできると思います。RubyやRailsにはオブジェクトの中身を識別して操作をするためのメソッドがたくさん用意されているので、是非調べてみてください。

28
28
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
28
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?