sql >> Databáze >  >> NoSQL >> MongoDB

Agregace Mongodb $group, omezte délku pole

Moderní

Od MongoDB 3.6 je k tomu "nový" přístup pomocí $lookup k provedení "vlastního připojení" v podstatě stejným způsobem jako původní zpracování kurzoru ukázané níže.

Protože v této verzi můžete zadat "pipeline" argument pro $lookup jako zdroj pro "join" to v podstatě znamená, že můžete použít $match a $limit shromáždit a "omezit" položky pro pole:

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

Volitelně můžete přidat další projekci za $lookup aby se z položek pole staly spíše hodnoty než dokumenty s _id klíč, ale základní výsledek je tam jednoduchým provedením výše uvedeného.

Stále existuje nevyřízený SERVER-9277, který ve skutečnosti požaduje „limit pro push“ přímo, ale pomocí $lookup tímto způsobem je v mezidobí životaschopnou alternativou.

POZNÁMKA :Existuje také $slice který byl představen po napsání původní odpovědi a zmíněn „výjimečným problémem JIRA“ v původním obsahu. I když můžete získat stejný výsledek s malými sadami výsledků, znamená to stále "vtlačit všechno" do pole a později omezit konečný výstup pole na požadovanou délku.

To je hlavní rozdíl a proč není obecně praktické $slice pro velké výsledky. Ale samozřejmě může být střídavě použit v případech, kdy je.

Existuje několik dalších podrobností o hodnotách skupiny mongodb v několika polích o alternativním použití.

Původní

Jak bylo uvedeno dříve, není to nemožné, ale rozhodně je to hrozný problém.

Ve skutečnosti, pokud je vaším hlavním zájmem, že vaše výsledná pole budou výjimečně velká, pak nejlepším přístupem je odeslat pro každé odlišné "conversation_ID" jako samostatný dotaz a poté zkombinovat své výsledky. Ve velmi syntaxi MongoDB 2.6, která může vyžadovat určité úpravy v závislosti na tom, jaká je ve skutečnosti implementace vašeho jazyka:

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

Ale vše záleží na tom, zda se tomu snažíte vyhnout. Takže ke skutečné odpovědi:

Prvním problémem je, že neexistuje žádná funkce, která by „omezila“ počet položek, které jsou „tlačeny“ do pole. Je to určitě něco, co bychom chtěli, ale funkce v současné době neexistuje.

Druhým problémem je, že i když vkládáte všechny položky do pole, nemůžete použít $slice nebo jakýkoli podobný operátor v agregačním kanálu. Neexistuje tedy žádný současný způsob, jak získat pouze „10 nejlepších“ výsledků z vytvořeného pole pomocí jednoduché operace.

Ve skutečnosti však můžete vytvořit sadu operací, které efektivně „rozkrájí“ hranice seskupení. Je to spravedlivě zapojeno a například zde zredukuji prvky pole „nakrájené“ pouze na „šest“. Hlavním důvodem je zde demonstrovat proces a ukázat, jak to udělat, aniž by to bylo destruktivní s poli, která neobsahují součet, na který chcete "porcovat".

Uvedený vzorek 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 }

Vidíte tam, že při seskupení podle vašich podmínek dostanete jedno pole s deseti prvky a další s „pětkou“. Co zde chcete udělat, zredukujte oba na horních "šest", aniž byste "zničili" pole, které bude odpovídat pouze "pěti" prvkům.

A následující dotaz:

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

Získáte nejlepší výsledky v poli, až šest položek:

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

Jak můžete vidět zde, spousta zábavy.

Poté, co jste původně seskupili, chcete v podstatě "vyskočit" $first hodnota mimo zásobník pro výsledky pole. Abychom tento proces trochu zjednodušili, provádíme to ve skutečnosti v počáteční operaci. Proces se tedy stává:

  • $unwind pole
  • Porovnejte s již zaznamenanými hodnotami s $eq rovnoprávnost
  • $sort výsledky "float" false neviditelné hodnoty nahoru (toto stále zachovává pořadí)
  • $group zpět a "vyskočí" $first neviditelná hodnota jako další člen v zásobníku. Toto také používá $cond operátor, který nahradí "viděné" hodnoty v zásobníku pole false pomoci při hodnocení.

