Ricerca di aggregati $ La dimensione totale dei documenti nella pipeline corrispondente supera la dimensione massima del documento

Ho una query di aggregazione di $lookup piuttosto semplice come la seguente:

 {'$lookup': {'from': 'edge', 'localField': 'gid', 'foreignField': 'to', 'as': 'from'}} 

Quando lo eseguo su una corrispondenza con abbastanza documenti ottengo il seguente errore:

 Command failed with error 4568: 'Total size of documents in edge matching { $match: { $and: [ { from: { $eq: "geneDatabase:hugo" } }, {} ] } } exceeds maximum document size' on server 

Tutti i tentativi di limitare il numero di documenti non riescono. allowDiskUse: true non fa nulla. L’invio di un cursor non fa nulla. Anche l’aggiunta di un $limit all’aggregazione non riesce.

Come può essere?

Quindi vedo di nuovo l’errore. Da dove viene quella $match e $and e $eq ? La pipeline di aggregazione dietro le quinte genera la chiamata $lookup a un’altra aggregazione, una esegue da sola che non ho la capacità di fornire limiti o utilizzare cursori con ??

Che cosa sta succedendo qui?

Come affermato in precedenza nel commento, l’errore si verifica perché quando si esegue la $lookup che per impostazione predefinita genera un “array” di destinazione all’interno del documento principale dai risultati della raccolta esterna, la dimensione totale dei documenti selezionati per quell’array provoca il superamento del genitore il limite di 16 milioni di BSON.

Il contatore per questo è quello di elaborare con un $unwind che segue immediatamente la fase di pipeline di $lookup . Questo in realtà altera il comportamento di $lookup in modo tale che invece di produrre un array nel genitore, i risultati sono invece una “copia” di ciascun genitore per ogni documento corrispondente.

Praticamente, proprio come l’uso regolare di $unwind , con l’eccezione che invece di elaborare come una fase “separata” della pipeline, l’azione di unwinding viene effettivamente aggiunta all’operazione $lookup pipeline di $lookup stessa. Idealmente, segui anche il $unwind con una condizione $match , che crea anche un argomento matching da aggiungere alla $lookup . Puoi vederlo effettivamente nell’output di explain per la pipeline.

L’argomento è in realtà trattato (brevemente) in una sezione di Ottimizzazione della pipeline di aggregazione nella documentazione di base:

$ lookup + $ unwind Coalescence

Novità nella versione 3.2.

Quando un $ unwind segue immediatamente un’altra ricerca $, e $ unwind opera nel campo as della $ lookup, l’ottimizzatore può fondere il $ unwind nella fase di ricerca $. Ciò evita la creazione di documenti intermedi di grandi dimensioni.

