「NoSQL データベースファーストガイド」(著:佐々木達也)という本を読んでるよ。各種NoSQLのひと通りの説明とそれぞれにRubyを使ったサンプルがあって、僕のようなNoSQL知識ゼロ(NoKnowledge)の人にとってはとてもためになるよ。特にサンプルは各NoSQLの利用状況を想定して作られているから、実用的でうれしいよね。

NoSQLデータベースファーストガイド by 佐々木 達也

その中に音楽視聴ランキングサイトの楽しいサンプルがあるんだよ。それはGyaoの音楽ランキングに基づいて、YouTubeから対応動画を取ってきてリスト表示するというものだよ。一度アクセスしたデータはmemcachedを使ってサイト側で保持することで、サイトのレスポンスを良くすると共に、他サイトへの負荷を下げるという例だよ。

早速僕も同じようなサイトを作ってmemcachedの使い方を学ぶよ。本の例はRailsをベースにしてるんだけど、僕はRailsをよく知らないのでここではSinatraを使うよ。そして折角だからHerokuにpushもしてみるね。

memcached

環境はOSX Snow Leopardだよ。ローカルでmemcachedを使うには、brew install memcachedすればいいよ。very verboseモードでの起動はこうだよ。

$ memcached -vv

Herokuでmemcachedを使う場合はadd-onするだけでいいみたいなんだ1

$ heroku addons:add memcache

5MBまでは無料だけど2add-onを利用するには、クレジットカード情報などによる登録が必要だよ。登録しないで上のコマンドを実行すると、登録サイトを教えてくれるからそれに従ってね。

最初にmemcachedは何かということなんだけれども、僕はこれをリクエスト間の共有メモリ空間と理解したんだ。普通Webサーバはステートレスつまり、ユーザからの各リクエストは別々のメモリ空間で処理されるんだけど、memcachedを使うと多数のリクエストが一つの共有メモリ空間を利用できるようになる。つまりmemcachedはWebサーバをステートフルにする、こう理解したんだけどあってるかな?

GyaoRankサイト

さて最終的にでき上がったものは以下にあるよ。

http://gyaorank.heroku.com/

まあ見た目がちゃちいけど、サンプルだから許してね..調べたらGyaoでは音楽以外のランキングデータも配信していたので3、ここではそれらにも対応してみたよ。ヒット率がかなり悪いけど..

ファイル構成をまず示すよ。

├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── system_extensions.rb
└── views
    ├── index.haml
    ├── layout.haml
    └── style.scss

Gemfileの中身は以下のとおりだよ。

source :rubygems
gem 'sinatra'
gem 'haml'
gem 'sass'
gem 'dalli'
gem 'nokogiri'

本の例ではmemcachedのRubyインタフェースにmemcache-clientというのを使ってるんだけど、HerokuではSASL4というセキュリティ上の認証ができるgemしか使えないようなんだ。だからSASLに対応したDalliという変わった名前のgemを使っているよ。余談だけどdalliとnokogiriを並べて書くと、僕にはdankogaiに見えてしょうがないんだよ!

SinatraでDalliを使うときは例えば以下のようにするよ。

set :cache, Dalli::Client.new(ENV['MEMCACHE_SERVERS'],
                    :username => ENV['MEMCACHE_USERNAME'],
                    :password => ENV['MEMCACHE_PASSWORD'],
                    :expires_in => 1.day)
data = settings.cache.get(key)
settings.cache.set(key,data)

つまりDalli::Clientオブジェクトをcacheという変数にセットして、getでkeyに対応するデータの取得をし、setでkeyにdataを保存する。データの保持期間はオブジェクト生成時のoptionで指定できる。optionを指定しないならmemcache serverの指定を含めて引数は不要だよ。

次にapp.rbだけど、ちょっと長いので分けて要点だけ説明するよ。

get '/:term/:category' do |term, category|
  @term, @category, @date = term, category, Date.today
  @videos = videos(term, category, @date.to_s)
  @urls = %w(daily weekly newly).product %w(music movie drama anime owarai variety all)
  haml :index
