O problema básico
Não é a ideia mais sábia tentar fazer isso na estrutura de agregação atual em um futuro próximo previsível. O principal problema, claro, vem desta linha no código que você já tem:
"items" : { "$push": "$$ROOT" }
E isso significa exatamente que, basicamente, o que precisa acontecer é que todos os objetos dentro da chave de agrupamento precisam ser inseridos em uma matriz para obter os resultados "N principais" em qualquer código posterior.
Isso claramente não é dimensionado, pois eventualmente o tamanho dessa matriz pode exceder o limite BSON de 16 MB e independentemente do restante dos dados no documento agrupado. O principal problema aqui é que não é possível "limitar o push" a apenas um certo número de itens. Há um problema de longa data do JIRA sobre isso.
Por esse motivo, a abordagem mais prática para isso é executar consultas individuais para os "N principais" itens de cada chave de agrupamento. Eles nem precisam ser
.aggregate()
statments (dependendo dos dados) e pode realmente ser qualquer coisa que simplesmente limite os valores "top N" que você deseja. Melhor abordagem
Sua arquitetura parece estar em
node.js
com mongoose
, mas qualquer coisa que dê suporte a E/S assíncrona e execução paralela de consultas será a melhor opção. Idealmente, algo com sua própria biblioteca de API que suporte a combinação dos resultados dessas consultas em uma única resposta. Por exemplo, há esta lista de exemplos simplificada usando sua arquitetura e bibliotecas disponíveis (principalmente
async
) que faz isso paralelo e combinado resulta exatamente:var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var data = [
{ "merchant": 1, "rating": 1 },
{ "merchant": 1, "rating": 2 },
{ "merchant": 1, "rating": 3 },
{ "merchant": 2, "rating": 1 },
{ "merchant": 2, "rating": 2 },
{ "merchant": 2, "rating": 3 }
];
var testSchema = new Schema({
merchant: Number,
rating: Number
});
var Test = mongoose.model( 'Test', testSchema, 'test' );
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
async.each(data,function(item,callback) {
Test.create(item,callback);
},callback);
},
function(callback) {
async.waterfall(
[
function(callback) {
Test.distinct("merchant",callback);
},
function(merchants,callback) {
async.concat(
merchants,
function(merchant,callback) {
Test.find({ "merchant": merchant })
.sort({ "rating": -1 })
.limit(2)
.exec(callback);
},
function(err,results) {
console.log(JSON.stringify(results,undefined,2));
callback(err);
}
);
}
],
callback
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
Isso resulta apenas nos 2 principais resultados para cada comerciante na saída:
[
{
"_id": "560d153669fab495071553ce",
"merchant": 1,
"rating": 3,
"__v": 0
},
{
"_id": "560d153669fab495071553cd",
"merchant": 1,
"rating": 2,
"__v": 0
},
{
"_id": "560d153669fab495071553d1",
"merchant": 2,
"rating": 3,
"__v": 0
},
{
"_id": "560d153669fab495071553d0",
"merchant": 2,
"rating": 2,
"__v": 0
}
]
É realmente a maneira mais eficiente de processar isso, embora exija recursos, pois ainda são várias consultas. Mas nem de longe os recursos consumidos no pipeline de agregação se você tentar armazenar todos os documentos em uma matriz e processá-la.
O problema agregado, agora e no futuro próximo
Para essa linha, é possível considerando que o número de documentos não infrinja o limite de BSON que isso pode ser feito. Métodos com a versão atual do MongoDB não são ótimos para isso, mas a próxima versão (até o momento, 3.1.8 dev branch faz isso) pelo menos introduz um
$slice
operador para o pipeline de agregação. Então, se você for mais esperto sobre a operação de agregação e usar um $sort
primeiro, os itens já classificados na matriz podem ser escolhidos facilmente:var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var data = [
{ "merchant": 1, "rating": 1 },
{ "merchant": 1, "rating": 2 },
{ "merchant": 1, "rating": 3 },
{ "merchant": 2, "rating": 1 },
{ "merchant": 2, "rating": 2 },
{ "merchant": 2, "rating": 3 }
];
var testSchema = new Schema({
merchant: Number,
rating: Number
});
var Test = mongoose.model( 'Test', testSchema, 'test' );
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
async.each(data,function(item,callback) {
Test.create(item,callback);
},callback);
},
function(callback) {
Test.aggregate(
[
{ "$sort": { "merchant": 1, "rating": -1 } },
{ "$group": {
"_id": "$merchant",
"items": { "$push": "$$ROOT" }
}},
{ "$project": {
"items": { "$slice": [ "$items", 2 ] }
}}
],
function(err,results) {
console.log(JSON.stringify(results,undefined,2));
callback(err);
}
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
O que produz o mesmo resultado básico, pois os 2 principais itens são "fatiados" da matriz, uma vez que foram classificados primeiro.
Na verdade, também é "possível" nas versões atuais, mas com as mesmas restrições básicas, pois isso ainda envolve empurrar todo o conteúdo para uma matriz depois de classificar o conteúdo primeiro. É preciso apenas uma abordagem "iterativa". Você pode codificar isso para produzir o pipeline de agregação para entradas maiores, mas apenas mostrar "dois" deve mostrar que não é uma ótima ideia tentar:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var data = [
{ "merchant": 1, "rating": 1 },
{ "merchant": 1, "rating": 2 },
{ "merchant": 1, "rating": 3 },
{ "merchant": 2, "rating": 1 },
{ "merchant": 2, "rating": 2 },
{ "merchant": 2, "rating": 3 }
];
var testSchema = new Schema({
merchant: Number,
rating: Number
});
var Test = mongoose.model( 'Test', testSchema, 'test' );
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
async.each(data,function(item,callback) {
Test.create(item,callback);
},callback);
},
function(callback) {
Test.aggregate(
[
{ "$sort": { "merchant": 1, "rating": -1 } },
{ "$group": {
"_id": "$merchant",
"items": { "$push": "$$ROOT" }
}},
{ "$unwind": "$items" },
{ "$group": {
"_id": "$_id",
"first": { "$first": "$items" },
"items": { "$push": "$items" }
}},
{ "$unwind": "$items" },
{ "$redact": {
"$cond": [
{ "$eq": [ "$items", "$first" ] },
"$$PRUNE",
"$$KEEP"
]
}},
{ "$group": {
"_id": "$_id",
"first": { "$first": "$first" },
"second": { "$first": "$items" }
}},
{ "$project": {
"items": {
"$map": {
"input": ["A","B"],
"as": "el",
"in": {
"$cond": [
{ "$eq": [ "$$el", "A" ] },
"$first",
"$second"
]
}
}
}
}}
],
function(err,results) {
console.log(JSON.stringify(results,undefined,2));
callback(err);
}
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
E novamente, embora "possível" em versões anteriores (isso está usando recursos introduzidos 2.6 para encurtar, já que você já marca
$$ROOT
), as etapas básicas são armazenar a matriz e, em seguida, retirar cada item da pilha usando $first
e comparando isso (e potencialmente outros) com itens dentro da matriz para removê-los e, em seguida, obter o "próximo primeiro" item dessa pilha até que seu "N superior" seja concluído. Conclusão
Até que chegue o dia em que exista uma operação que permita os itens em um
$push
acumulador de agregação seja limitado a uma certa contagem, então esta não é realmente uma operação prática para agregação. Você pode fazê-lo, se os dados que você tem nesses resultados são pequenos o suficiente, e pode até ser mais eficiente do que o processamento do lado do cliente se os servidores de banco de dados tiverem especificações suficientes para fornecer uma vantagem real. Mas as chances são de que nenhum dos dois será o caso na maioria das aplicações reais de uso razoável.
A melhor aposta é usar a opção "consulta paralela" demonstrada primeiro. Ele sempre vai escalar bem, e não há necessidade de "codificar em torno" de tal lógica que um agrupamento específico pode não retornar pelo menos o total de "N principais" itens necessários e descobrir como retê-los ( muito mais exemplo disso omitido ), pois ele simplesmente executa cada consulta e combina os resultados.
Use consultas paralelas. Será melhor do que a abordagem codificada que você tem e superará em muito a abordagem de agregação demonstrada. Até que haja uma opção melhor, pelo menos.