MongoDB mapReduce方法意外结果

kit*_*kit 3 javascript mapreduce mongodb mongodb-query aggregation-framework

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

我试图mapReduce这100个文件,以具有键值对,如分组.

一切正常,直到我在DB中有第101个重复记录.

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

例如:

我现在正在使用firstName和lastName.

当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")
        },...]
    },

}
Run Code Online (Sandbox Code Playgroud)

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

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

假设第101个文件是

{
    firstName: "foo",
    lastName: "bar",
    email: "foo@bar.com",
    mobile: "019894793"
}
Run Code Online (Sandbox Code Playgroud)

包含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")
        }]
    },

}
Run Code Online (Sandbox Code Playgroud)

包含102个文件:

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

}
Run Code Online (Sandbox Code Playgroud)

我发现stackoverflow上的另一个主题有类似我的问题,但答案对我来说不起作用 MapReduce结果似乎限于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;
};
Run Code Online (Sandbox Code Playgroud)

源代码现在:

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;
};
Run Code Online (Sandbox Code Playgroud)

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

value我在map函数中发出了一些问题吗?

Nei*_*unn 5

解释问题


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

文档中经常遗漏或至少被误解的观点在文档中:

  • 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 },
    }
)
Run Code Online (Sandbox Code Playgroud)

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

    { "count": 1, "duplicate": [this] } // Note [this]
    // Same as
    var reduced = { "count": 0, "duplicate": [] };
Run Code Online (Sandbox Code Playgroud)

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

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

        return reduced;
Run Code Online (Sandbox Code Playgroud)

替代解决方案


回答的另一个原因是考虑到您期望的输出,这实际上更适合于聚合框架.它比mapReduce更快地执行此操作,并且编码起来更加简单:

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

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

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

该核心文档本身引用:

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

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