sql >> Database teknologi >  >> NoSQL >> MongoDB

Mongodb aggregering $gruppe, begræns længden af ​​array

Moderne

Fra MongoDB 3.6 er der en "ny" tilgang til dette ved at bruge $lookup at udføre en "selv joinforbindelse" på nogenlunde samme måde som den originale markørbehandling vist nedenfor.

Da du i denne udgivelse kan angive en "pipeline" argument til $lookup som en kilde til "join", betyder dette i bund og grund, at du kan bruge $match og $limit for at samle og "begrænse" indgange for arrayet:

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

Du kan eventuelt tilføje yderligere projektion efter $lookup for at gøre array-elementerne til blot værdierne i stedet for dokumenter med en _id nøgle, men det grundlæggende resultat er der ved blot at gøre ovenstående.

Der er stadig den udestående SERVER-9277, som faktisk anmoder om en "grænse til at skubbe" direkte, men ved hjælp af $lookup på denne måde er et levedygtigt alternativ i mellemtiden.

BEMÆRK :Der er også $slice som blev introduceret efter at have skrevet det originale svar og nævnt ved "udestående JIRA-problem" i det originale indhold. Selvom du kan få det samme resultat med små resultatsæt, involverer det stadig at "skubbe alt" ind i arrayet og derefter begrænse det endelige array-output til den ønskede længde.

Så det er den vigtigste forskel, og hvorfor det generelt ikke er praktisk at $slice for store resultater. Men kan selvfølgelig skiftevis bruges i de tilfælde, hvor det er.

Der er et par flere detaljer om mongodb-gruppeværdier ved flere felter om enten alternativ brug.

Original

Som tidligere nævnt er dette ikke umuligt, men bestemt et forfærdeligt problem.

Faktisk, hvis din største bekymring er, at dine resulterende arrays bliver usædvanligt store, så er din bedste tilgang at indsende for hver særskilt "samtale_ID" som en individuel forespørgsel og derefter kombinere dine resultater. I meget MongoDB 2.6-syntaks, som måske trænger til nogle justeringer afhængigt af, hvad din sprogimplementering faktisk er:

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

Men det hele afhænger af, om det er det, du forsøger at undgå. Så videre til det rigtige svar:

Det første problem her er, at der ikke er nogen funktion til at "begrænse" antallet af elementer, der "skubbes" ind i et array. Det er bestemt noget, vi gerne vil, men funktionaliteten eksisterer ikke i øjeblikket.

Det andet problem er, at selv når du skubber alle elementer ind i et array, kan du ikke bruge $slice , eller en lignende operatør i aggregeringspipelinen. Så der er ingen aktuel måde at få kun "top 10" resultater fra et produceret array med en simpel handling.

Men du kan faktisk producere et sæt operationer for effektivt at "skære" på dine grupperingsgrænser. Det er ret involveret, og for eksempel her vil jeg reducere array-elementerne "sliced" til kun "seks". Hovedårsagen her er at demonstrere processen og vise, hvordan man gør dette uden at være destruktiv med arrays, der ikke indeholder den total, du vil "slice" til.

Givet et eksempel på dokumenter:

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

Du kan se der, at når du grupperer efter dine betingelser, vil du få et array med ti elementer og et andet med "fem". Hvad du vil gøre her, reducerer begge til de øverste "seks" uden at "ødelægge" arrayet, der kun vil matche til "fem" elementer.

Og følgende forespørgsel:

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

Du får de bedste resultater i arrayet, op til seks poster:

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

Som du kan se her, masser af sjov.

Når du først har grupperet, ønsker du grundlæggende at "poppe" $first værdi ud af stakken for matrixresultaterne. For at gøre denne proces lidt forenklet, gør vi faktisk dette i den indledende operation. Så processen bliver:

  • $unwind arrayet
  • Sammenlign med de værdier, der allerede er set med en $eq ligestillingsmatch
  • $sort resultaterne til at "flyde" false usete værdier til toppen (dette bevarer stadig rækkefølgen)
  • $group tilbage igen og "pop" $first uset værdi som det næste medlem på stakken. Dette bruger også $cond operator til at erstatte "sete" værdier i matrixstakken med false for at hjælpe med evalueringen.

Den sidste handling med $cond er der for at sikre, at fremtidige iterationer ikke blot tilføjer den sidste værdi af arrayet igen og igen, hvor "slice"-antallet er større end array-medlemmerne.

Hele denne proces skal gentages for så mange emner, som du ønsker at "slice". Da vi allerede har fundet det "første" element i den indledende gruppering, betyder det n-1 iterationer for det ønskede udsnitsresultat.

De sidste trin er egentlig kun en valgfri illustration af at konvertere alt tilbage til arrays for resultatet som endeligt vist. Så egentlig bare betinget skubbe elementer eller false tilbage ved deres matchende position og til sidst "filtrere" alle de false fra værdier, så ende-arrays har henholdsvis "seks" og "fem" medlemmer.

Så der er ikke en standard operatør til at imødekomme dette, og du kan ikke bare "begrænse" pushet til 5 eller 10 eller hvilke som helst elementer i arrayet. Men hvis du virkelig skal gøre det, så er dette din bedste tilgang.

Du kunne muligvis gribe dette an med mapReduce og forsage aggregeringsrammen alle sammen. Den tilgang, jeg ville tage (inden for rimelige grænser), ville være effektivt at have et hash-kort i hukommelsen på serveren og akkumulere arrays til det, mens jeg bruger JavaScript-udsnit til at "begrænse" resultaterne:

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

Så det opbygger bare "in-memory"-objektet, der matcher de udsendte "nøgler" med et array, der aldrig overstiger den maksimale størrelse, du ønsker at hente fra dine resultater. Derudover generer dette ikke engang at "udsende" emnet, når den maksimale stak er nået.

Reduceringsdelen gør faktisk ikke andet end i det væsentlige blot at reducere til "nøgle" og en enkelt værdi. Så bare i tilfælde af, at vores reducer ikke blev kaldt, som det ville være sandt, hvis der kun eksisterede 1 værdi for en nøgle, sørger færdiggørelsesfunktionen for at kortlægge "stash"-tasterne til det endelige output.

Effektiviteten af ​​dette varierer med størrelsen af ​​outputtet, og JavaScript-evaluering er bestemt ikke hurtig, men muligvis hurtigere end at behandle store arrays i en pipeline.

Stem op for JIRA-problemerne for faktisk at have en "slice"-operator eller endda en "grænse" på "$push" og "$addToSet", hvilket begge ville være praktisk. Håber personligt, at der i det mindste kan foretages nogle ændringer af $map operatør for at afsløre "aktuelt indeks"-værdi ved behandling. Det ville effektivt tillade "slicing" og andre operationer.

Du vil virkelig gerne kode dette op for at "generere" alle de nødvendige iterationer. Hvis svaret her får nok kærlighed og/eller anden ventetid, som jeg har i tuits, så kan jeg tilføje noget kode for at demonstrere, hvordan man gør dette. Det er allerede et rimeligt langt svar.

Kode til at generere 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" }
    }
}); 

Det bygger den grundlæggende iterative tilgang op til maxLen med trinene fra $unwind til $group . Der er også indlejret detaljer om de endelige fremskrivninger, der kræves og den "indlejrede" betingede erklæring. Den sidste er grundlæggende tilgangen til dette spørgsmål:

Garanterer MongoDB's $in-klausul?



  1. MongoDB $dateToString Format Specifiers

  2. Brugerdefinerede fejlmeddelelser med Mongoose

  3. Google Cloud Mongo DB:Ekstern IP forbinder ikke

  4. Sådan sletter du nøgler, der matcher et mønster i Redis Cluster