Le problème de base
Ce n'est pas l'idée la plus sage d'essayer de le faire dans le cadre d'agrégation à l'heure actuelle dans un avenir proche prévisible. Le problème principal vient bien sûr de cette ligne dans le code que vous avez déjà :
"items" : { "$push": "$$ROOT" }
Et cela signifie exactement que, dans la mesure où ce qui doit essentiellement se produire, c'est que tous les objets de la clé de regroupement doivent être poussés dans un tableau afin d'accéder aux "top N" résultats dans tout code ultérieur.
Cela ne s'adapte clairement pas, car la taille de ce tableau lui-même peut très bien dépasser la limite BSON de 16 Mo, et ce, quel que soit le reste des données dans le document groupé. Le hic ici étant qu'il n'est pas possible de "limiter le push" à un certain nombre d'éléments. Il y a un problème JIRA de longue date sur une telle chose.
Pour cette seule raison, l'approche la plus pratique consiste à exécuter des requêtes individuelles pour les éléments "top N" pour chaque clé de regroupement. Ceux-ci n'ont même pas besoin d'être .aggregate()
déclarations (selon les données) et peut vraiment être tout ce qui limite simplement les valeurs "top N" que vous voulez.
Meilleure approche
Votre architecture semble être sur node.js
avec mongoose
, mais tout ce qui prend en charge les E/S asynchrones et l'exécution parallèle des requêtes sera la meilleure option. Idéalement, quelque chose avec sa propre bibliothèque d'API qui prend en charge la combinaison des résultats de ces requêtes en une seule réponse.
Par exemple, il y a cet exemple simplifié de liste utilisant votre architecture et les bibliothèques disponibles (notamment async
) qui fait exactement ces résultats parallèles et combinés :
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();
}
);
Cela se traduit par seulement les 2 premiers résultats pour chaque marchand dans la sortie :
[
{
"_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
}
]
C'est vraiment le moyen le plus efficace de traiter cela même si cela va prendre des ressources car il s'agit toujours de plusieurs requêtes. Mais loin des ressources consommées dans le pipeline d'agrégation si vous essayez de stocker tous les documents dans un tableau et de le traiter.
Le problème global, maintenant et dans un avenir proche
À cette ligne, il est possible compte tenu du fait que le nombre de documents n'entraîne pas de dépassement de la limite BSON que cela peut être fait. Les méthodes avec la version actuelle de MongoDB ne sont pas idéales pour cela, mais la prochaine version (au moment de l'écriture, la branche de développement 3.1.8 le fait) introduit au moins un $slice
opérateur au pipeline d'agrégation. Donc, si vous êtes plus intelligent sur l'opération d'agrégation et utilisez un $sort
d'abord, les éléments déjà triés dans le tableau peuvent être sélectionnés facilement :
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();
}
);
Ce qui donne le même résultat de base car les 2 premiers éléments sont "tranchés" du tableau une fois qu'ils ont été triés en premier.
C'est également "possible" dans les versions actuelles, mais avec les mêmes contraintes de base en ce sens que cela implique toujours de pousser tout le contenu dans un tableau après avoir d'abord trié le contenu. Il faut juste une approche "itérative". Vous pouvez coder ceci pour produire le pipeline d'agrégation pour des entrées plus importantes, mais le simple fait d'afficher "deux" devrait montrer que ce n'est pas vraiment une bonne idée d'essayer :
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();
}
);
Et encore une fois alors que "possible" dans les versions antérieures (cela utilise les fonctionnalités introduites dans la version 2.6 pour raccourcir puisque vous balisez déjà $$ROOT
), les étapes de base consistent à stocker le tableau, puis à retirer chaque élément "de la pile" à l'aide de $first
et comparer cela (et potentiellement d'autres) aux éléments du tableau pour les supprimer, puis retirer l'élément "suivant en premier" de cette pile jusqu'à ce que votre "top N" soit finalement terminé.
Conclusion
Jusqu'au jour où il existe une telle opération qui autorise les éléments dans un $push
que l'accumulateur d'agrégation soit limité à un certain nombre, alors ce n'est pas vraiment une opération pratique pour l'agrégation.
Vous pouvez le faire, si les données que vous avez dans ces résultats sont suffisamment petites, et cela pourrait même être plus efficace que le traitement côté client si les serveurs de base de données sont de spécifications suffisantes pour fournir un réel avantage. Mais il y a de fortes chances que ni l'un ni l'autre ne soit le cas dans la plupart des applications réelles d'utilisation raisonnable.
Le mieux est d'utiliser l'option "requête parallèle" démontrée en premier. Cela va toujours bien évoluer, et il n'est pas nécessaire de "coder autour" d'une telle logique qu'un groupement particulier pourrait ne pas renvoyer au moins le total des éléments "top N" requis et déterminer comment les conserver (exemple beaucoup plus long de cela omis ) car il exécute simplement chaque requête et combine les résultats.
Utilisez des requêtes parallèles. Ce sera mieux que l'approche codée que vous avez, et cela surpassera l'approche d'agrégation démontrée de loin. Jusqu'à ce qu'il y ait au moins une meilleure option.