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

Aggregazione Mongodb $gruppo, limita la lunghezza dell'array

Moderno

Da MongoDB 3.6 esiste un approccio "romanzo" a questo utilizzando $lookup per eseguire un "auto join" più o meno allo stesso modo dell'elaborazione del cursore originale illustrata di seguito.

Poiché in questa versione puoi specificare una "pipeline" argomento in $lookup come fonte per il "join", questo significa essenzialmente che puoi usare $match e $limit per raccogliere e "limitare" le voci per l'array:

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

Puoi opzionalmente aggiungere una proiezione aggiuntiva dopo il $lookup per rendere gli elementi dell'array semplicemente i valori anziché i documenti con un _id chiave, ma il risultato di base è lì semplicemente facendo quanto sopra.

C'è ancora l'eccezionale SERVER-9277 che in realtà richiede un "limite per spingere" direttamente, ma usando $lookup in questo modo è una valida alternativa nel frattempo.

NOTA :C'è anche $slice che è stato introdotto dopo aver scritto la risposta originale e menzionato da "problema JIRA eccezionale" nel contenuto originale. Sebbene tu possa ottenere lo stesso risultato con piccoli set di risultati, implica ancora "spingere tutto" nell'array e quindi limitare l'output finale dell'array alla lunghezza desiderata.

Quindi questa è la distinzione principale e il motivo per cui generalmente non è pratico $slice per grandi risultati. Ma ovviamente può essere usato alternativamente nei casi in cui lo è.

Sono disponibili alcuni dettagli in più sui valori del gruppo mongodb in più campi sull'utilizzo alternativo.

Originale

Come affermato in precedenza, questo non è impossibile ma sicuramente un problema orribile.

In realtà, se la tua preoccupazione principale è che gli array risultanti saranno eccezionalmente grandi, l'approccio migliore è inviare per ogni "conversation_ID" distinto come una query individuale e quindi combinare i risultati. In una sintassi molto MongoDB 2.6 che potrebbe richiedere alcune modifiche a seconda di quale sia effettivamente l'implementazione del tuo linguaggio:

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

Ma tutto dipende dal fatto che sia quello che stai cercando di evitare. Quindi, alla vera risposta:

Il primo problema qui è che non esiste alcuna funzione per "limitare" il numero di elementi che vengono "spinti" in un array. È certamente qualcosa che vorremmo, ma la funzionalità al momento non esiste.

Il secondo problema è che anche quando si inseriscono tutti gli elementi in un array, non è possibile utilizzare $slice o qualsiasi operatore simile nella pipeline di aggregazione. Quindi non esiste un modo attuale per ottenere solo i "primi 10 risultati" da un array prodotto con una semplice operazione.

Ma puoi effettivamente produrre una serie di operazioni per "tagliare" efficacemente i confini del tuo raggruppamento. È abbastanza complicato e, ad esempio, qui ridurrò gli elementi dell'array "sliced" solo a "sei". Il motivo principale qui è dimostrare il processo e mostrare come farlo senza essere distruttivi con array che non contengono il totale che vuoi "tagliare".

Dato un campione di documenti:

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

Puoi vedere che quando raggruppi in base alle tue condizioni otterrai un array con dieci elementi e un altro con "cinque". Quello che vuoi fare qui riduce entrambi ai primi "sei" senza "distruggere" l'array che corrisponderà solo a "cinque" elementi.

E la seguente query:

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

Ottieni i primi risultati nell'array, fino a sei voci:

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

Come puoi vedere qui, un sacco di divertimento.

Dopo aver inizialmente raggruppato, in pratica vuoi "far scoppiare" il $first valore fuori dallo stack per i risultati dell'array. Per semplificare un po' questo processo, lo facciamo effettivamente nell'operazione iniziale. Quindi il processo diventa:

  • $unwind l'array
  • Confronta con i valori già visti con un $eq corrispondenza di uguaglianza
  • $sort i risultati "fluttuano" false valori non visti in alto (questo mantiene ancora l'ordine)
  • $group indietro di nuovo e "pop" il $first valore invisibile come membro successivo nello stack. Anche questo usa il $cond operatore per sostituire i valori "visti" nello stack dell'array con false per aiutare nella valutazione.

L'ultima azione con $cond è lì per assicurarsi che le iterazioni future non aggiungano semplicemente l'ultimo valore dell'array più e più volte dove il conteggio "slice" è maggiore dei membri dell'array.

L'intero processo deve essere ripetuto per tutti gli elementi che desideri "tagliare". Poiché abbiamo già trovato il "primo" elemento nel raggruppamento iniziale, significa n-1 iterazioni per il risultato della fetta desiderato.

I passaggi finali sono in realtà solo un'illustrazione opzionale della riconversione di tutto in array per il risultato come finalmente mostrato. Quindi, in realtà, semplicemente spingendo oggetti in modo condizionale o false indietro dalla loro posizione di corrispondenza e infine "filtrando" tutti i false valori in modo che le matrici finali abbiano rispettivamente "sei" e "cinque" membri.

Quindi non esiste un operatore standard per soddisfare questo, e non puoi semplicemente "limitare" la spinta a 5 o 10 o qualsiasi elemento nell'array. Ma se devi davvero farlo, allora questo è il tuo approccio migliore.

Potresti avvicinarti a questo con mapReduce e abbandonare il framework di aggregazione tutti insieme. L'approccio che adotterei (entro limiti ragionevoli) sarebbe quello di avere effettivamente una hash-map in memoria sul server e accumulare array su di essa, utilizzando la sezione JavaScript per "limitare" i risultati:

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

In modo che fondamentalmente costruisca l'oggetto "in-memory" che corrisponde alle "chiavi" emesse con un array che non supera mai la dimensione massima che desideri recuperare dai tuoi risultati. Inoltre, questo non si preoccupa nemmeno di "emettere" l'oggetto quando viene raggiunto lo stack massimo.

La parte di riduzione in realtà non fa altro che essenzialmente ridurre a "chiave" e un singolo valore. Quindi, nel caso in cui il nostro riduttore non venisse chiamato, come sarebbe vero se esistesse solo 1 valore per una chiave, la funzione finalize si occupa di mappare le chiavi "stash" sull'output finale.

L'efficacia di questo varia in base alla dimensione dell'output e la valutazione JavaScript non è certamente veloce, ma forse più veloce dell'elaborazione di grandi array in una pipeline.

Vota i problemi di JIRA per avere effettivamente un operatore "slice" o anche un "limite" su "$ push" e "$ addToSet", che sarebbero entrambi utili. Personalmente sperando che almeno alcune modifiche possano essere apportate alla $map operatore per esporre il valore "indice corrente" durante l'elaborazione. Ciò consentirebbe effettivamente l'"affettatura" e altre operazioni.

Davvero vorresti codificarlo per "generare" tutte le iterazioni richieste. Se la risposta qui ottiene abbastanza amore e / o altro tempo in attesa che ho in tutorial, allora potrei aggiungere del codice per dimostrare come farlo. È già una risposta abbastanza lunga.

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

Questo costruisce l'approccio iterativo di base fino a maxLen con i passaggi da $unwind a $group . Incorporati anche i dettagli delle proiezioni finali richieste e la dichiarazione condizionale "annidata". L'ultimo è sostanzialmente l'approccio adottato su questa domanda:

La clausola $in di MongoDB garantisce l'ordine?