MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Agrégation Mongodb $ groupe, restreindre la longueur du tableau

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 par false 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 ?