mongodbのmapReduceのscopeで変数は渡せるけれど、関数を渡せない問題の回避策

下記のようにmongodbのmapReduceのmap/reduce関数内でちょっとした関数を呼びたい場合があります。(sampleです)

  • sampleMR.js
var getCareer = function(ua){
  if(ua.indexOf('DoCoMo') === 0){
    return 'DoCoMo';
  }else if(ua.indexOf('KDDI') === 0){
    return 'KDDI';
  }else if(ua.indexOf('SoftBank') === 0){
    return 'SoftBank';
  }else{
    return null;
  }
};

var map = function(){
  if(this.ua){
    emit(getCareer(this.ua), 1);
  }
};

var reduce = function(k, vs){
  var sum = 0;
  vs.forEach(function(v){
    sum += v;
  });
  return sum;
};

db.logs.mapReduce(map, reduce, {out: 'logs.career'});

これを実行すると、下のようなエラーがでて終わってしまいます。

$ mongo access sampleMR.js 
MongoDB shell version: 1.8.0-rc0
connecting to: access
Sat Mar 26 02:16:17 uncaught exception: map reduce failed:{
	"assertion" : "map invoke failed: JS Error: ReferenceError: getCareer is not defined nofile_b:1",
	"assertionCode" : 9014,
	"errmsg" : "db assertion failure",
	"ok" : 0
}
failed to load: sampleMR.js

scopeを使う

これに対して、scopeというoptionを追加するとglobalに登録するので変数が利用出来るようです
MapReduce - MongoDB

var getCareer = function(ua){
  /* 略 */
};
var map = function(){
  /* 略 */
};
var reduce = function(k, vs){
  /* 略 */
};
db.logs.mapReduce(map, reduce, {scope: {x: 0, getCareer: getCareer}, out: 'logs.career'});

mongodbのdocumentではscopeでglobalにくっつけてmap/reduce/finalizeから使えるよーっとあるのですが(サンプル mongo/jstests/mr4.js at master · mongodb/mongo · GitHub) 、どうにも使えません。
 関数以外を渡すとたしかに動作しているのですが、関数を渡すとあいかわらず使えません。

map関数内で定義する

var map = function(){
  var getCareer = function(ua){ /* 略 */ };
  if(this.ua){
    emit(getCareer(this.ua), 1);
  }
};
/* 略 */

これは動作しますが、map関数はおそらくdocの数だけ起動される=関数オブジェクトがdocの数だけ生成されては破棄される、というのは避けたいです。

クロージャを使う

クロージャを使って関数を渡してみます。

var map = function(){
  var getCareer = function(ua){ /* 略 */ };
  return function(){
    if(this.ua){
      emit(getCareer(this.ua), 1);
    }
  };
}();

これにしても

$ mongo access sampleMR.js 
MongoDB shell version: 1.8.0-rc0
connecting to: access
Sat Mar 26 02:51:39 uncaught exception: map reduce failed:{
	"assertion" : "map invoke failed: JS Error: ReferenceError: getCareer is not defined nofile_b:2",
	"assertionCode" : 9014,
	"errmsg" : "db assertion failure",
	"ok" : 0
}
failed to load: sampleMR.js

冷たい応答です…><

db.system.jsで渡す

@doryokujinさんにtweeterでもらったのですがWorking With Stored JavaScript in MongoDB - Mike Dirolfを見ると「collection db.system.jsに関数を登録するとstored JSとして使えるよー」な感じです。

db.system.js.save({"_id": "getCareer", "value": function(ua){ /* 略 */ } });
var map = function(){
 /* 略 */
};
/* 略 */
db.logs.mapReduce(map, reduce, {out: 'logs.career'});

これを実行してみると、うまくいきます!!

$ mongo access --eval 'db.logs.career.find().forEach(function(o){printjson(o)})'
{ "_id" : null, "value" : 122453 }
{ "_id" : "DoCoMo", "value" : 47024 }
{ "_id" : "KDDI", "value" : 21720 }
{ "_id" : "SoftBank", "value" : 7971 }

別ファイルに書いておいて保存

毎回、関数を定義するのもやなので、util.jsみたいな名前をつけておいて

var getCareer = function(ua){
  /* 略 */
};
var hoge = function(args){};
var fuga = function(args){};

db.system.js.save({"_id": "getCareer", "value": getCareer});
db.system.js.save({"_id": "hoge", "value": hoge});
db.system.js.save({"_id": "fuga", "value": fuga});

これらに変更があったときに

$ mongo access util.js

として登録するといいと思います(system.jsはdbのcollectionの一つで、stored JSなのでmongodを再起動しても普通に使えます)

これで、mapReduceやgroupも怖くない!

ただ、結局、map/reduce/finalizeの環境はなんなんだろう・・・mapはthisにbindされてのはわかるんだけど・・・