MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Mongodb-Aggregation $group, Länge des Arrays einschränken

Modern

Ab MongoDB 3.6 gibt es dafür einen "neuartigen" Ansatz, indem $lookup verwendet wird um eine "Selbstverknüpfung" auf die gleiche Weise durchzuführen wie die ursprüngliche Cursor-Verarbeitung, die unten gezeigt wird.

Da Sie in dieser Version eine "pipeline" angeben können Argument für $lookup Als Quelle für den "Join" bedeutet dies im Wesentlichen, dass Sie $match verwenden können und $limit um die Einträge für das Array zu sammeln und zu "begrenzen":

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"
  }}
])

Sie können optional eine zusätzliche Projektion nach $lookup hinzufügen um die Array-Elemente einfach die Werte statt Dokumente mit einer _id zu machen Schlüssel, aber das grundlegende Ergebnis ist da, indem Sie einfach das oben Gesagte tun.

Es gibt noch den ausstehenden SERVER-9277, der tatsächlich direkt ein "Limit to push" anfordert, aber mit $lookup auf diese Weise ist in der Zwischenzeit eine praktikable Alternative.

HINWEIS :Es gibt auch $slice die nach dem Schreiben der ursprünglichen Antwort eingeführt und im ursprünglichen Inhalt durch "ausstehendes JIRA-Problem" erwähnt wurde. Während Sie mit kleinen Ergebnismengen dasselbe Ergebnis erzielen können, müssen Sie immer noch "alles" in das Array schieben und dann später die endgültige Array-Ausgabe auf die gewünschte Länge begrenzen.

Das ist also der Hauptunterschied und warum es im Allgemeinen nicht praktikabel ist, $slice zu verwenden für große Ergebnisse. Kann aber natürlich alternativ verwendet werden, wenn es so ist.

Es gibt ein paar weitere Details zu mongodb-Gruppenwerten nach mehreren Feldern zu beiden alternativen Verwendungen.

Original

Wie bereits erwähnt, ist dies nicht unmöglich, aber sicherlich ein schreckliches Problem.

Wenn Ihr Hauptanliegen darin besteht, dass Ihre resultierenden Arrays außergewöhnlich groß werden, ist es am besten, für jede einzelne "conversation_ID" eine einzelne Abfrage einzureichen und dann Ihre Ergebnisse zu kombinieren. In sehr MongoDB 2.6-Syntax, die möglicherweise etwas angepasst werden muss, je nachdem, was Ihre Sprachimplementierung tatsächlich ist:

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 );
    });
});

Aber es hängt alles davon ab, ob Sie das vermeiden wollen. Also weiter zur eigentlichen Antwort:

Das erste Problem hier ist, dass es keine Funktion gibt, um die Anzahl der Elemente zu "begrenzen", die in ein Array "geschoben" werden. Es ist sicherlich etwas, was wir gerne hätten, aber die Funktionalität ist derzeit nicht vorhanden.

Das zweite Problem ist, dass Sie $slice nicht verwenden können, selbst wenn Sie alle Elemente in ein Array verschieben , oder einen ähnlichen Operator in der Aggregationspipeline. Daher gibt es derzeit keine Möglichkeit, mit einer einfachen Operation nur die "Top 10"-Ergebnisse aus einem erstellten Array zu erhalten.

Aber Sie können tatsächlich eine Reihe von Operationen erstellen, um Ihre Gruppierungsgrenzen effektiv zu "slicen". Es ist ziemlich kompliziert, und zum Beispiel werde ich hier die Array-Elemente "in Scheiben geschnitten" auf nur "sechs" reduzieren. Der Hauptgrund hier ist, den Prozess zu demonstrieren und zu zeigen, wie man dies macht, ohne mit Arrays destruktiv zu sein, die nicht die Gesamtsumme enthalten, auf die Sie "slicen" möchten.

Gegeben ein Beispiel von Dokumenten:

{ "_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 }

Sie können dort sehen, dass Sie beim Gruppieren nach Ihren Bedingungen ein Array mit zehn Elementen und ein weiteres mit "fünf" erhalten. Was Sie hier tun möchten, reduzieren Sie beide auf die oberen "sechs", ohne das Array zu "zerstören", das nur mit "fünf" Elementen übereinstimmt.

Und die folgende Abfrage:

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" }
    }}
])

Sie erhalten die besten Ergebnisse im Array, bis zu sechs Einträge:

{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }

Wie Sie hier sehen können, jede Menge Spaß.

