【问题标题】:MongoDB mapReduce method unexpected resultsMongoDB mapReduce 方法意外结果
【发布时间】:2015-03-24 19:29:54
【问题描述】:

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

我正在尝试 mapReduce 这 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
        }]
    },

}

我在 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

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

【问题讨论】:

  • 您的 map 和 reduce 函数产品对象的形状是否完全相同?见stackoverflow.com/questions/14138344/…。如果您仍然遇到问题,请编辑您的问题以包含您的 map 和 reduce 函数。

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


【解决方案1】:

解释问题


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

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

  • 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;

替代解决方案


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

所以这确实是一个权衡的案例,更适合手头的问题。

【讨论】:

  • mapReduce 解决方案对我有用,但我仍然不确定它为什么会失败,请查看已编辑的问题。我必须在map 函数中发出带有数组属性的value 吗?
  • 附注"out": { "inline": 1 }不行,应该是"out": "inline"(我用的是mongoDB 2.6.7)
  • @kit 不,"out": { } 的格式完全一样。其他选项包括“替换”和“合并”作为“键”,“值”作为“集合名称”。所以我说得对,你输入了一些不同的东西。除非您在谈论 pymongo 实现,否则它会以不同的方式执行此操作。
  • 没关系,也许两者都是正确的。如果我使用 Robomongo 运行脚本。
  • 您的回答终于向我解释了这一点。谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-04-21
  • 2014-07-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
相关资源
最近更新 更多