Redis 使ってみました。

会社に転がってた、↓ の本をつらつらと眺めていて

今まで、よくわかっていなかった NoSQL という物に興味が湧いてきました。
上記の本は、そろそろ出揃った感のある NoSQL 系のデータストアエンジンを一通り並列に紹介してくれる本で、見て、軽くわかった気になるには最適です。

ただ、結局のところ、いつも使っている SQL と何が違うのかまでは読んだだけでは充分に理解できないよね、やっぱり試してみなくちゃね、という事で、使ってみました。

NoSQL と言えども、データストア!データを保存しなくては。入れるデータの元ネタに困りましたが、困った時は他人のふんどしを借りようと Twitter から適宜引っ張ってくる事にしました。キャズムを越えたらしいという噂の Twitter から URL を含んだツイートを引っ張りだして集計したら、世間で話題になっているネタが拾えそうかなと。誰かが勝手にデータをたれ流してくれるのは収集に持ってこいですし。この辺を思い付きだけでやってしまったのが後で祟る事になるのですが、そこは後述。

保存用のスクリプトは Ruby で書く事にしたので、Redis へのアクセスに redis の gem 、また、Twitter からデータを取ってくるのにTwitter の gem を使います。

Twitter はクライアントなどが自動的に URL を短縮した状態で投稿する事が多いので、それを元 URL に戻す処理も入れる事にしました。

#!/usr/bin/ruby

require "rubygems"
require "redis"
require 'twitter'
require 'uri'
require 'net/http'

$lang = "ja"

# 短縮 URL を元に戻すための処理
def extract_url(url)
  begin
    uri = URI.parse(url)
    # 引数で与えられた URL に対して HEAD メソッドを実行
    Net::HTTP.start(uri.host, uri.port) do |http|
      response = http.head(uri.path, 'User-Agent' => "Redirect Tracker")
      # 実URLだと判断する条件は、以下のいずれかに合致する事
      # ・レスポンスコードが200
      # ・レスポンスヘッダにLocationが含まれない
      # ・レスポンスヘッダLocationが、元URLと同一
      if (response.code == "200") or (response['Location'] == nil) or (url == response['Location']) then
        return url
      else
        moved_to = response['Location'].match(/^https?:\/\//) ? response['Location'] : "#{uri.host}/#{response['Location']}"
        # 二重に短縮されている場合などのために再帰処理
        extract_url(moved_to)
      end
    end
  rescue => e
    puts " === ERROR ( #{url} ) ==== "
    puts e
  end
end

begin
  # ローカルホストの Redis に接続
  redis = Redis.new(:host => "localhost")
  # 前回取得時の最終 Tweet ID を拾う
  last_id = redis.get "meta:last_id:#{$lang}"
  result = nil

  # 前回取得した以降の、日本語のツイートの中から、http という文字列で検索
  Twitter::Search.new.containing("http").since_id(last_id).lang($lang).per_page(100).filter.reverse_each do |result|
    # URL らしき文字列の切り出し
    result.text.scan(/https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:@&=+$,%#]+/).each do |url|

      # 短縮URL を元に戻す
      exturl =  extract_url(url)
      if (exturl != nil) then
        uri = URI.parse(exturl)
      else
        next
      end

      # 集計系は incr メソッドでアトミックに処理
      id = redis.incr "data:count:whole"
      host_count = redis.incr "data:count:#{uri.host}"
      url_count = redis.incr "data:count:#{uri.host}:#{uri.path}"

      # URL情報をリスト型で記録
      redis.lpush "list:whole", exturl
      redis.lpush "list:#{$lang}", exturl
      redis.lpush "list:hosts", uri.host

      # 念のため、ツイート情報も取っておく / Marshal.dump でオブジェクトを serialize する
      redis.set "data:tweet:#{id}", Marshal.dump(result)
    end
  end
rescue => e
  puts e
ensure
  # 最後に処理した Tweet ID を記録しておく
  redis.set "meta:last_id:#{$lang}", result.id
end

上記スクリプトを、cron で定期実行して、情報を収集しておきます。
読み返してみたら、検索結果を Reverse_each した上でリストに lpush してるけど、素直に each & rpush したらよかったんじゃないだろうか…。

表示系は Sinatra で間に合わせ。

require 'rubygems'
require 'sinatra'
require 'redis'
require 'uri'

set :haml, {:format => :html5}

before do
	@redis = Redis.new(:host => "localhost")
end

get %r{/([0-9]+|all)?} do
        # 全データ件数を取得
	@total_count = @redis.get "data:count:whole"
	@datas = []
        # リスト長を取得(アレ?上の全データ件数と同一では?)
	length = @redis.llen "list:whole"

        # データ表示件数を取得。デフォルトは 100 件で、指定があれば、その件数。all なら全件
	limit = 100
	if params[:captures] != nil then
		if params[:captures].first == "all" then
			limit = length
		else
			limit = params[:captures].first.to_i
		end
	end
        # downto に渡す都合で加工。リスト長が指定件数に満たない場合はリスト長を限界にする
	limit = length - (length < limit ? length - 1 : limit - 1)

        # 表示用データを配列に突っ込む
	length.downto(limit) do |i|
		url = @redis.lindex("list:whole", length - i)
		begin
			uri = URI.parse(url)
			count = @redis.get "data:count:#{uri.host}:#{uri.path}"
		rescue
			count = 0
		end
		@datas << { :url => url, :count => count }
	end
	haml :index
end

__END__

@@ layout
%html
	%head
		%title twitter url counter
	%body
		= yield

@@ index
%h1 twitter url counter
%a{:href => "/hosts"} host oriented
%div
	There are #{@total_count} datas here.
%div
	- @datas.each do |item|
		%div
			#{item[:count]} :
			%a{:href => "#{item[:url]}"} #{item[:url]}

所定の件数を Redis から取得して表示するだけです。

これで、こんな表示が出来るようになりました。

しかし、KVS ほぼ初体験な自分には「ほぼハッシュだからそのつもりで使えよ」という概念がどうも理解されていなかったようです。こんなランキング系のコンテンツなのにソートの機能を持たない list 型に突っ込んでいるのが致命的。ダラダラ出すのには使えますが、それ以外の表示方法の可能性がほぼスポイルされてしまいました。

上記の本の中にあったベンチマークのデータでは、”スピードの向こう側”に突き抜けそうなブッチ切りの爆速を示していた、Redis の速度も、集計時に、URL のリダイレクトを辿るケースが頻発する今回のプログラムではネットワークの遅延が問題になってしまって、あまり恩恵受けれず…。結局何のための Redis なのか、というところが全く見えませんでした。使ってみた、というのが唯一の成果。ちゃんとアドバンテージとディスアドバンテージを見極めた上で新技術を導入しましょうね!というお話でした。

次回は今回の反省を活かして、データストアを MongoDB とかにしてみる予定!!


参考文献

preload preload preload