やっぱり MapReduce むずいけど ...

またまた自分メモ。

CouchDBのJOINはこのあたりが詳しいが単純なケースにしか通用しない、という話。以下、今書いているブログアプリを例に取る。


ユーザー毎にブログを作成できるようにしたいので、Setting => Entry => Comment のような関連ができる。Setting はブログの設定、アクセス制御とかその辺のパラメーターをもっていて、Entryはエントリーそのもの、Comment はエントリーにつけられたコメント。

で、まぁとりあえず、Setting と Entry はドキュメントを別々にする。1ブログ1Setting, N Entryドキュメント。で、Comment は難しいところだが、Entryドキュメントないに comments という配列でつっこむことにした。正規化なんてしない!コメントはどうせ追加するだけで、更新も削除することもまれなので。

ここで問題が。すべての公開されているブログの最近のエントリー一覧というビューがうまく定義できない。

// Setting
{ id: "yssk22", permission_view : {all :true, tags: [...] }, updated_at : ... }
// Entry
{ id: "{UUID}", setting_id : "yssk22", title : "...", content : "...", comments : [], updated_at: ... }

こんな感じで、setting.permission_view.all == true が公開ブログという意味。公開ブログを取り出すだけであれば、

function(doc){
  if( doc.type == "Setting") {
    if(doc.setting.permission_view.all){
      emit(doc.id, doc);
    }
  }
}
||

だが、公開ブログのエントリーを取り出すとなると、Setting と Entry でJOINが必要。

>|js|
function(doc){
  if( doc.type == "Setting") {
    if(doc.setting.permission_view.all){
      emit([doc.id, 0], doc);
    }
  }
  else if( doc.type == "Entry") {
      emit([doc.setting_id, 1], doc);
  }
}
||

ここまではOKで、「最近のエントリー」 という話になると、updated_at をkeyに絡ませる必要がある。

>|js|
function(doc){
  if( doc.type == "Setting") {
    if(doc.setting.permission_view.all){
      emit([doc.id, 0], doc);
    }
  }
  else if( doc.type == "Entry") {
      emit([doc.setting_id, 1, doc.updated_at], doc);
  }
}
||

これで、「あるブログに関して設定と最近のエントリーN件」を取り出すことは可能。問題は、順番が逆で、すべてのブログエントリについてupdated_atの降順にならべて、その中から公開ブログのエントリーを取り出す、となるので、


>|js|
function(doc){
  if( doc.type == "Setting") {
    if(doc.setting.permission_view.all){
      emit([null, doc.id, 0], doc);
    }
  }
  else if( doc.type == "Entry") {
      emit([doc.updated_at, doc.setting_id, 1], doc);
  }
}
||

な感じで、updated_at をキーの先頭に持ってくる必要がある。しかし、これだと、null, doc.updated_at で集められてしまうので、JOINが成立しない。

じゃ、いっそのこと、

>|js|
function(doc){
  if( doc.type == "Entry") {
     emit(doc.updated_at, doc.setting_id);
  }
}
||

で、最新の更新順にsetting_id を必要な件数(m件)取り出して、

>|js|
function(doc){
  if( doc.type == "Setting") {
     emit([setting.permission_view.all, doc._id], null);
  }
}
||

これで、startkey=[true]&endkey=[true, "\u0000"] で取り出せた、エントリーだけ残すようにする、という案。しかし、例えば、全部でN件のエントリーがあったとして、すべてのエントリーのブログの設定が permnission_view.all==false だとした場合に、N / m * 2 回のビューリクエストが発生するので、m=50, N = 10000 だとしても200回であって、おーい、という話だ。Web見知りをする30人が毎日1エントリーを起こすと、1年でこの規模に達する。

というわけで解決案。

Setting 側に最新のエントリー数件はidのリンクを持たせる。

>|js|
// Setting
{ id: "yssk22", permission_view : {all :true, tags: [...] }, updated_at : ...,
  recent_updated: [{entry_id : "id1", updated_at: ...}, ...]
}

これであれば、

function(doc){
  if( doc.type == "Setting") {
    if(doc.setting.permission_view.all){
       for(var i in doc.recent_updated_ids){
         emit(doc.recent_updated_ids[i].updated_at, doc.recent_updated_ids[i].entry_id);
       }
    }
  }
}

で、更新日順に並び替えられたエントリーIDの一覧で、すべてpermission_view.all == true なもの、が取得できる。

Ruby で実装する際は、自作のマッパーに ActiveSupport::Callbacks を入れて、いわゆるEntry#after_save でSetting#recent_updated を書き換えるようにした。(本当は_bulk_update を使って両方更新されることを1トランザクションであつかった方がいいのかもしれないが、ブログの更新情報が1回分欠落したって誰も文句は言わないと思う)。