Gruppo di aggregazione Mongodb $, limitare la lunghezza della matrice

Voglio raggruppare tutti i documenti in base a un campo, ma limitare il numero di documenti raggruppati per ciascun valore.

Ogni messaggio ha un conversation_ID. Ho bisogno di ottenere 10 o meno numeri di messaggi per ogni ID_cursa.

Sono in grado di raggruppare in base al seguente comando ma non riesco a capire come limitare il numero di documenti raggruppati oltre a tagliare i risultati Message.aggregate({'$group':{_id:'$conversation_ID',msgs:{'$push':{msgid:'$_id'}}}})

Come limitare la lunghezza della matrice msgs per ogni conversation_ID a 10?

Moderno

Da MongoDB 3.6 c’è un approccio “nuovo” a questo usando $lookup per eseguire un “self join” più o meno allo stesso modo dell’elaborazione del cursore originale mostrata di seguito.

Poiché in questa versione è ansible specificare un argomento "pipeline" a $lookup come origine per “join”, ciò significa essenzialmente che è ansible utilizzare $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" }} ]) 

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

Esiste ancora l’eccezionale SERVER-9277 che richiede direttamente un “limite di push”, ma l’utilizzo di $lookup in questo modo è un’alternativa valida nel frattempo.

NOTA : Esiste anche una $slice che è stata introdotta dopo aver scritto la risposta originale e menzionata da “eccezionale problema JIRA” nel contenuto originale. Mentre è ansible ottenere lo stesso risultato con insiemi di risultati piccoli, esso implica ancora “spingere tutto” nell’array e quindi limitare l’output finale dell’array alla lunghezza desiderata.

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

Ci sono alcuni altri dettagli sui valori del gruppo mongodb in base a più campi sull’utilizzo alternativo.


Originale

Come affermato in precedenza, questo non è imansible ma certamente un problema orribile.

In realtà se la tua preoccupazione principale è che gli array risultanti saranno eccezionalmente grandi, allora l’approccio migliore è di presentare per ciascun “ID_condivisione” distinto come una singola query e quindi combinare i risultati. Nella syntax molto MongoDB 2.6 che potrebbe richiedere qualche ritouch a seconda di quale sia effettivamente l’implementazione della lingua:

 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 una funzione per “limitare” il numero di elementi che vengono “spinti” in una matrice. È certamente qualcosa che vorremmo, ma attualmente non esiste la funzionalità.

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

Ma puoi effettivamente produrre una serie di operazioni per “affettare” efficacemente i tuoi confini di raggruppamento. È abbastanza coinvolto, e per esempio qui ridurrò gli elementi dell’array “tagliati” solo a “sei”. Il motivo principale qui è quello di dimostrare il processo e mostrare come farlo senza essere distruttivo con gli array che non contengono il totale a cui si desidera “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 } 

Potete vedere lì che quando si raggruppano le condizioni si otterrà un array con dieci elementi e un altro con “cinque”. Quello che vuoi fare qui è ridurre entrambi i “sei” in alto senza “distruggere” la matrice che corrisponderà solo a “cinque” elementi.

E la seguente domanda:

 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 risultati migliori 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 saltare” il $first valore $first dello stack per i risultati dell’array. Per semplificare un po ‘questo processo, lo facciamo effettivamente nell’operazione iniziale. Quindi il processo diventa:

  • $unwind la matrice
  • Confronta con i valori già visti con una partita di parità di $eq
  • $sort i risultati per “rendere mobili” i valori non visualizzati false in cima (questo mantiene ancora l’ordine)
  • $group nuovamente e “pop” il $first valore mai visto $first come membro successivo nello stack. Inoltre, utilizza l’operatore $cond per sostituire i valori “visti” nello stack dell’array con false per aiutare nella valutazione.

L’azione finale con $cond è lì per assicurarsi che le future iterazioni non stiano semplicemente aggiungendo l’ultimo valore dell’array all’infinito 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 la “prima” voce nel raggruppamento iniziale, ciò significa n-1 iterazioni per il risultato di sezione desiderato.

I passaggi finali sono in realtà solo un’illustrazione facoltativa della conversione di tutto in array per il risultato, come mostrato alla fine. Quindi, in realtà, spingere condizionatamente gli oggetti o il false indietro dalla posizione corrispondente e infine “filtrare” tutti i false valori, così gli array finali hanno rispettivamente “sei” e “cinque” membri.

Quindi non c’è un operatore standard per sistemare questo, e non si può semplicemente “limitare” il push a 5 o 10 o qualsiasi elemento nell’array. Ma se davvero devi farlo, allora questo è il tuo approccio migliore.


