我有一个存储浮点数组的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名,因此不需要在读取数据后进行计算。
最佳答案
当前处理是mapreduce
如果需要在服务器上执行此操作,并对前100个结果进行排序,然后只保留前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能够正常工作都很重要。
排序和归约是在归约器本身中完成的,当检查每个发出的文档时,元素被放入一个临时数组中,进行排序,并返回最上面的结果。
这一点很重要,这就是为什么即使一开始只有一个元素,发射器也会将其作为数组生成。MapReduce的工作原理是处理“块”结果,因此即使所有发出的文档都具有相同的键,它们也不会同时被处理。相反,reducer将其结果放回要减少的已发出结果队列中,直到只剩下一个文档用于该特定键。
为了简洁起见,我将这里的“slice”输出限制为10,并包含一些统计数据来说明一点,因为在这个10000示例上调用的100个reduce循环可以看到:
{
"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格式,其中“value”包含一个元素,这个元素是排序和限制结果的数组。
未来的加工是聚合的
在编写本文时,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
不会抱怨并将其重新定义为数组(目前看来是一个小错误,因为其他数组函数不存在直接输入的问题),并使用“index”字段pr由$unwind
产生以作比较。数学运算由
$subtract
完成,当然还有$abs
中的另一个新操作符来满足您的功能。另外,由于首先需要展开数组,所以所有这一切都是在$group
阶段中完成的,该阶段会累积每个文档的所有数组成员,并通过$sum
累加器应用条目的添加。最后用
$sort
处理所有结果文档,然后应用$limit
返回最上面的结果。摘要
即使mongodb的聚合框架即将具备新的功能,但究竟哪种方法更有效地获得结果还是有争议的。这主要是因为仍然需要
$unwind
数组内容,这有效地为要处理的管道中的每个数组成员生成每个文档的副本,这通常会导致开销。因此,虽然MapReduce是在新版本发布之前实现这一点的唯一方法,但它实际上可能会优于聚合语句,这取决于要处理的数据量,而且尽管聚合框架在本机编码的运算符上工作,而不是在转换的JavaScriptOpe上工作口粮。
与所有事情一样,我们始终建议您进行测试,看看哪种情况更适合您的目的,哪种情况为您预期的处理提供了最佳性能。
样品
当然,问题中提供的示例文档的预期结果是所应用的数学公式的
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 - MongoDB计算两个数组的值,排序和限制,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33874022/