我有一个Item
模型,它有一个属性category
。我希望按类别分组的项目计数。我为此功能编写了一个地图缩减。它工作正常。我最近编写了一个脚本来创建 5000 个项目。现在我意识到我的地图归约只给出了最后 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调用多次归约函数。如何实现我想要的功能?
在MongoDB中编写map-reduce代码时,必须遵循一条规则(实际上是一些规则)。 一个是 emit(发出键/值对)必须具有与 reduce 函数将返回的值相同的格式。
如果emit(this.key, this.value)
则 reduce 必须返回与this.value
完全相同的类型。 如果emit({},1)
则reduce必须返回一个数字。 如果emit({},{category: this.category})
则reduce必须返回格式{category:"string"}
的文档(假设类别是一个字符串)。
这显然不可能是你想要的,因为你想要总数,所以让我们看看减少是什么返回,并从中计算出你应该发出什么。
看起来最后您想累积一个文档,其中每个类别都有一个键名,其值是一个表示其出现次数的数字。 像这样:
{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;
}
请注意,它满足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的格式相匹配,而不是您拥有的原始格式,该格式将类别名称输出为键。 然而,聚合框架总是比MapReduce快得多。
我同意Neil Lunn的评论。
我从提供的信息中可以看到,如果您使用的是大于或等于 2.2 的 MongoDB 版本,您可以使用聚合框架而不是 map-reduce。
db.items.aggregate([
{ $group: { _id: '$category', category_count: { $sum: 1 } }
])
这要简单得多,性能也好得多(参见 映射/归化与聚合框架 )