#382 Tagging
- Download:
- source codeProject Files in Zip (94.1 KB)
- mp4Full Size H.264 Video (27.5 MB)
- m4vSmaller H.264 Video (13.8 MB)
- webmFull Size VP8 Video (15.6 MB)
- ogvFull Size Theora Video (32.7 MB)
下のスクリーンショットは、記事の一覧を表示しているブログアプリケーションのページです。タグ機能を追加して、各記事に複数のタグをつけられるようにします。今回のエピソードでは、Railsアプリケーションにタグ機能を付加する2つの方法を紹介します。
Acts-as-Taggable-onを使ってタグ機能を付加する
タグ付け用のライブラリはいくつもありますが、飛び抜けて人気があるのがacts-as-taggable-onです。このgemは比較的古いですが、長い間メンテナンスされている数少ないgemの一つです。Railsアプリケーションで利用するには、gemfileに追加してbundleコマンドを実行してインストールします。
gem 'acts-as-taggable-on'次にこのgemが提供するジェネレータを実行してmigrationファイルを作成し、データベースのマイグレーションを行なってacts-as-taggable-onが使用するフィールドを追加します。
$ rails g acts_as_taggable_on:migration $ rake db:migrate
このマイグレーションによってtagsとtaggingsの2つのテーブルが追加されます。これらのモデルはgemに含まれているので改めて作成する必要はありません。では早速アプリケーションにタグ機能を付加する作業に入りましょう。記事の編集ページに、その記事のタグを入力するためのフィールドを追加することにします。そのためにArticleモデルに、gemが提供するacts_as_taggableメソッドの呼び出しを追加し、さらにattr_accessibleのリストにtag_listフィールドを追加します。
class Article < ActiveRecord::Base attr_accessible :content, :name, :tag_list acts_as_taggable end
次に記事の入力フォームに、タグのリストを編集するための新規のフィールドを追加します。
<div class="field"> <%= f.label :tag_list, "Tags (separated by commas)" %><br /> <%= f.text_field :tag_list %> </div>
ではこれを試してみましょう。記事を編集して新しいフィールドにいくつかのタグをカンマ区切りで入力すると、後で再度同じ記事を編集するときにはこれらのタグがリスト表示されているので、ここまでのところはうまく動いているようです。
タグ編集用に自動補完機能を付加したい場合はエピソード102を参照してください。その詳細はここでは省略します。次に各記事のタグをそれぞれの本文の下に追加します。これを行なうもっとも簡単な方法は、各記事ごとのtag_listを表示する方法です。
<h1>Articles</h1> <div id="articles"> <% @articles.each do |article| %> <h2><%= link_to article.name, article %></h2> <%= simple_format article.content %> <p>Tags: <%= article.tag_list %></p> <p><%= link_to "Edit Article", edit_article_path(article) %></p> <% end %> </div> <p><%= link_to "New Article", new_article_path %></p>
各記事のタグがindexページに表示されました。
タグがリンクになっていてクリックするとそのタグを持つ記事だけが表示されるようになっていたら便利でしょう。タグリストを設定したときにはタグをカンマで区切った単独の文字列として設定したのですが、実際にはtag_list属性は文字列の配列を返します。このタグの配列を、次のようなリンクのセットに変換できます。
<p>Tags: <%= article.tag_list.map { |t| link_to t, tag_path(t) }.join(', ') %></p>
タグのためのルートがないので設定します。すべてのタグのresourceを設定することもできますが、シンプルに保つために単一のルートを設定します。これは:tagという引数をとって、記事のindexページにマッピングします。
Blog::Application.routes.draw do get 'tags/:tag', to: 'articles#index', as: :tag resources :articles root to: 'articles#index' end
ページをリロードすると、タグ部分のHTMLがエスケープされているので、うまく動作しません。これを修復するには、このコードをエスケープされないようにrawメソッドを介して渡します。このメソッドを使用しているときは、ビュー層で少し複雑なことをしているというサインなので、ヘルパーメソッドへ移すことを検討したほうがいいでしょう。ここではそれは行ないませんが、頭の片隅には入れておいてください。
<p>Tags: <%= raw article.tag_list.map { |t| link_to t, tag_path(t) }.join(', ') %></p>
ページをリロードすると、各タグのリンクができていてそれぞれが正しいパスを指定しています。しかし、タグをクリックしてもまだ記事リストにフィルタがかかりません。
これはArticlesControllerのindexアクションで簡単に行なうことができます。:tagパラメータをチェックして、見つかったらそのタグでリストをフィルタリングします。gemにはtagged_withというメソッドがあり、ある特定のタグが付いた記事を検索することができます。
def index if params[:tag] @articles = Article.tagged_with(params[:tag]) else @articles = Article.all end end
ページをリロードすると、記事は選択したタグでフィルタがかけられています。
タグクラウドを追加する
タグリストが動作するようになりましたが、すべてのタグを見たい場合にはどうすればいいでしょうか? よく利用されるのがタグクラウドを使用する方法で、すべてのタグを人気度によってサイズを変えて表示させます。この機能をindexテンプレートの最上部の近くに追加します。gemが提供するtag_cloudメソッドがあるのでこの作業はとても簡単です。
<div id="tag_cloud"> <% tag_cloud Article.tag_counts, %w{s m l} do |tag, css_class| %> <%= link_to tag.name, tag_path(tag.name), class: css_class %> <% end %> </div>
このメソッドは2つの引数をとります。1つ目は表示させたいタグのセットです。これを簡単に取得するには、Article.tag_countsを呼び出すと、タグのリストを表示し、それぞれが使用された回数を表示します。この情報は各タグの大きさを決めるのに必要です。二つ目に渡す引数は、複数種のサイズとして使用するためのCSSクラスの配列です。またこのメソッドはブロックをとり、そこにtagオブジェクトとそれと一致するCSSクラスが渡されます。このブロックの中でタグを描画して、tagオブジェクトの名前へのlink_to、 tag_path (URLでタグ名を使いたいので、これにもtag名を渡します)、CSSクラスを渡します。タグクラウド用のスタイルをまだ定義していなかったので、ここで追加します。
#tag_cloud { width: 400px; line-height: 1.6em; .s { font-size: 0.8em; } .m { font-size: 1.2em; } .l { font-size: 1.8em; } }
記事のページをリロードすると、追加した2つのタグを含むタグクラウドが表示されます。記事にいくつかタグを追加してページをリロードすると、見た目がきれいになったタグクラウドが表示され、そこにはタグがついた記事の数によってサイズが異なるいくつかのタグが含まれています。タグをクリックすると、そのタグが付いた記事だけを表示するようフィルタがかかります。
その他にも、いろいろなことができます。現在のタグをハイライト表示したり、タグを名前順にソートしたりできますが、ここではそれは行ないません。
ゼロからタグ機能を実装する
acts-as-taggable-on gemを利用していろいろなことができましたが、同じようなものをゼロから作った場合はどれくらいの作業が必要になるでしょうか? これを行なう前に、追加した機能のいくつかを元に戻します。まずデータベースに対しておこなった変更を取り消し、マイグレーションを削除します。
$ rake db:rollback $ rails d acts_as_taggable_on:migration
次にGemfileからgemを削除して、bundleコマンドを再度実行します。ようやくこれで、タグ機能をゼロから作り直す準備ができました。まず最初にTagモデルを作成し、nameのカラムを付加します。
$ rails g model tag name
次にTaggingモデルを作成し、これをTagとArticleの両方に属する(belongs_to)ように設定します。
$ rails g model tagging tag:belongs_to article:belongs_to
gemが内部でおこなっているのと同じようにポリモーフィック関連を設定することもできますが、簡単に設定するために、ここではArticleモデルに直接リンクします。そしてrake db:migrateを実行して新規テーブルを追加します。次にモデルに関連を設定していきます。まず最初はTagです。
class Tag < ActiveRecord::Base attr_accessible :name has_many :taggings has_many :articles, through: :taggings end
acts_as_taggableの機能を置き換えるためには、Articleモデルをもう少し複雑にする必要があります。タグを管理するための関連といくつかのメソッドを追加します。
class Article < ActiveRecord::Base attr_accessible :content, :name, :tag_list has_many :taggings has_many :tags, through: :taggings def self.tagged_with(name) Tag.find_by_name!(name).articles end def self.tag_counts Tag.select("tags.*, count(taggings.tag_id) as count"). joins(:taggings).group("taggings.tag_id") end def tag_list tags.map(&:name).join(", ") end def tag_list=(names) self.tags = names.split(",").map do |n| Tag.where(name: n.strip).first_or_create! end end end
まずtaggingsとの関連とtaggingsを介したtagsとの関連を設定し、Articleモデルに対してアプリケーションの他の場所から呼び出すための4つのメソッドを実装します。tagged_withメソッドは名前が一致したタグを持つ記事を返し、tag_countsメソッドはタグとそのカウント数を返します。(このメソッドはTagモデルに置いた方がいいかも知れません。) またtag_listのgetterとsetterのメソッドも作りますが、getterが配列ではなく文字列を返すという違いがあります。このため、現状indexテンプレートがtag_listを配列として処理してしまうのでこれを修正します。article.tagsを呼び出してname属性をマップさせることで、gemと似た振る舞いになるようにします。
<%= raw article.tags.map(&:name).map { |t| link_to t, tag_path(t) }.join(', ') %>最後に書くのがtag_cloudメソッドです。これをApplicationHelperモジュールに書きます。
module ApplicationHelper def tag_cloud(tags, classes) max = tags.sort_by(&:count).last tags.each do |tag| index = tag.count.to_f / max.count * (classes.size - 1) yield(tag, classes[index.round]) end end end
このメソッドはとてもシンプルで、gemのメソッドと同じ引数を取ります。ここでもっとも件数が多いタグを探して、他のタグの相対的な出現頻度を決めて、それぞれで使用するクラスを決定します。これでおおよそ同じ機能をゼロから作ることができました。記事にいくつかのタグを追加してから一覧に戻ると、記事の横にタグが表示され、クラウドにも追加されているのがわかります。
今回の方法では、コードの大部分はArticleモデルにあります。この機能を実装するのにそれほど多くのコードを書かずに済み、アプリケーションに依存しすぎていることもありません。gemの方には、スクラッチで開発したバージョンには加えることができなかった機能がいくつかあります。例えばモデルにタグを作成するときに違うコンテキストを与えることができる点などです。gem版では所有者を記録することができるので、例えばUserをタグを付けた人としてモデルの記録に残すこともできます。
シンプルなタグ付けのソリューションが欲しければスクラッチで開発するのも一つの案です。一方フル機能のソリューションを求めるのであれば、gemを利用する方がいいでしょう。


