Mongoid MapReduce为递归reduce函数提供不规则的结果

时间:2014-04-01 14:21:56

标签: ruby-on-rails mongodb mapreduce mongoid

我有一个Item模型,其属性为category。我希望项目计数按类别分组。我为这个功能写了一个地图reduce。它工作正常。我最近写了一个脚本来创建5000个项目。现在我意识到我的map reduce只给出了最后80条记录的结果。以下是mapreduce函数的代码。

map = %Q{
  function(){
    emit({},{category: this.category});
  }
}

reduce = %Q{
  function(key, values){
    var category_count = {};
    values.forEach(function(value){
      if(category_count.hasOwnProperty(value.category))
        category_count[value.category]++;  
      else
        category_count[value.category] = 1 
    })
    return category_count;
  }
}

Item.map_reduce(map,reduce).out(inline: true).first.try(:[],"value")

经过研究,我发现了mongodb invokes reduce function multiple times。如何实现我想要的功能?

2 个答案:

答案 0 :(得分:2)

在MongoDB(a few rules, actually)中编写map-reduce代码时,必须遵循一条规则。一个是emit(发出键/值对)必须具有与reduce函数返回的值相同的格式。

如果您emit(this.key, this.value),则reduce必须返回与this.value完全相同的类型。如果你emit({},1)那么reduce必须返回一个数字。如果你emit({},{category: this.category})那么reduce必须返回格式为{category:"string"}的文档(假设category是一个字符串)。

所以显然不能成为你想要的东西,因为你想要总数,所以让我们来看看减少的东西是什么,并从你应该发出的东西中解决。

在最后,您希望累积一个文档,其中每个类别都有一个键名,其值是一个表示其出现次数的数字。类似的东西:

{category_name1:total, category_name2:total}

如果是这种情况,那么正确的地图功能将emit({},{"this.category":1}),在这种情况下,您的reduce需要将每个键对应一个类别的数字相加。

这是地图的样子:

map=function (){
     category = { };
     category[this.category]=1;
     emit({},category);
}

这是正确的相应减少:

reduce=function (key,values) {
     var category_count = {};
     values.forEach(function(value){
        for (cat in value) {
           if( !category_count.hasOwnProperty(cat) ) category_count[cat]=0;
           category_count[cat] += value[cat];
        }
     });
     return category_count;
}

请注意,它满足two other requirements for MapReduce - 如果reduce函数是从不调用它会正常工作(如果集合中只有一个文档就是这种情况)并且它会工作正确的话,如果多次调用reduce函数(当你有超过100个文档时就会发生这种情况)。

更常规的方法是将类别名称作为键,将数字作为值发出。这简化了地图并减少了:

map=function() { 
   emit(this.category, 1);
}

reduce=function(key,values) {
    var count=0;
    values.forEach(function(val) {
        count+=val;
    }
    return count;
}

这将总结每个类别出现的次数。这个满足MapReduce的要求 - 如果永远不会调用reduce函数(对于只出现一次的任何类别都是如此),它可以正常工作,并且如果reduce函数被调用多次,它将正常工作时间(如果任何类别出现超过100次,将会发生这种情况)。

正如其他人所指出的那样,聚合框架使得同样的练习更加简单:

db.collection.aggregate({$group:{_id:"$category",count:{$sum:1}}})

虽然它与我展示的第二个mapReduce的格式相匹配,而不是你输出类别名称作为键的原始格式。但是aggregation framework will always be significantly faster than MapReduce

答案 1 :(得分:0)

我同意Neil Lunn的评论。

从我提供的信息中我可以看到,如果您使用的MongoDB版本大于或等于2.2,则可以使用聚合框架而不是map-reduce。

db.items.aggregate([
  { $group: { _id: '$category', category_count: { $sum: 1 } }
])

这更简单,更高效(见Map/Reduce vs. Aggregation Framework