Migliore dimostrazione con un elenco che mette il server sotto stress creando documenti “correlati” che supererebbero il limite di 16 milioni di BSON. Fatto il più brevemente ansible per rompere e aggirare il limite BSON:

 const MongoClient = require('mongodb').MongoClient; const uri = 'mongodb://localhost/test'; function data(data) { console.log(JSON.stringify(data, undefined, 2)) } (async function() { let db; try { db = await MongoClient.connect(uri); console.log('Cleaning....'); // Clean data await Promise.all( ["source","edge"].map(c => db.collection(c).remove() ) ); console.log('Inserting...') await db.collection('edge').insertMany( Array(1000).fill(1).map((e,i) => ({ _id: i+1, gid: 1 })) ); await db.collection('source').insert({ _id: 1 }) console.log('Fattening up....'); await db.collection('edge').updateMany( {}, { $set: { data: "x".repeat(100000) } } ); // The full pipeline. Failing test uses only the $lookup stage let pipeline = [ { $lookup: { from: 'edge', localField: '_id', foreignField: 'gid', as: 'results' }}, { $unwind: '$results' }, { $match: { 'results._id': { $gte: 1, $lte: 5 } } }, { $project: { 'results.data': 0 } }, { $group: { _id: '$_id', results: { $push: '$results' } } } ]; // List and iterate each test case let tests = [ 'Failing.. Size exceeded...', 'Working.. Applied $unwind...', 'Explain output...' ]; for (let [idx, test] of Object.entries(tests)) { console.log(test); try { let currpipe = (( +idx === 0 ) ? pipeline.slice(0,1) : pipeline), options = (( +idx === tests.length-1 ) ? { explain: true } : {}); await new Promise((end,error) => { let cursor = db.collection('source').aggregate(currpipe,options); for ( let [key, value] of Object.entries({ error, end, data }) ) cursor.on(key,value); }); } catch(e) { console.error(e); } } } catch(e) { console.error(e); } finally { db.close(); } })(); 

Dopo aver inserito alcuni dati iniziali, l’elenco tenterà di eseguire un aggregato costituito semplicemente da $lookup che fallirà con il seguente errore:

{MongoError: dimensione totale dei documenti nella pipeline di corrispondenza degli spigoli {$ match: {$ e: [{gid: {$ eq: 1}}, {}]}} supera la dimensione massima del documento

Il che sostanzialmente ti dice che il limite BSON è stato superato al momento del recupero.

Al contrario, il prossimo tentativo aggiunge gli stadi $unwind e $match pipeline

L’output di Explain :

  { "$lookup": { "from": "edge", "as": "results", "localField": "_id", "foreignField": "gid", "unwinding": { // $unwind now is unwinding "preserveNullAndEmptyArrays": false }, "matching": { // $match now is matching "$and": [ // and actually executed against { // the foreign collection "_id": { "$gte": 1 } }, { "_id": { "$lte": 5 } } ] } } }, // $unwind and $match stages removed { "$project": { "results": { "data": false } } }, { "$group": { "_id": "$_id", "results": { "$push": "$results" } } } 

E questo risultato, naturalmente, ha esito positivo, poiché poiché i risultati non vengono più inseriti nel documento principale, il limite BSON non può essere superato.

Questo accade solo in seguito all’aggiunta di $unwind only, ma la $match viene aggiunta per esempio per mostrare che questo viene aggiunto anche nella fase di $lookup e che l’effetto complessivo è di “limitare” i risultati restituiti in modo efficace , poiché è tutto fatto in $lookup e nessun altro risultato diverso da quelli corrispondenti viene effettivamente restituito.

Costruendo in questo modo è ansible eseguire una ricerca per “dati di riferimento” che superino il limite BSON e quindi se si desidera $group i risultati vengano $group in un formato array, una volta che sono stati effettivamente filtrati dalla “query nascosta” che è effettivamente eseguito da $lookup .


MongoDB 3.6 e versioni successive – Ulteriori informazioni per “SINISTRA UNISCI”

Come tutti i contenuti di cui sopra, il limite BSON è un limite “difficile” che non è ansible violare e questo è generalmente il motivo per cui il $unwind è necessario come passaggio provvisorio. Esiste tuttavia la limitazione che “LEFT JOIN” diventa un “INNER JOIN” in virtù del $unwind cui non è ansible preservare il contenuto. Persino anche preserveNulAndEmptyArrays nega la “coalescenza” e lascia comunque l’array intatto, causando lo stesso problema del limite BSON.

MongoDB 3.6 aggiunge una nuova syntax alla $lookup che consente di utilizzare un’espressione “sub-pipeline” al posto delle chiavi “local” e “foreign”. Quindi, invece di usare l’opzione “coalescenza” come dimostrato, finché l’array prodotto non viola anche il limite è ansible mettere le condizioni in quella pipeline che restituisce l’array “intatto”, e possibilmente senza corrispondenze come sarebbe indicativo di un “LEFT JOIN”.

La nuova espressione sarebbe quindi:

 { "$lookup": { "from": "edge", "let": { "gid": "$gid" }, "pipeline": [ { "$match": { "_id": { "$gte": 1, "$lte": 5 }, "$expr": { "$eq": [ "$$gid", "$to" ] } }} ], "as": "from" }} 

In realtà questo sarebbe fondamentalmente ciò che MongoDB sta facendo “sotto le copertine” con la syntax precedente poiché 3.6 usa $expr “internamente” per build la dichiarazione. La differenza ovviamente è che non esiste un’opzione "unwinding" presente in come la $lookup viene effettivamente eseguita.

Se nessun documento viene effettivamente prodotto come risultato dell’espressione "pipeline" , l’array di destinazione all’interno del documento master sarà di fatto vuoto, proprio come un “SINISTRA SINISTRA” in realtà lo fa e sarebbe il normale comportamento di $lookup senza alcun altre opzioni.

Tuttavia, l’array di output NON DEVE causare che il documento in cui viene creato superi il limite BSON . Quindi spetta davvero a te assicurare che qualsiasi contenuto “corrispondente” delle condizioni rimanga sotto questo limite o lo stesso errore persisterà, a meno che, ovviamente, non usi effettivamente $unwind per effettuare il “JOIN INTERNO”.