O processamento atual é mapReduce
Se você precisar executar isso no servidor e classificar os principais resultados e manter os 100 primeiros, poderá usar mapReduce para isso assim:
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 } }
)
Assim, a função mapeadora faz o cálculo e produz tudo sob a mesma chave para que todos os resultados sejam enviados ao redutor. A saída final estará contida em uma matriz em um único documento de saída, portanto, é importante que todos os resultados sejam emitidos com o mesmo valor de chave e que a saída de cada emissão seja ela mesma uma matriz para que mapReduce possa funcionar corretamente.
A ordenação e redução são feitas no próprio redutor, à medida que cada documento emitido é inspecionado os elementos são colocados em um único array temporário, ordenados, e os principais resultados são retornados.
Isso é importante, e apenas a razão pela qual o emissor produz isso como uma matriz, mesmo que seja um único elemento no início. O MapReduce funciona processando os resultados em "pedaços", portanto, mesmo que todos os documentos emitidos tenham a mesma chave, nem todos são processados de uma só vez. Em vez disso, o redutor coloca seus resultados de volta na fila de resultados emitidos para serem reduzidos até que haja apenas um único documento restante para essa chave específica.
Estou restringindo a saída "fatia" aqui a 10 para brevidade da listagem e incluindo as estatísticas para enfatizar, pois os 100 ciclos de redução chamados nesta amostra de 10.000 podem ser vistos:
{
"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
}
Portanto, esta é uma saída de documento único, no formato mapReduce específico, onde o "valor" contém um elemento que é uma matriz do resultado classificado e limitado.
O processamento futuro é agregado
No momento da escrita, a versão estável mais recente do MongoDB é 3.0, e isso não possui a funcionalidade para tornar sua operação possível. Mas a próxima versão 3.2 apresenta novos operadores que tornam isso possível:
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 }
])
Também limitando aos mesmos 10 resultados para brevidade, você obtém uma saída assim:
{ "_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 }
Isso é possível em grande parte devido a
$unwind
sendo modificado para projetar um campo nos resultados que contém o índice do array e também devido a $arrayElemAt
que é um novo operador que pode extrair um elemento de matriz como um valor singular de um índice fornecido. Isso permite a "pesquisa" de valores por posição de índice de sua matriz de entrada para aplicar a matemática a cada elemento. A matriz de entrada é facilitada pelo
$literal
operador então $arrayElemAt
não reclama e o recongiza como um array (parece ser um pequeno bug no momento, já que outras funções de array não têm o problema de entrada direta) e obtém o valor de índice correspondente apropriado usando o campo "index" produzido por $unwind
para comparação. A matemática é feita por
$subtract
e, claro, outro novo operador em $abs
para atender a sua funcionalidade. Além disso, como foi necessário desenrolar o array em primeiro lugar, tudo isso é feito dentro de um $grupo
estágio acumulando todos os membros da matriz por documento e aplicando a adição de entradas por meio do $soma
acumulador. Finalmente, todos os documentos de resultados são processados com
$sort
e, em seguida, o $limit
é aplicado apenas para retornar os principais resultados. Resumo
Mesmo com a nova funcionalidade prestes a ser disponibilizada ao framework de agregação para MongoDB é discutível qual abordagem é realmente mais eficiente para os resultados. Isso se deve em grande parte à necessidade de
$unwind
o conteúdo da matriz, que efetivamente produz uma cópia de cada documento por membro da matriz no pipeline a ser processado e que geralmente causa uma sobrecarga. Portanto, embora mapReduce seja a única maneira atual de fazer isso até um novo lançamento, ele pode realmente superar a instrução de agregação, dependendo da quantidade de dados a serem processados, e apesar do fato de a estrutura de agregação funcionar em operadores codificados nativos em vez de JavaScript traduzido operações.
Como em todas as coisas, o teste é sempre recomendado para ver qual caso se adapta melhor aos seus propósitos e qual oferece o melhor desempenho para o processamento esperado.
Amostra
Claro que o resultado esperado para o documento de amostra fornecido na pergunta é
0.9
pela matemática aplicada. Mas apenas para fins de teste, aqui está uma pequena listagem usada para gerar alguns dados de exemplo que eu queria pelo menos verificar se o código mapReduce estava funcionando como deveria: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();
}
}
As matrizes são valores de ponto decimal único totalmente aleatórios, portanto, não há muita distribuição nos resultados listados que dei como saída de amostra.