Moderne
À partir de MongoDB 3.6, il existe une "nouvelle" approche en utilisant $lookup
pour effectuer une "jointure automatique" de la même manière que le traitement du curseur d'origine illustré ci-dessous.
Puisque dans cette version, vous pouvez spécifier un "pipeline"
argument de $lookup
en tant que source pour la "jointure", cela signifie essentiellement que vous pouvez utiliser $match
et $limit
pour rassembler et "limiter" les entrées du tableau :
db.messages.aggregate([
{ "$group": { "_id": "$conversation_ID" } },
{ "$lookup": {
"from": "messages",
"let": { "conversation": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
{ "$limit": 10 },
{ "$project": { "_id": 1 } }
],
"as": "msgs"
}}
])
Vous pouvez éventuellement ajouter une projection supplémentaire après le $lookup
afin que les éléments du tableau soient simplement des valeurs plutôt que des documents avec un _id
clé, mais le résultat de base est là en faisant simplement ce qui précède.
Il y a toujours l'exceptionnel SERVER-9277 qui demande en fait une "limite à pousser" directement, mais en utilisant $lookup
de cette manière est une alternative viable dans l'intervalle.
REMARQUE :Il y a aussi
$slice
qui a été introduit après avoir écrit la réponse originale et mentionné par "problème JIRA en suspens" dans le contenu original. Bien que vous puissiez obtenir le même résultat avec de petits ensembles de résultats, cela implique toujours de "tout pousser" dans le tableau, puis de limiter ultérieurement la sortie finale du tableau à la longueur souhaitée.C'est donc la principale distinction et pourquoi il n'est généralement pas pratique de
$slice
pour de grands résultats. Mais bien sûr peut être utilisé en alternance dans les cas où c'est le cas.Il y a quelques détails supplémentaires sur les valeurs de groupe mongodb par plusieurs champs concernant l'une ou l'autre utilisation alternative.
Original
Comme indiqué précédemment, ce n'est pas impossible mais certainement un problème horrible.
En fait, si votre principale préoccupation est que vos tableaux résultants seront exceptionnellement volumineux, la meilleure approche consiste à soumettre pour chaque "conversation_ID" distinct en tant que requête individuelle, puis à combiner vos résultats. Dans la syntaxe très MongoDB 2.6 qui peut nécessiter quelques ajustements en fonction de l'implémentation réelle de votre langage :
var results = [];
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID"
}}
]).forEach(function(doc) {
db.messages.aggregate([
{ "$match": { "conversation_ID": doc._id } },
{ "$limit": 10 },
{ "$group": {
"_id": "$conversation_ID",
"msgs": { "$push": "$_id" }
}}
]).forEach(function(res) {
results.push( res );
});
});
Mais tout dépend si c'est ce que vous essayez d'éviter. Passons à la vraie réponse :
Le premier problème ici est qu'il n'y a pas de fonction pour "limiter" le nombre d'éléments qui sont "poussés" dans un tableau. C'est certainement quelque chose que nous aimerions, mais la fonctionnalité n'existe pas actuellement.
Le deuxième problème est que même en poussant tous les éléments dans un tableau, vous ne pouvez pas utiliser $slice
, ou tout opérateur similaire dans le pipeline d'agrégation. Il n'existe donc aucun moyen actuel d'obtenir uniquement les "10 meilleurs résultats" d'un tableau produit avec une opération simple.
Mais vous pouvez en fait produire un ensemble d'opérations pour "découper" efficacement vos limites de regroupement. C'est assez compliqué, et par exemple ici je réduirai les éléments du tableau "tranchés" à "six" seulement. La raison principale ici est de démontrer le processus et de montrer comment le faire sans être destructeur avec des tableaux qui ne contiennent pas le total que vous voulez "trancher".
Étant donné un échantillon de documents :
{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }
Vous pouvez y voir que lors du regroupement par vos conditions, vous obtiendrez un tableau avec dix éléments et un autre avec "cinq". Ce que vous voulez faire ici, réduire les deux aux "six" premiers sans "détruire" le tableau qui ne correspondra qu'à "cinq" éléments.
Et la requête suivante :
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID",
"first": { "$first": "$_id" },
"msgs": { "$push": "$_id" },
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"seen": { "$eq": [ "$first", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"seen": { "$eq": [ "$second", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"seen": { "$eq": [ "$third", "$msgs" ] },
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"seen": { "$eq": [ "$forth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"seen": { "$eq": [ "$fifth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$fifth" },
"sixth": { "$first": "$msgs" },
}},
{ "$project": {
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"sixth": 1,
"pos": { "$const": [ 1,2,3,4,5,6 ] }
}},
{ "$unwind": "$pos" },
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [
{ "$eq": [ "$pos", 1 ] },
"$first",
{ "$cond": [
{ "$eq": [ "$pos", 2 ] },
"$second",
{ "$cond": [
{ "$eq": [ "$pos", 3 ] },
"$third",
{ "$cond": [
{ "$eq": [ "$pos", 4 ] },
"$forth",
{ "$cond": [
{ "$eq": [ "$pos", 5 ] },
"$fifth",
{ "$cond": [
{ "$eq": [ "$pos", 6 ] },
"$sixth",
false
]}
]}
]}
]}
]}
]
}
}
}},
{ "$unwind": "$msgs" },
{ "$match": { "msgs": { "$ne": false } }},
{ "$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}}
])
Vous obtenez les meilleurs résultats dans le tableau, jusqu'à six entrées :
{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }
Comme vous pouvez le voir ici, beaucoup de plaisir.
Après avoir initialement groupé, vous voulez essentiellement "faire apparaître" le $first
valeur hors de la pile pour les résultats du tableau. Pour simplifier un peu ce processus, nous le faisons en fait lors de l'opération initiale. Ainsi, le processus devient :
$unwind
le tableau- Comparer aux valeurs déjà vues avec un
$eq
correspondance d'égalité $sort
les résultats à "float"false
valeurs invisibles vers le haut (cela conserve toujours l'ordre)$group
revenir en arrière et "faire sauter" le$first
valeur invisible comme membre suivant sur la pile. Cela utilise également le$cond
opérateur pour remplacer les valeurs "vu" dans la pile du tableau parfalse
pour aider à l'évaluation.
L'action finale avec $cond
est là pour s'assurer que les futures itérations ne se contentent pas d'ajouter la dernière valeur du tableau encore et encore là où le nombre de "tranches" est supérieur aux membres du tableau.
Tout ce processus doit être répété pour autant d'éléments que vous souhaitez "découper". Comme nous avons déjà trouvé le "premier" élément du groupement initial, cela signifie n-1
itérations pour le résultat de tranche souhaité.
Les étapes finales ne sont vraiment qu'une illustration facultative de tout reconvertir en tableaux pour le résultat finalement montré. Donc, vraiment, pousser conditionnellement des éléments ou false
retour par leur position correspondante et enfin "filtrer" tous les false
valeurs afin que les tableaux de fin aient respectivement "six" et "cinq" membres.
Il n'y a donc pas d'opérateur standard pour s'adapter à cela, et vous ne pouvez pas simplement "limiter" la poussée à 5 ou 10 ou à n'importe quel élément du tableau. Mais si vous devez vraiment le faire, alors c'est votre meilleure approche.
Vous pouvez éventuellement aborder cela avec mapReduce et abandonner complètement le cadre d'agrégation. L'approche que j'adopterais (dans des limites raisonnables) serait d'avoir effectivement une carte de hachage en mémoire sur le serveur et d'y accumuler des tableaux, tout en utilisant une tranche JavaScript pour "limiter" les résultats :
db.messages.mapReduce(
function () {
if ( !stash.hasOwnProperty(this.conversation_ID) ) {
stash[this.conversation_ID] = [];
}
if ( stash[this.conversation_ID.length < maxLen ) {
stash[this.conversation_ID].push( this._id );
emit( this.conversation_ID, 1 );
}
},
function(key,values) {
return 1; // really just want to keep the keys
},
{
"scope": { "stash": {}, "maxLen": 10 },
"finalize": function(key,value) {
return { "msgs": stash[key] };
},
"out": { "inline": 1 }
}
)
Donc, cela crée simplement l'objet "en mémoire" correspondant aux "clés" émises avec un tableau ne dépassant jamais la taille maximale que vous souhaitez extraire de vos résultats. De plus, cela ne prend même pas la peine "d'émettre" l'objet lorsque la pile maximale est atteinte.
La partie réduire ne fait en fait rien d'autre que réduire essentiellement à "clé" et à une valeur unique. Donc, juste au cas où notre réducteur n'aurait pas été appelé, comme ce serait le cas s'il n'existait qu'une seule valeur pour une clé, la fonction finalize s'occupe de mapper les clés "cachées" sur la sortie finale.
L'efficacité de cela varie en fonction de la taille de la sortie, et l'évaluation JavaScript n'est certainement pas rapide, mais peut-être plus rapide que le traitement de grands tableaux dans un pipeline.
Votez pour les problèmes JIRA pour avoir un opérateur "tranche" ou même une "limite" sur "$push" et "$addToSet", ce qui serait pratique. En espérant personnellement qu'au moins quelques modifications puissent être apportées à la $map
opérateur pour exposer la valeur "index actuel" lors du traitement. Cela permettrait effectivement le "tranchage" et d'autres opérations.
Vraiment, vous voudriez coder ceci pour "générer" toutes les itérations requises. Si la réponse ici reçoit suffisamment d'amour et/ou d'autre temps en attente que j'ai en tuits, alors je pourrais ajouter du code pour montrer comment faire cela. C'est déjà une réponse assez longue.
Code pour générer le pipeline :
var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;
var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": [] } } };
for ( var x = 1; x <= maxLen; x++ ) {
fproj["$project"][""+x] = 1;
fproj["$project"]["pos"]["$const"].push( x );
var rec = {
"$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
};
if ( stack.length == 0 ) {
rec["$cond"].push( false );
} else {
lval = stack.pop();
rec["$cond"].push( lval );
}
stack.push( rec );
if ( x == 1) {
pipe.push({ "$group": {
"_id": key,
"1": { "$first": val },
"msgs": { "$push": val }
}});
} else {
pipe.push({ "$unwind": "$msgs" });
var proj = {
"$project": {
"msgs": 1
}
};
proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };
var grp = {
"$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
}
}
};
for ( n=x; n >= 1; n-- ) {
if ( n != x )
proj["$project"][""+n] = 1;
grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
}
pipe.push( proj );
pipe.push({ "$sort": { "seen": 1 } });
pipe.push(grp);
}
}
pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": stack[0] }
}
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}
});
Cela construit l'approche itérative de base jusqu'à maxLen
avec les étapes de $unwind
à $group
. Il contient également des détails sur les projections finales requises et l'énoncé conditionnel "imbriqué". Le dernier est essentiellement l'approche adoptée sur cette question :
La clause $in de MongoDB garantit-elle la commande ?