MongoDB mapReduce方法意外结果

时间:2015-01-26 11:28:05

标签: javascript mongodb mapreduce mongodb-query aggregation-framework

我的mongoDB中有100个文档,假设它们中的每一个都可能与不同条件下的其他文档重复,例如firstName&姓氏,电子邮件和手机。

我正在尝试mapReduce这100个文档,以获得键值对,如分组。

一切正常,直到我在数据库中有第101条重复记录。

与第101条记录重复的其他文档的mapReduce结果输出已损坏。

例如:

我正在开发firstName& lastName now。

当DB包含100个文档时,我可以得到包含

的结果
{
    _id: {
        firstName: "foo",
        lastName: "bar,
    },
    value: {
        count: 20
        duplicate: [{
            id: ObjectId("/*an object id*/"),
            fullName: "foo bar",
            DOB: ISODate("2000-01-01T00:00:00.000Z")
        },{
            id: ObjectId("/*another object id*/"),
            fullName: "foo bar",
            DOB: ISODate("2000-01-02T00:00:00.000Z")
        },...]
    },

}

这正是我想要的,但是......

当数据库包含100多个可能的重复文档时,结果就像这样,

假设第101个文件是

{
    firstName: "foo",
    lastName: "bar",
    email: "foo@bar.com",
    mobile: "019894793"
}

包含101个文件:

{
    _id: {
        firstName: "foo",
        lastName: "bar,
    },
    value: {
        count: 21
        duplicate: [{
            id: undefined,
            fullName: undefined,
            DOB: undefined
        },{
            id: ObjectId("/*another object id*/"),
            fullName: "foo bar",
            DOB: ISODate("2000-01-02T00:00:00.000Z")
        }]
    },

}

包含102个文件:

{
    _id: {
        firstName: "foo",
        lastName: "bar,
    },
    value: {
        count: 22
        duplicate: [{
            id: undefined,
            fullName: undefined,
            DOB: undefined
        },{
            id: undefined,
            fullName: undefined,
            DOB: undefined
        }]
    },

}

我发现stackoverflow上的另一个主题有类似我的问题,但答案对我不起作用 MapReduce results seem limited to 100?

有什么想法吗?

编辑:

原始源代码:

var map = function () {
    var value = {
        count: 1,
        userId: this._id
    };
    emit({lastName: this.lastName, firstName: this.firstName}, value);
};

var reduce = function (key, values) {
    var reducedObj = {
        count: 0,
        userIds: []
    };
    values.forEach(function (value) {
        reducedObj.count += value.count;
        reducedObj.userIds.push(value.userId);
    });
    return reducedObj;
};

源代码:

var map = function () {
    var value = {
        count: 1,
        users: [this]
    };
    emit({lastName: this.lastName, firstName: this.firstName}, value);
};

var reduce = function (key, values) {
    var reducedObj = {
        count: 0,
        users: []
    };
    values.forEach(function (value) {
        reducedObj.count += value.count;
        reducedObj.users = reducedObj.users.concat(values.users); // or using the forEach method

        // value.users.forEach(function (user) {
        //     reducedObj.users.push(user);
        // });

    });
    return reducedObj;
};

我不明白为什么它会失败,因为我也把一个值(userId)推到reducedObj.userIds

我在value函数中发出的map是否存在一些问题?

1 个答案:

答案 0 :(得分:5)

解释问题


这是一个常见的mapReduce陷阱,但显然您遇到的问题的一部分是您找到的问题没有能够清楚甚至正确解释这些问题的答案。所以答案在这里是合理的。

文档中经常遗漏或至少被误解的一点是here in the documentation

  
      
  • MongoDB可以为同一个密钥多次调用reduce函数。在这种情况下,该键的reduce函数的先前输出将成为该键的下一个reduce函数调用的输入值之一。
  •   

稍后在页面上添加:

  
      
  • 返回对象的类型必须相同,与value函数发出的map类型相同。
  •   

在您的问题的上下文中,这意味着在某个时刻有“太多”重复键值传入 reduce 阶段以执行操作在一次通过中,因为它可以用于较少数量的文档。根据设计,reduce方法被多次调用,通常从已经减少的数据中获取“输出”,作为另一次传递的“输入”的一部分。

这就是mapReduce用于处理非常大的数据集的方式,通过处理“块”中的所有内容,直到它最终“减少”到每个键的单个分组结果。这就是为什么下一个语句很重要的原因是emitreduce输出的结构需要完全相同,以便reduce代码能够正确处理它。

解决问题


您可以通过修正map中的数据发布方式以及如何在reduce函数中返回和处理来更正此问题:

db.collection.mapReduce(
    function() {
        emit(
            { "firstName": this.firstName, "lastName": this.lastName },
            { "count": 1, "duplicate": [this] } // Note [this]
        )
    },
    function(key,values) {
        var reduced = { "count": 0, "duplicate": [] };
        values.forEach(function(value) {
            reduced.count += value.count;
            value.duplicate.forEach(function(duplicate) {
                reduced.duplicate.push(duplicate);
            });
        });

        return reduced;
    },
    { 
       "out": { "inline": 1 },
    }
)

关键点可以在emit的内容和reduce函数的第一行中看到。基本上这些呈现的是相同的结构。在emit的情况下,生成的数组只有一个单数元素并不重要,但无论如何都是这样发送的。并排:

    { "count": 1, "duplicate": [this] } // Note [this]
    // Same as
    var reduced = { "count": 0, "duplicate": [] };

这也意味着reduce函数的其余部分将始终假定“重复”内容实际上是一个数组,因为它是原始输入的方式,也是它的返回方式:

        values.forEach(function(value) {
            reduced.count += value.count;
            value.duplicate.forEach(function(duplicate) {
                reduced.duplicate.push(duplicate);
            });
        });

        return reduced;

替代解决方案


答案的另一个原因是考虑到你期望的输出,这实际上更适合aggregation framework。它会比mapReduce更快地完成这项任务,并且编码起来更加简单:

db.collection.aggregate([
    { "$group": {
       "_id": { "firstName": "$firstName", "lastName": "$lastName" },
       "duplicate": { "$push": "$$ROOT" },
       "count": { "$sum": 1 }
    }},
    { "$match": { "count": { "$gt": 1 } }}
])

就是这样。您可以通过在需要的位置添加$out阶段来写出集合。但基本上无论是mapReduce还是聚合,通过将“重复”项添加到数组中,您仍然会对文档大小设置相同的16MB限制。

另请注意,您可以简单地执行mapReduce无法在此处执行的操作,并且只是“省略”结果中实际上不是“重复”的任何项目。如果没有首先向集合生成输出,然后在单独的查询中“过滤”结果,mapReduce方法就无法做到这一点。

核心文档本身引用:

  


  对于大多数聚合操作,聚合管道提供更好的性能和更一致的接口。但是,map-reduce操作提供了一些目前在聚合管道中不可用的灵活性。

所以这真的是一个称重的案例,它更适合手头的问题。