MapReduce むずいwww

JOINするのも一苦労ですよっと。生産性が悪いというよりは、生産を開始する前に頭を使うのに時間がかかりすぎる気がする。


ページモデル。下記はtopコンテナに3つのウィジェットインスタンス化されている例であるが、以下のような形でレイアウトの情報と、ウィジェットへのポインタが入っている(1)。

{
   "_id"     : "page1",
   "class"   : "WjPage",
   "widgets" : {
     "top"    : [{ "instance_id" : "abcdefghijk010"} },
                 { "instance_id" : "abcdefghijk011"} },
                 { "instance_id" : "abcdefghijk012"} ],
     "center" : [],
     "left"   : [],
     "right"  : [],
     "bottom" : []
   }
}

で、実際のインスタンス情報は同じDB上に以下のような形でもっている。

{
   "_id"        : "abcdefghijk010",
   "class"      : "WjWidgetInstance",
   "wj_page_id" : "page1",
   "component"  : "stikey",
   "widget"     : "text",
   "parameters" : { ... } 
}

このようにやっておくと、ページ(class : "WjPage"のほう)のレイアウトの変更情報と、ウィジェットインスタンス(class : "WjWidgetInstance"のほう)のメタデータ変更をぶつかることなく独立して行うことができる。でも、レイアウトの変更時に、ウィジェットインスタンスが参照されなくなる場合があるわけです。

たとえば、(1)の状態から次の状態にアップデートする。

{
   "_id"     : "page1",
   "class"   : "WjPage",
   "widgets" : {
     "top"    : [{ "instance_id" : "abcdefghijk011"} },
                 { "instance_id" : "abcdefghijk012"} ],
     "center" : [],
     "left"   : [],
     "right"  : [],
     "bottom" : []
   }
}

こうすると abcdefghijk010 の id を持つ、WjWidgetInstance は参照されなくなるわけです。じゃ、削除すればよいかというとそうではなくて、Page のリビジョンを戻せば(CouchDBの機能)参照されるので、これでWikiっぽくページの履歴管理ができます。

ただし、そうすると、WjWidgetInstance のほうが果てしなく増殖し続けます。なので、指定したWjPageの_idから、レイアウトされているWiWidgetInstanceのリストを取得したいわけです。

最初に書いたのが以下のMapのみを行うクエリ。

  view :widget_instances, {
    :by_page => {
      :map => <<-EOS
function(doc) {
   if( doc.class == "WjWidgetInstance") {
     emit(doc.wj_page_id, doc);
   }
}
EOS

これだと、_design/widget_instances/by_page?key="page1" で取得できそうですが、レイアウトしていないウィジェットインスタンスまで参照してしまいます。

ということで、ページの widgets プロパティの情報と、WjWidgetInstance の _id をJOINさせる必要があると。。

前置きが長くなりましたが、reduce するときにうまいことすればOK。

結構長くなったので、JavaScriptで書く。まず、map関数。

function(doc) {
   if( doc.class == "WjPage") {
     var joinkeys =  {};
     if( doc.widgets ){
        for(var l in doc.widgets){
          for(var i in doc.widgets[l] ){
             joinkeys[doc.widgets[l][i].instance_id] = true;
          }
        }
     }
     emit([doc._id, 0], {joinkeys : joinkeys});
   }
   if( doc.class == "WjWidgetInstance") {
     emit([doc.wj_page_id, 1], doc);
   }
}

ポイントは、[WjPage._id, 0 or 1] でキーにしておいて、WjPage のほうはレイアウトされたウィジェットインスタンスすべてについて、{インスタンスID : true} という形でJOIN用のキーを作っておくこと。WjWidgetInstanceの方は何も考えずにドキュメントをそのままつっこんでおきます。

で、reduce関数に渡るのは [WjWidgetInstance ドキュメント, ..., JOIN用のキー]となるはず。ただし、descending=false と group=true,そして group_level=1 確実に指定する(なぜかgroup_level=1を指定すると reduce関数に渡るのはkeyの逆順になる??)。

というわけで、reduce の values に渡る最終要素を使って それ以前の要素のフィルタリング処理を行うようにreduceを実装する。ただし、reduce が分割されて、

  • reduce([WjWidgetInstance ドキュメント, ...])
  • reduce([WjWidgetInstance ドキュメント, ...,JOIN用のキー])

な感じで実行されたらorzなのでその辺もチェックして、確実にreduceできるように!!

function(keys, values, rr){
  if( values.length > 0 ){
     // 最終要素を取る
     var last = values[values.length - 1];
     if(last.joinkeys ){  // 最後が joinkeys プロパティを含んでいればそのキーを使ってフィルタする。
        var matched = [];
        for(var i=0; i<values.length-1; i++){
          var instance = values[i];
          if( last.joinkeys[instance._id] ){
             matched.push(instance);
          }
        }
        matched.push(last);  // rereduce に備えて、最後にjoinkeys を戻しておくのを忘れずに!!
        return matched;
     }
     else{
       return values;
     }
  }
  else{
     return [];
  }
}

JOIN用のキーが先頭または末尾にくることを保証しているかどうかは不明なんだけれども。。あとでMLできいてみるか。。

あわせて読みたい

以下、追記に続く