我有一个存储浮点数组的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名,那么你可以使用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格式的单个文档输出,其中"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
不会抱怨并将其重新配置为数组(目前似乎是一个小错误,因为其他数组函数不存在直接输入的问题),并通过使用$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();
}
}
数组完全是随机的单小数点值,所以在我作为样本输出给出的列表结果中没有太多分布。