【问题标题】:MongoDB Calculate Values from Two Arrays, Sort and LimitMongoDB从两个数组计算值,排序和限制
【发布时间】:2016-02-25 17:38:40
【问题描述】:

我有一个存储浮点数组的 MongoDB 数据库。假设文档集合采用以下格式:

{
    "id" : 0,
    "vals" : [ 0.8, 0.2, 0.5 ]
}

有一个查询数组,例如值[ 0.1, 0.3, 0.4 ],我想为集合中的所有元素计算一个距离(例如,差异总和;对于给定的文档和查询,它将由abs(0.8 - 0.1) + abs(0.2 - 0.3) + abs(0.5 - 0.4) = 0.9 计算)。

我尝试使用 MongoDB 的聚合功能来实现这一点,但我不知道如何迭代数组。 (我没有使用 MongoDB 的内置地理操作,因为数组可能相当长)

我还需要对结果进行排序并限制到前100,所以不需要读取数据后计算。

【问题讨论】:

  • 为什么不想在单独的应用程序中进行这些计算?
  • 我真的建议在您的应用程序中执行此操作。如果可能的话,用聚合来做这件事会非常痛苦。
  • 这里我没提,最后我想把检索到的结果限制在前100个。如果我在数据库中这样做,我可以将负载分布在单个节点上,我猜?
  • @DmytroShevchenko 很遗憾,OP 没有打算“标记”任何在他们自己的评论中发表评论的人,因为这似乎是对问题的最重要的澄清,并使其在服务器。

标签: mongodb mapreduce mongodb-query aggregation-framework


【解决方案1】:

当前处理是 mapReduce

如果您需要在服务器上执行此操作并对排名靠前的结果进行排序并仅保留前 100 个,那么您可以像这样使用 mapReduce:

db.test.mapReduce(
    function() {
        var input = [0.1,0.3,0.4];
        var value = Array.sum(this.vals.map(function(el,idx) {
            return Math.abs( el - input[idx] )
        }));

        emit(null,{ "output": [{ "_id": this._id, "value": value }]});
    },
    function(key,values) {
        var output = [];

        values.forEach(function(value) {
            value.output.forEach(function(item) {
                output.push(item);
            });
        });

        output.sort(function(a,b) {
            return a.value < b.value;
        });

        return { "output": output.slice(0,100) };
    },
    { "out": { "inline": 1 } }
)

因此,mapper 函数在同一个键下进行计算和输出所有内容,因此所有结果都发送到 reducer。最终输出将包含在单个输出文档的数组中,因此所有结果都以相同的键值发出,并且每个发出的输出本身就是一个数组,这样 mapReduce 才能正常工作,这一点很重要。

排序和归约是在 reducer 本身中完成的,因为每个发出的文档都会被检查,元素被放入一个临时数组中,排序,然后返回最高的结果。

这很重要,这也是发射器将其作为数组生成的原因,即使最初是单个元素。 MapReduce 通过在“块”中处理结果来工作,因此即使所有发出的文档都具有相同的键,它们也不会一次全部处理。相反,reducer 将它的结果放回已发出结果的队列中以进行缩减,直到该特定键只剩下一个文档。

为了简洁起见,我将此处的“切片”输出限制为 10,并包括统计数据以表明观点,因为可以看到对这 10000 个样本调用的 100 个减少循环:

{
    "results" : [
        {
            "_id" : null,
            "value" : {
                "output" : [
                    {
                        "_id" : ObjectId("56558d93138303848b496cd4"),
                        "value" : 2.2
                    },
                    {
                        "_id" : ObjectId("56558d96138303848b49906e"),
                        "value" : 2.2
                    },
                    {
                        "_id" : ObjectId("56558d93138303848b496d9a"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d93138303848b496ef2"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497861"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497b58"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497ba5"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497c43"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d95138303848b49842b"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d96138303848b498db4"),
                        "value" : 2.1
                    }
                ]
            }
        }
    ],
    "timeMillis" : 1758,
    "counts" : {
            "input" : 10000,
            "emit" : 10000,
            "reduce" : 100,
            "output" : 1
    },
    "ok" : 1
}

所以这是一个单独的文档输出,采用特定的 mapReduce 格式,其中“值”包含一个元素,该元素是排序和限制结果的数组。

未来的处理是聚合的

截至撰写本文时,MongoDB 的最新稳定版本是 3.0,它缺少使您的操作成为可能的功能。但是即将发布的 3.2 版本引入了新的运算符,使这成为可能:

db.test.aggregate([
    { "$unwind": { "path": "$vals", "includeArrayIndex": "index" }},
    { "$group": {
        "_id": "$_id",
        "result": {
            "$sum": {
                "$abs": {
                    "$subtract": [ 
                        "$vals", 
                        { "$arrayElemAt": [ { "$literal": [0.1,0.3,0.4] }, "$index" ] } 
                    ]
                }
            }
        }
    }},
    { "$sort": { "result": -1 } },
    { "$limit": 100 }
])

为了简洁起见,还限制为相同的 10 个结果,您会得到如下输出:

{ "_id" : ObjectId("56558d96138303848b49906e"), "result" : 2.2 }
{ "_id" : ObjectId("56558d93138303848b496cd4"), "result" : 2.2 }
{ "_id" : ObjectId("56558d96138303848b498e31"), "result" : 2.1 }
{ "_id" : ObjectId("56558d94138303848b497c43"), "result" : 2.1 }
{ "_id" : ObjectId("56558d94138303848b497861"), "result" : 2.1 }
{ "_id" : ObjectId("56558d96138303848b499037"), "result" : 2.1 }
{ "_id" : ObjectId("56558d96138303848b498db4"), "result" : 2.1 }
{ "_id" : ObjectId("56558d93138303848b496ef2"), "result" : 2.1 }
{ "_id" : ObjectId("56558d93138303848b496d9a"), "result" : 2.1 }
{ "_id" : ObjectId("56558d96138303848b499182"), "result" : 2.1 }

这主要是因为 $unwind 被修改为在包含数组索引的结果中投影一个字段,还因为 $arrayElemAt 是一个新的运算符,可以从提供的索引。

这允许通过输入数组中的索引位置“查找”值,以便将数学应用于每个元素。输入数组由现有的$literal 运算符提供便利,因此$arrayElemAt 不会抱怨并将其识别为数组,(目前似乎是一个小错误,因为其他数组函数直接输入没有问题)并通过$unwind产生的“index”字段进行比较,得到合适的匹配索引值。

数学由$subtract 完成,当然还有$abs 中的另一个新运算符来满足您的功能。此外,由于首先需要展开数组,所有这些都在 $group 阶段内完成,该阶段累积每个文档的所有数组成员,并通过 $sum 累加器应用添加条目。

最后所有结果文档都使用$sort 处理,然后应用$limit 只返回顶部结果。

总结

即使 MongoDB 的聚合框架即将提供新功能,哪种方法实际上对结果更有效仍存在争议。这主要是因为仍然需要$unwind 数组内容,这有效地为要处理的管道中的每个数组成员生成每个文档的副本,这通常会导致开销。

因此,尽管在新版本发布之前,mapReduce 是目前唯一的方法,但它实际上可能优于聚合语句,具体取决于要处理的数据量,尽管聚合框架适用于原生编码运算符而不是比翻译的 JavaScript 操作。

与所有事情一样,始终建议进行测试以查看哪种情况更适合您的目的以及哪种情况可以为您的预期处理提供最佳性能。


示例

当然,问题中提供的示例文档的预期结果是 0.9 应用的数学。但仅出于我的测试目的,这里有一个简短的列表,用于生成一些示例数据,我想至少验证 mapReduce 代码是否正常工作:

var bulk = db.test.initializeUnorderedBulkOp();

var x = 10000;

while ( x-- ) {
    var vals = [0,0,0];

    vals = vals.map(function(val) {
        return Math.round((Math.random()*10),1)/10;
    });

    bulk.insert({ "vals": vals });

    if ( x % 1000 == 0) {
        bulk.execute();
        bulk = db.test.initializeUnorderedBulkOp();
    }
}

数组是完全随机的单个小数点值,因此我作为示例输出提供的列出的结果中没有很多分布。

【讨论】:

  • 感谢您提供这个非常详细的答案,特别是使用 MongoDB 3.2 的答案,在我看来这是更自然的一个,因为它不会尝试将 MapReduce 用于不真正遵循范式!非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-19
  • 2021-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多