end

Gyaoではmusic movie drama anime owarai variety allの各カテゴリデータにつき、daily weekly newlyという期間データを用意しているんだ。だからgetではそれらをパラメータとして取って、これに応じたビデオのリストをvideosメソッドで取得するようにしている。また@urlsはプルダウンメニューで使っているよ。

videosメソッドを見るよ。

helpers do
  def videos(*term_category_date)
    key = term_category_date * '/'
    if vdata = settings.cache.get(key)
      vdata
    else
      vdata = get_videos(*term_category_date)
      settings.cache.set(key, vdata)
      vdata
    end
  rescue
    get_videos(*term_category_date)
  end
end

ここではmemcachedにアクセスするためのkeyとして渡された引数term, category, dateを / で連結したものを使っているよ。最初にmemcachedに同じキーのデータがあるか見てなければget_videosメソッドでGyaoとYouTubeにアクセスして、データを取得しmemcachedにセットする。rescueでmemcachedが死んでる場合にも一応対応したよ。

次にget_videosメソッドを見るよ。

helpers do
  def get_videos(*term_category_date)
    ranking = rank_data(term_category_date.first 2)
    video_data(ranking)
  end
end

ここではrank_dataメソッドでGyaoにアクセスし、内容を解析してランキングデータを取得し、video_dataメソッドでそのデータに対応する動画をYouTubeから取得しているよ。

で、具体的なこれらの処理の内容は次のとおりだよ。Gyaoのデータはrssライブラリを使って、YouTubeのデータはnokogiriライブラリを使って解析しているよ。やっていることは本の例と基本的には同じだよ。

helpers do
  def rank_data(*term_category)
    rss = open( URL(:rank) + term_category*'/' ) { |f| RSS::Parser.parse f.read }
    rss.items.inject([]) { |mem, item| mem << item.title.scan(/[^「」]+/) }
  end
  def video_data(ranking)
    ranking.thread_with(true) do |artist, title|
      opts = ["vq=" + URI.encode("%s %s" % [artist, title]), "format=5"]*'&'
      entry = Nokogiri::XML(open [URL(:video), opts]*'?').search('entry').first
      data = { artist: artist, title:  title }
      if entry
        q = { url:    entry.xpath('media:group/media:content').first['url'],
              type:   entry.xpath('media:group/media:content').first['type'],
              count:  entry.xpath('yt:statistics').first['viewCount'] }
        data.update q
      end
      data
    end
  end
  def URL(code)
    { video: 'http://gdata.youtube.com/feeds/api/videos',
      rank:  'http://gyao.yahoo.co.jp/rss/ranking/c/' }[code]
  end
end

ただここではビデオの取得にスレッドを使っているよ。折角だからここではこの間作った Enumerable#thread_withを使ってみたよ。これはsystem_extensions.rbというファイルに置いてあるよ。

system_extensions.rbの中身はこうだよ。

# encoding: UTF-8
module Enumerable
  def thread_with(order=false)
    mem = []
    map.with_index do |*item, i|
      Thread.new(*item) do |*_item|
        mem << [i, yield(*_item)]
      end
    end.each(&:join)
    (order ? mem.sort : mem).map(&:last)
  end
end
class Fixnum
  def day
    self##60*24
  end
  alias days day
end
module Kernel
  def requires(*features)
    features.each { |f| require f.to_s }
  end
end

Fixnum#dayとKernel#requiresは app.rbでつかってるんだけどまあおまけだよ。

layout.hamlとindex.hamlには特に面白いところはないので説明は省くね。

NoSQLってなんかもっと難しいものをイメージしてたけど、全然そんなこと無いんだね。

(追記:2011-7-11)Dalli::Client.newの引数を修正しました。5

  1. http://devcenter.heroku.com/articles/memcache
  2. http://addons.heroku.com/memcache
  3. http://www.redcruise.com/search.php?srcstr=GyaO
  4. Simple Authentication and Security Layer
  5. https://github.com/mperham/dalli/wiki/Heroku-Configuration


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS