sql >> Databasteknik >  >> NoSQL >> MongoDB

Mongodb aggregering $grupp, begränsa längden på arrayen

Modern

Från MongoDB 3.6 finns det en "ny" metod för detta genom att använda $lookup att utföra en "self join" på ungefär samma sätt som den ursprungliga markörbearbetningen som visas nedan.

Eftersom du i den här utgåvan kan ange en "pipeline" argument till $lookup som en källa för "join", betyder detta i huvudsak att du kan använda $match och $limit för att samla in och "begränsa" posterna för arrayen:

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 valfritt lägga till ytterligare projektion efter $lookup för att göra arrayobjekten helt enkelt till värdena snarare än dokument med en _id nyckel, men det grundläggande resultatet är där genom att helt enkelt göra ovanstående.

Det finns fortfarande den utestående SERVER-9277 som faktiskt begär en "limit to push" direkt, men med $lookup på detta sätt är ett gångbart alternativ under tiden.

OBS :Det finns också $slice som introducerades efter att ha skrivit det ursprungliga svaret och nämnts av "outstanding JIRA issue" i originalinnehållet. Även om du kan få samma resultat med små resultatuppsättningar, innebär det att du fortfarande "skjuter in allt" i arrayen och sedan begränsar den slutliga arrayutgången till önskad längd.

Så det är den huvudsakliga skillnaden och varför det i allmänhet inte är praktiskt att $slice för stora resultat. Men kan givetvis användas omväxlande i de fall det är det.

Det finns några fler detaljer om mongodb-gruppvärden i flera fält om endera alternativ användning.

Original

Som nämnts tidigare är detta inte omöjligt men definitivt ett fruktansvärt problem.

Om ditt största bekymmer faktiskt är att dina resulterande arrayer kommer att bli exceptionellt stora, är det bästa sättet att skicka in för varje distinkt "conversation_ID" som en individuell fråga och sedan kombinera dina resultat. I mycket MongoDB 2.6-syntax som kan behöva finjusteras beroende på vad din språkimplementering faktiskt är:

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 beror helt på om det är det du försöker undvika. Så till det verkliga svaret:

Det första problemet här är att det inte finns någon funktion för att "begränsa" antalet objekt som "skjuts" in i en array. Det är verkligen något vi skulle vilja, men funktionen finns inte för närvarande.

Det andra problemet är att du inte kan använda $slice även när du trycker in alla objekt i en array , eller någon liknande operatör i aggregeringspipelinen. Så det finns inget nuvarande sätt att få bara "topp 10"-resultaten från en producerad array med en enkel operation.

Men du kan faktiskt producera en uppsättning operationer för att effektivt "skiva" på dina grupperingsgränser. Det är ganska involverat, och till exempel här kommer jag att reducera arrayelementen "skivade" till endast "sex". Det främsta skälet här är att demonstrera processen och visa hur man gör detta utan att vara destruktiv med arrayer som inte innehåller den totala summan du vill "skiva" till.

Med ett exempel på dokument:

{ "_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 där att när du grupperar efter dina förutsättningar får du en array med tio element och en annan med "fem". Vad du vill göra här reducera båda till de översta "sex" utan att "förstöra" arrayen som bara matchar "fem" element.

Och följande fråga:

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 bästa resultaten i arrayen, upp till sex poster:

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

Som du kan se här, massor av kul.

Efter att du initialt har grupperat vill du i princip "poppa" $first värde utanför stacken för arrayresultaten. För att göra denna process lite förenklad gör vi faktiskt detta i den inledande operationen. Så processen blir:

  • $unwind arrayen
  • Jämför med de värden som redan har setts med en $eq jämställdhetsmatch
  • $sort resultaten att "flyta" false osynliga värden till toppen (detta behåller fortfarande ordningen)
  • $group tillbaka igen och "poppa" $first osynligt värde som nästa medlem i stacken. Även detta använder $cond operatorn för att ersätta "sedda" värden i arraystacken med false för att hjälpa till i utvärderingen.

Den sista åtgärden med $cond finns det för att se till att framtida iterationer inte bara lägger till det sista värdet av arrayen om och om igen där "slice"-antalet är större än arraymedlemmarna.

Hela den processen måste upprepas för så många föremål som du vill "skiva". Eftersom vi redan hittat det "första" objektet i den initiala grupperingen betyder det n-1 iterationer för det önskade skivresultatet.

De sista stegen är egentligen bara en valfri illustration av att konvertera allt tillbaka till arrayer för resultatet som slutligen visas. Så egentligen bara att villkorligt pusha objekt eller false tillbaka genom sin matchande position och slutligen "filtrera" bort alla false värden så att ändmatriserna har "sex" respektive "fem" medlemmar.

Så det finns ingen standardoperatör för detta, och du kan inte bara "begränsa" pushen till 5 eller 10 eller vilka objekt som helst i arrayen. Men om du verkligen måste göra det, så är detta ditt bästa tillvägagångssätt.

Du kan möjligen närma dig detta med mapReduce och överge aggregeringsramverket tillsammans. Tillvägagångssättet jag skulle ta (inom rimliga gränser) skulle vara att effektivt ha en hash-karta i minnet på servern och samla arrayer till den, samtidigt som jag använder JavaScript-segment för att "begränsa" resultaten:

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 bygger bara i princip upp "in-memory"-objektet som matchar de utsända "nycklarna" med en array som aldrig överskrider den maximala storleken du vill hämta från dina resultat. Dessutom bryr det sig inte ens om att "sända ut" objektet när den maximala stapeln är uppfylld.

Reduceringsdelen gör faktiskt inget annat än att i huvudsak bara reducera till "nyckel" och ett enda värde. Så ifall vår reducerare inte skulle bli anropad, vilket skulle vara sant om det bara fanns ett värde för en nyckel, tar finaliseringsfunktionen hand om att mappa "stash"-nycklarna till den slutliga utgången.

Effektiviteten av detta varierar beroende på storleken på utdata, och JavaScript-utvärdering är verkligen inte snabb, men möjligen snabbare än att bearbeta stora arrayer i en pipeline.

Rösta upp JIRA-frågorna för att faktiskt ha en "slice"-operator eller till och med en "limit" på "$push" och "$addToSet", vilket båda skulle vara praktiskt. Jag hoppas personligen att åtminstone vissa ändringar kan göras i $map operatör för att exponera det "aktuella index"-värdet vid bearbetning. Det skulle effektivt tillåta "slicing" och andra operationer.

Du skulle verkligen vilja koda detta för att "generera" alla nödvändiga iterationer. Om svaret här får tillräckligt med kärlek och/eller annan tid i väntan som jag har i tuits, så kan jag lägga till lite kod för att visa hur man gör detta. Det är redan ett lagom långt svar.

Kod för att generera 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äggande iterativa metoden upp till maxLen med stegen från $unwind till $group . Också inbäddade i det finns detaljer om de slutliga projektionerna som krävs och det "kapslade" villkorliga uttalandet. Den sista är i grunden det tillvägagångssätt som använts i denna fråga:

Garanterar MongoDB:s $in-klausul?



  1. Jag försöker köra mongod server på ubuntu :undantag i initAndListen:29 Datakatalogen /data/db hittades inte., avslutas

  2. Array-delmängd i pipeline för aggregeringsramverk

  3. MongoDB vs. Redis vs. Cassandra för en snabbskrivande, temporär radlagringslösning

  4. Okänslig sökning i Mongo