node.jsでTwitter stream を CouchDB に放り込みつつ、ブラウザに垂れ流すアプリを書いてみた

以前 node.jsでTwitter ChiarpUserStreamを閲覧するWebアプリを作った - 酒日記 はてな支店 を見て、おもしろいなぁ、と思っていたので、やってみました。

変更点は 単にtwitter streamを垂れ流すのではなく、一度CouchDBに放り込んで、CouchDB から MapReduce した結果をstreamさせるところです。MapReduce のストリーミングは、HadoopMapReduce に初めて出会ったときからやりたかったので、CouchDB ありがとう。

デモアプリを http://demo.yssk22.info/twitter-couchdb-streaming においておきました。ブラウザを開きながら CouchDB に関するつぶやきをすると、リアルタイムに表示が更新されるかと思います。

以下詳細です。

インフラ構成

実際の構成は下のような形です。

node.js を CouchDB Proxy に使っていますが、Proxy じゃなくて直接接続でも問題ないと思います。Proxy に使っているのは、とある別のアプリで twitter の oauth 認証をかます必要があって、そのアプリでこのstreamingの仕組みを作った結果です。

実際には node.js の手前に varnish がいるので、がつがつキャッシュしてくれています。

node.js で、Streaming を CouchDB に.

完全なライブラリソースはgistに→ http://gist.github.com/543500

特におもしろいことをしてるわけではなく、酒日記さんのスクリプトを参考にしつつ、ストリームデータの塊を受け取ったときにCouchDBに放り込むハンドラをかいただけです。 node.js でそのままブラウザに垂れ流す場合は、酒日記さんのほう参照で。CouchDB の場合は CouchDB 自体が commet サーバーにもなるので、その部分は CouchDB にやらせます。

CouchDBtwitter の ストリームを MapReduce する

完全なソースはgistに→ http://gist.github.com/543508

単なる垂れ流し text から正規表現で link、hashtag、mention を抽出しているぐらいはやっておきました。View の名前は timeline です。

ブラウザから CouchDB の変更されたデータをstreamで受け取る

本題はここです。クライアントでリアルタイムに、、とか面倒そうな気がしますが、そんなことはありません。

  $.couch.app(function(){
  $("#timeline").evently(this.ddoc.evently.timeline, this);
  });

ええ、これだけ。 CouchApp にくっついてくる evently という jQueryプラグインが、サーバーサイドと完全に統合された動きをしてくれます。

this.ddoc.evently.timeline とあるのが、これがサーバーサイドのJavaScriptコードの断片です。

実際には、ディレクトリとして、

+ couchapp_root
    + evently
        + _changes
            - query.json
            - data.js
            - mustache.html
            - render.text

という構成にしておきます。

まず、_changes というディレクトリですが、このディレクトリ名を使うと、CouchApp のほうで、自動的に CouchDB の _changes API に接続して、変更通知を受け取ったときに、このディレクトリ配下にあるイベント定義を動作させるような仕組みになっています。 _changes API は Long Poll で実現される CouchDBAPIです。

次に、query.json ですが、これはイベント発火時に、どのクエリを発行するかの定義をJSONで書いておきます。

{
   "view" : "timeline",
   "descending" : true,
   "type" : "newRows"
}

こんな感じでやっておくと、timeline View に descending=true のパラメータをつけるようになり、type: "newRows" のおかげで、_changes から受け取ったdoc idをベースに、前回からの変更分のみを取得するようになります。

さらに、data.js は、クエリから受け取った結果をどう使うかを定義しておきます。

function(r){
   var source = r.value.source;
   for(var k in source.user){
      source['user.' + k] = source.user[k];
   }
   source.created_at = $.prettyDate(source.created_at).toString();
   return source;
}

ここでは、Viewの結果の1行(r)をうけとって、そこから、時刻表示の変換をしているだけです。 return はテンプレートエンジンに渡すデータを指定します。テンプレートエンジンの都合から、階層化されたオブジェクトを扱えないので、for文で入れ子になっているuserオブジェクトを展開しています。

そして、mustache.html。これは evently 作者の jchris 氏が押してやまないヒゲテンプレートエンジンで、evently は今のところこのテンプレートエンジンのみに対応しています。 

<li class="jta-tweet-list-item">
  <div class="jta-tweet-profile-image">
    <a target="_blank" href="http://twitter.com/{{user.screen_name}}" 
       class="jta-tweet-profile-image-link">
      <img border="0" width="48" height="48" title="{{user.name}}" alt="{{user.name}}" 
           src="{{user.profile_image_url}}">
    </a>
  </div>
  <div class="jta-tweet-body jta-tweet-body-list-profile-image-present">
    <span class="tweet-text">{{{text}}}</span>
    <span class="jta-tweet-timestamp">
      <a href="http://twitter.com/{{user.screen_name}}">
        {{created_at}}
      </a>
    </span>
  </div>
  <div class="jta-clear">&nbsp;</div>
</li>

こんな感じで、data.js から受け取った 1つの行に対応するHTMLテンプレートを書いておきます。 jta ってあるのは単に、jTweetAnywhere の css をそのまま使いたかったから、それだけ。

最後に render.text。 これは、上で query, data, mustache で生成されたHTMLのフラグメントをどうやって、jQuery オブジェクト$('#timeline')に放り込むかの定義を、jQueryセレクタで書いておきます。新しく到着したtweetデータを挿入したいので、prepend と書いておけばOK。

evently の概要はこんな感じで、詳細は、ドキュメントはありませんが、WebCastがあるのでその辺りを聴いておけば大体わかるかと思いま。。。。。 英語なので大体x大体=ちょっとわかったので、あとはソース読んだ方がいいかも。

http://oreillynet.com/pub/e/1604

それはともかく、HTML と JavaScript だけで、本格的なリアルタイムアプリケーションをデータベース付きで実現できるんだぜ、というのは革命的な気がします。

パフォーマンスどうなのよ?

Long Poll なので、コネクションの数には多少気を遣う必要があるかも。ただし、レスポンスは良好です。

手元のML115(Opeteron 1.8GHz, 8GB RAM)で、LAN内(1Gbps)でテストしたところ

  • ab -n 10000 -c 200 のViewアクセス(Varnish -> node.js -> CouchDB)で、3500 req/sec ぐらい
  • ab -n 10000 -c 200 のViewアクセス(node.js -> CouchDB )で、100 req/sec ぐらい
  • ab -n 10000 -c 200 のViewアクセス(CouchDB直接)で、120 req/sec ぐらい