Poslední akce s $cond je zde, aby se zajistilo, že budoucí iterace nebudou pouze přidávat poslední hodnotu pole znovu a znovu tam, kde je počet "slice" větší než počet členů pole.

Celý tento proces je třeba opakovat pro tolik položek, kolik chcete „nakrájet“. Protože jsme již našli "první" položku v počátečním seskupení, znamená to n-1 iterací pro požadovaný výsledek řezu.

Poslední kroky jsou ve skutečnosti jen volitelnou ilustrací převodu všeho zpět na pole pro výsledek, jak je nakonec ukázáno. Takže opravdu jen podmíněné tlačení položek nebo false zpět podle jejich odpovídající pozice a nakonec "odfiltrování" všech false hodnoty, takže koncová pole mají „šest“ a „pět“ členů.

Neexistuje tedy žádný standardní operátor, který by se tomu přizpůsobil, a nemůžete jen „omezit“ push na 5 nebo 10 nebo jakékoli jiné položky v poli. Ale pokud to opravdu musíte udělat, pak je toto váš nejlepší přístup.

Možná byste k tomu mohli přistoupit pomocí mapReduce a společně opustit agregační rámec. Přístup, který bych zvolil (v rozumných mezích), by byl efektivně mít na serveru hash-mapu v paměti a shromažďovat k ní pole, zatímco bych k „omezení“ výsledků používal JavaScript:

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

Takže to v podstatě vytvoří "in-memory" objekt odpovídající emitovaným "klíčům" s polem, které nikdy nepřekročí maximální velikost, kterou chcete načíst z vašich výsledků. Navíc se to ani neobtěžuje "vysílat" položku, když je splněn maximální stack.

Část zmenšení ve skutečnosti nedělá nic jiného, ​​než že v podstatě jen sníží na "klíč" a jedinou hodnotu. Takže pro případ, že by náš reduktor nebyl zavolán, což by platilo, kdyby pro klíč existovala pouze 1 hodnota, funkce finalizace se postará o mapování "stash" klíčů na konečný výstup.

Efektivita tohoto se liší podle velikosti výstupu a vyhodnocení JavaScriptu rozhodně není rychlé, ale možná rychlejší než zpracování velkých polí v potrubí.

Hlasujte pro problémy JIRA, aby skutečně měl operátor „slice“ nebo dokonce „limit“ na „$push“ a „$addToSet“, což by bylo obojí užitečné. Osobně doufám, že lze provést alespoň nějaké úpravy na $map operátora k odhalení hodnoty "aktuálního indexu" při zpracování. To by efektivně umožnilo „krájení“ a další operace.

Opravdu byste to chtěli kódovat, abyste „vygenerovali“ všechny požadované iterace. Pokud odpověď zde dostane dostatek lásky a/nebo jiného času, který mám ve výuce, pak bych mohl přidat nějaký kód, abych demonstroval, jak to udělat. Je to již přiměřeně dlouhá odpověď.

Kód pro generování kanálu:

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

To staví základní iterační přístup až po maxLen pomocí kroků z $unwind do $group . Jsou zde také začleněny podrobnosti o požadovaných konečných projekcích a "vnořený" podmíněný příkaz. Poslední je v podstatě přístup k této otázce:

Zaručuje MongoDB klauzule $in řád?



  1. Rozdíl mezi replikací Redis a redis sharding (klastr).

  2. Zkombinujte dva dotazy NEBO s AND v Mongoose

  3. Meteor vrací chybu neplatného hexadecimálního řetězce při pokusu o vytvoření ObjectID?

  4. Robustní serializace zpráv v Apache Kafka pomocí Apache Avro, část 1