È ansible avvicinarsi a questo con mapReduce e abbandonare il framework di aggregazione tutti insieme. L’approccio che prenderei (entro limiti ragionevoli) sarebbe quello di avere effettivamente una mappa hash in memoria sul server e accumulare array per quello, mentre si usa la slice 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 } } ) 

Quindi, in pratica, si costruisce semplicemente l'object "in memoria" che corrisponde alle "chiavi" emesse con una matrice che non supera mai la dimensione massima che si desidera recuperare dai risultati. Inoltre questo non si preoccupa nemmeno di "emettere" l'object quando viene raggiunto lo stack massimo.

La parte di riduzione in realtà non fa altro che essenzialmente solo 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 un tasto, la funzione finalizzazione si occuperà di mappare i tasti "stash" all'output finale.

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


Valutare i problemi JIRA per avere effettivamente un operatore "fetta" o anche un "limite" su "$ push" e "$ addToSet", che sarebbero entrambi a portata di mano. Personalmente sperando che almeno qualche modifica possa essere apportata all'operatore $map per esporre il valore "index corrente" durante l'elaborazione. Ciò consentirebbe effettivamente "slicing" e altre operazioni.

Veramente vorresti codificarlo per "generare" tutte le iterazioni richieste. Se la risposta qui ottiene abbastanza amore e / o altro tempo in sospeso che ho in tuits, 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 . Inoltre sono inclusi i dettagli delle proiezioni finali richieste e l'istruzione condizionale "annidata". L'ultimo è sostanzialmente l'approccio adottato su questa domanda:

L'ordine di garanzia di $ in clausola MongoDB?

L’operatore $ slice non è un operatore di aggregazione, quindi non puoi farlo (come ho suggerito in questa risposta, prima della modifica):

 db.messages.aggregate([ { $group : {_id:'$conversation_ID',msgs: { $push: { msgid:'$_id' }}}}, { $project : { _id : 1, msgs : { $slice : 10 }}}]); 

La risposta di Neil è molto dettagliata, ma puoi usare un approccio leggermente diverso (se adatto al tuo caso d’uso). È ansible aggregare i risultati e inviarli a una nuova raccolta:

 db.messages.aggregate([ { $group : {_id:'$conversation_ID',msgs: { $push: { msgid:'$_id' }}}}, { $out : "msgs_agg" } ]); 

L’operatore $ out scriverà i risultati dell’aggregazione in una nuova raccolta. È quindi ansible utilizzare una query di ricerca normale per proiettare i risultati con l’operatore $ slice:

 db.msgs_agg.find({}, { msgs : { $slice : 10 }}); 

Per questo test documenti:

 > db.messages.find().pretty(); { "_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" : 7, "conversation_ID" : 1234 } { "_id" : 8, "conversation_ID" : 1234 } { "_id" : 9, "conversation_ID" : 1234 } 

Il risultato sarà:

 > db.msgs_agg.find({}, { msgs : { $slice : 10 }}); { "_id" : 1234, "msgs" : [ { "msgid" : 7 }, { "msgid" : 8 }, { "msgid" : 9 } ] } { "_id" : 123, "msgs" : [ { "msgid" : 1 }, { "msgid" : 2 }, { "msgid" : 3 }, { "msgid" : 4 }, { "msgid" : 5 } ] } 

modificare

Presumo che ciò significherebbe duplicare l’intera collezione di messaggi. Non è eccessivo?

Bene, ovviamente questo approccio non si ridimensionerà con enormi collezioni. Tuttavia, dal momento che stai pensando di utilizzare grandi pipeline di aggregazione o grandi lavori di riduzione delle mappe, probabilmente non lo utilizzerai per le richieste “in tempo reale”.

Ci sono molti svantaggi di questo approccio: limite di 16 MB BSON se si stanno creando enormi documenti con aggregazione, spreco di spazio su disco / memoria con duplicazione, aumento del disco IO …

I pro di questo approccio: è semplice da implementare e quindi facile da modificare. Se la tua raccolta è raramente aggiornata puoi usare questa raccolta “fuori” come una cache. In questo modo non dovresti eseguire l’operazione di aggregazione più volte e potresti anche supportare richieste client “in tempo reale” sulla raccolta “out”. Per aggiornare i dati, è ansible eseguire periodicamente l’aggregazione (ad esempio in un processo in background che viene eseguito ogni notte).

Come è stato detto nei commenti questo non è un problema facile e non c’è una soluzione perfetta per questo (ancora!). Ti ho mostrato un altro approccio che puoi utilizzare, sta a te decidere e decidere cosa è più appropriato per il tuo caso d’uso.