MongoDB mapReduce方法意外结果

MongoDB mapReduce method unexpected results

本文关键字:结果 意外 方法 mapReduce MongoDB      更新时间:2023-09-26

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

我正在尝试映射减少这 100 个文档以拥有键值对,例如分组。

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

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

例如:

我现在正在研究名字和姓氏。

当数据库包含 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
        }]
    },
}

发现另一个关于堆栈溢出的主题与我一样有类似的问题,但答案对我不起作用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;
};

现在的源代码:

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问题?

解释问题


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

文档中

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

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

并在页面后面添加:

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

在您的问题的上下文中,这意味着在某个时候,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;

替代解决方案


答案的另一个原因是,考虑到您期望的输出,这实际上更适合聚合框架。它将比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 操作提供了聚合管道中当前不可用的一些灵活性。

因此,这实际上是一个权衡哪个更适合手头问题的情况。