Nachdem Sie anfänglich gruppiert haben, möchten Sie im Grunde das $first "knallen". Wert vom Stack für die Array-Ergebnisse. Um diesen Vorgang ein wenig zu vereinfachen, machen wir das tatsächlich in der Erstinbetriebnahme. Der Prozess wird also:

  • $unwind das Array
  • Vergleichen Sie die bereits gesehenen Werte mit einem $eq Gleichheitsübereinstimmung
  • $sort die Ergebnisse zu "float" false ungesehene Werte nach oben ( dadurch bleibt die Ordnung erhalten )
  • $group zurück und "knallen" Sie das $first ungesehener Wert als nächstes Mitglied auf dem Stapel. Auch dies verwendet den $cond -Operator, um "gesehene" Werte im Array-Stapel durch false zu ersetzen um bei der Auswertung zu helfen.

Die letzte Aktion mit $cond ist da, um sicherzustellen, dass zukünftige Iterationen nicht einfach den letzten Wert des Arrays immer wieder hinzufügen, wo die "Slice"-Anzahl größer ist als die Array-Mitglieder.

Dieser gesamte Vorgang muss für so viele Elemente wiederholt werden, wie Sie "slicen" möchten. Da wir bereits das "erste" Element in der anfänglichen Gruppierung gefunden haben, bedeutet dies n-1 Iterationen für das gewünschte Slice-Ergebnis.

Die letzten Schritte sind wirklich nur eine optionale Veranschaulichung, wie alles wieder in Arrays umgewandelt wird, um das Ergebnis wie gezeigt zu erhalten. Also wirklich nur bedingt Items pushen oder false durch ihre übereinstimmende Position zurück und "filtert" schließlich alle false heraus Werte, sodass die End-Arrays "sechs" bzw. "fünf" Mitglieder haben.

Es gibt also keinen Standardoperator, um dies zu berücksichtigen, und Sie können den Push nicht einfach auf 5 oder 10 oder andere Elemente im Array "begrenzen". Aber wenn Sie es wirklich tun müssen, dann ist dies Ihre beste Herangehensweise.

Sie könnten dies möglicherweise mit mapReduce angehen und das Aggregationsframework komplett aufgeben. Der Ansatz, den ich (innerhalb vernünftiger Grenzen) wählen würde, wäre, effektiv eine In-Memory-Hash-Map auf dem Server zu haben und Arrays dafür zu sammeln, während JavaScript-Slice verwendet wird, um die Ergebnisse zu "begrenzen":

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 }
    }
)

Das baut also im Grunde nur das "in-Memory"-Objekt auf, das den ausgegebenen "Schlüsseln" mit einem Array entspricht, das niemals die maximale Größe überschreitet, die Sie aus Ihren Ergebnissen abrufen möchten. Außerdem macht es sich nicht einmal die Mühe, das Item zu "emittieren", wenn der maximale Stack erreicht ist.

Der Reduce-Teil macht eigentlich nichts anderes, als im Wesentlichen nur auf "Schlüssel" und einen einzelnen Wert zu reduzieren. Für den Fall, dass unser Reducer nicht aufgerufen wurde, was der Fall wäre, wenn nur ein Wert für einen Schlüssel vorhanden wäre, kümmert sich die Finalize-Funktion darum, die "Stash"-Schlüssel der endgültigen Ausgabe zuzuordnen.

Die Effektivität hängt von der Größe der Ausgabe ab, und die JavaScript-Auswertung ist sicherlich nicht schnell, aber möglicherweise schneller als die Verarbeitung großer Arrays in einer Pipeline.

Stimmen Sie die JIRA-Probleme ab, um tatsächlich einen „Slice“-Operator oder sogar ein „Limit“ für „$push“ und „$addToSet“ zu haben, was beide praktisch wäre. Ich persönlich hoffe, dass zumindest einige Änderungen an der $map vorgenommen werden können -Operator, um den "aktuellen Index"-Wert bei der Verarbeitung verfügbar zu machen. Das würde effektiv "Slicing" und andere Operationen ermöglichen.

Wirklich möchten Sie dies codieren, um alle erforderlichen Iterationen zu "generieren". Wenn die Antwort hier genug Liebe bekommt und/oder andere Zeit ansteht, die ich in Tuits habe, dann füge ich vielleicht etwas Code hinzu, um zu demonstrieren, wie das geht. Es ist bereits eine ziemlich lange Antwort.

Code zum Generieren der 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" }
    }
}); 

Das bildet den grundlegenden iterativen Ansatz bis zu maxLen mit den Schritten von $unwind zu $group . Darin eingebettet sind auch Details der erforderlichen endgültigen Projektionen und der "verschachtelten" bedingten Anweisung. Das letzte ist im Grunde der Ansatz zu dieser Frage:

Garantiert die $in-Klausel von MongoDB Ordnung?