MongoDB从两个数组(排序和限制)计算值

nav*_*ige 3 mapreduce mongodb mongodb-query aggregation-framework

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

{
    "id" : 0,
    "vals" : [ 0.8, 0.2, 0.5 ]
}
Run Code Online (Sandbox Code Playgroud)

有一个查询数组(例如具有values)[ 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的内置geo操作,因为数组可能很长)

我还需要对结果进行排序并将其限制在前100位,因此不需要读取数据后进行计算。

Bla*_*ven 5

当前处理为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 } }
)
Run Code Online (Sandbox Code Playgroud)

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

排序和归约是在化简器中完成的,在检查每个发出的文档时,将元素放入单个临时数组中进行排序,然后返回最上面的结果。

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

为了简化清单,这里将“ slice”输出限制为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
}
Run Code Online (Sandbox Code Playgroud)

因此,这是单个文档输出,采用特定的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 }
])
Run Code Online (Sandbox Code Playgroud)

为简洁起见,同样限制为相同的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 }
Run Code Online (Sandbox Code Playgroud)

之所以能够做到这一点,很大程度上是因为它$unwind被修改为在包含数组索引的结果中投影一个字段,并且还由于$arrayElemAt它是一个新运算符,可以从提供的索引中提取数组元素作为奇异值。

这样可以从输入数组中按索引位置“查找”值,以便将数学应用于每个元素。输入数组由现有的$literal运算符提供便利,因此$arrayElemAt不会抱怨并将其重新确认为数组(目前似乎是一个小错误,因为其他数组函数在直接输入方面没有问题)并获取适当的匹配索引通过使用“索引”字段产生的值$unwind进行比较。

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

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