valores del grupo mongodb por campos múltiples

Por ejemplo, tengo estos documentos:

{ "addr": "address1", "book": "book1" }, { "addr": "address2", "book": "book1" }, { "addr": "address1", "book": "book5" }, { "addr": "address3", "book": "book9" }, { "addr": "address2", "book": "book5" }, { "addr": "address2", "book": "book1" }, { "addr": "address1", "book": "book1" }, { "addr": "address15", "book": "book1" }, { "addr": "address9", "book": "book99" }, { "addr": "address90", "book": "book33" }, { "addr": "address4", "book": "book3" }, { "addr": "address5", "book": "book1" }, { "addr": "address77", "book": "book11" }, { "addr": "address1", "book": "book1" } 

y así.

¿Cómo puedo hacer una solicitud, que describirá las principales direcciones N y los mejores M libros por dirección?

Ejemplo de resultado esperado:

dirección1 | book_1: 5
| book_2: 10
| book_3: 50
| total: 65
______________________
dirección2 | book_1: 10
| book_2: 10
| …
| book_M: 10
| total: M * 10

______________________
direcciónN | book_1: 20
| book_2: 20
| …
| book_M: 20
| total: M * 20

TLDR Resumen

En los lanzamientos modernos de MongoDB, puede forzar esto con $slice justo después del resultado de agregación básico. Para resultados “grandes”, ejecute consultas paralelas en su lugar para cada agrupación (una lista de demostración se encuentra al final de la respuesta) o espere a que se resuelva SERVER-9377 , lo que permitiría un “límite” de la cantidad de elementos a $push para una matriz.

 db.books.aggregate([ { "$group": { "_id": { "addr": "$addr", "book": "$book" }, "bookCount": { "$sum": 1 } }}, { "$group": { "_id": "$_id.addr", "books": { "$push": { "book": "$_id.book", "count": "$bookCount" }, }, "count": { "$sum": "$bookCount" } }}, { "$sort": { "count": -1 } }, { "$limit": 2 }, { "$project": { "books": { "$slice": [ "$books", 2 ] }, "count": 1 }} ]) 

Vista previa de MongoDB 3.6

Aún no se ha resuelto SERVER-9377 , pero en esta versión $lookup permite una nueva opción “no correlacionada” que toma una expresión "pipeline" como argumento en lugar de las "localFields" y "foreignFields" . Esto permite una “autocombinación” con otra expresión de interconexión, en la que podemos aplicar $limit para devolver los resultados “top-n”.

 db.books.aggregate([ { "$group": { "_id": "$addr", "count": { "$sum": 1 } }}, { "$sort": { "count": -1 } }, { "$limit": 2 }, { "$lookup": { "from": "books", "let": { "addr": "$_id" }, "pipeline": [ { "$match": { "$expr": { "$eq": [ "$addr", "$$addr"] } }}, { "$group": { "_id": "$book", "count": { "$sum": 1 } }}, { "$sort": { "count": -1 } }, { "$limit": 2 } ], "as": "books" }} ]) 

La otra adición aquí es, por supuesto, la capacidad de interpolar la variable a través de $expr utilizando $match para seleccionar los elementos coincidentes en la “unión”, pero la premisa general es una “tubería dentro de una tubería” donde el contenido interno puede ser filtrado por coincidencias del padre. Como ambos son “tuberías”, podemos $limit cada resultado por separado.

Esta sería la siguiente mejor opción para ejecutar consultas paralelas, y en realidad sería mejor si se permitiera el $match y pudiese usar un índice en el procesamiento “sub-pipeline”. Entonces, cuál es el que no utiliza el “límite de $push “, como se lo pregunta el problema mencionado, realmente ofrece algo que debería funcionar mejor.


Contenido original

Pareces haber tropezado con el problema “N” superior. De alguna manera, su problema es bastante fácil de resolver, aunque no con la limitación exacta que usted solicita:

 db.books.aggregate([ { "$group": { "_id": { "addr": "$addr", "book": "$book" }, "bookCount": { "$sum": 1 } }}, { "$group": { "_id": "$_id.addr", "books": { "$push": { "book": "$_id.book", "count": "$bookCount" }, }, "count": { "$sum": "$bookCount" } }}, { "$sort": { "count": -1 } }, { "$limit": 2 } ]) 

Ahora que te dará un resultado como este:

 { "result" : [ { "_id" : "address1", "books" : [ { "book" : "book4", "count" : 1 }, { "book" : "book5", "count" : 1 }, { "book" : "book1", "count" : 3 } ], "count" : 5 }, { "_id" : "address2", "books" : [ { "book" : "book5", "count" : 1 }, { "book" : "book1", "count" : 2 } ], "count" : 3 } ], "ok" : 1 } 

Por lo tanto, esto difiere de lo que usted está solicitando en que, aunque obtenemos los mejores resultados para los valores de la dirección, la selección subyacente de “libros” no se limita a una cantidad requerida de resultados.

Esto resulta ser muy difícil de hacer, pero se puede hacer, aunque la complejidad simplemente aumenta con la cantidad de elementos que necesita para combinar. Para mantenerlo simple, podemos mantener esto en 2 partidos como máximo:

 db.books.aggregate([ { "$group": { "_id": { "addr": "$addr", "book": "$book" }, "bookCount": { "$sum": 1 } }}, { "$group": { "_id": "$_id.addr", "books": { "$push": { "book": "$_id.book", "count": "$bookCount" }, }, "count": { "$sum": "$bookCount" } }}, { "$sort": { "count": -1 } }, { "$limit": 2 }, { "$unwind": "$books" }, { "$sort": { "count": 1, "books.count": -1 } }, { "$group": { "_id": "$_id", "books": { "$push": "$books" }, "count": { "$first": "$count" } }}, { "$project": { "_id": { "_id": "$_id", "books": "$books", "count": "$count" }, "newBooks": "$books" }}, { "$unwind": "$newBooks" }, { "$group": { "_id": "$_id", "num1": { "$first": "$newBooks" } }}, { "$project": { "_id": "$_id", "newBooks": "$_id.books", "num1": 1 }}, { "$unwind": "$newBooks" }, { "$project": { "_id": "$_id", "num1": 1, "newBooks": 1, "seen": { "$eq": [ "$num1", "$newBooks" ]} }}, { "$match": { "seen": false } }, { "$group":{ "_id": "$_id._id", "num1": { "$first": "$num1" }, "num2": { "$first": "$newBooks" }, "count": { "$first": "$_id.count" } }}, { "$project": { "num1": 1, "num2": 1, "count": 1, "type": { "$cond": [ 1, [true,false],0 ] } }}, { "$unwind": "$type" }, { "$project": { "books": { "$cond": [ "$type", "$num1", "$num2" ]}, "count": 1 }}, { "$group": { "_id": "$_id", "count": { "$first": "$count" }, "books": { "$push": "$books" } }}, { "$sort": { "count": -1 } } ]) 

Así que eso realmente le dará los 2 mejores “libros” de las dos primeras entradas de “dirección”.

Pero por mi dinero, quédese con la primera forma y luego simplemente “corte” los elementos de la matriz que se devuelven para tomar los primeros elementos “N”.


Código de demostración

El código de demostración es apropiado para su uso con las versiones LTS actuales de NodeJS de las versiones v8.x y v10.x. Eso es principalmente para la syntax async/await , pero no hay nada realmente dentro del flujo general que tenga dicha restricción, y se adapta con poca alteración a las promesas simples o incluso de regreso a la implementación de callback simple.

index.js

 const { MongoClient } = require('mongodb'); const fs = require('mz/fs'); const uri = 'mongodb://localhost:27017'; const log = data => console.log(JSON.stringify(data, undefined, 2)); (async function() { try { const client = await MongoClient.connect(uri); const db = client.db('bookDemo'); const books = db.collection('books'); let { version } = await db.command({ buildInfo: 1 }); version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]); // Clear and load books await books.deleteMany({}); await books.insertMany( (await fs.readFile('books.json')) .toString() .replace(/\n$/,"") .split("\n") .map(JSON.parse) ); if ( version >= 3.6 ) { // Non-correlated pipeline with limits let result = await books.aggregate([ { "$group": { "_id": "$addr", "count": { "$sum": 1 } }}, { "$sort": { "count": -1 } }, { "$limit": 2 }, { "$lookup": { "from": "books", "as": "books", "let": { "addr": "$_id" }, "pipeline": [ { "$match": { "$expr": { "$eq": [ "$addr", "$$addr" ] } }}, { "$group": { "_id": "$book", "count": { "$sum": 1 }, }}, { "$sort": { "count": -1 } }, { "$limit": 2 } ] }} ]).toArray(); log({ result }); } // Serial result procesing with parallel fetch // First get top addr items let topaddr = await books.aggregate([ { "$group": { "_id": "$addr", "count": { "$sum": 1 } }}, { "$sort": { "count": -1 } }, { "$limit": 2 } ]).toArray(); // Run parallel top books for each addr let topbooks = await Promise.all( topaddr.map(({ _id: addr }) => books.aggregate([ { "$match": { addr } }, { "$group": { "_id": "$book", "count": { "$sum": 1 } }}, { "$sort": { "count": -1 } }, { "$limit": 2 } ]).toArray() ) ); // Merge output topaddr = topaddr.map((d,i) => ({ ...d, books: topbooks[i] })); log({ topaddr }); client.close(); } catch(e) { console.error(e) } finally { process.exit() } })() 

books.json

 { "addr": "address1", "book": "book1" } { "addr": "address2", "book": "book1" } { "addr": "address1", "book": "book5" } { "addr": "address3", "book": "book9" } { "addr": "address2", "book": "book5" } { "addr": "address2", "book": "book1" } { "addr": "address1", "book": "book1" } { "addr": "address15", "book": "book1" } { "addr": "address9", "book": "book99" } { "addr": "address90", "book": "book33" } { "addr": "address4", "book": "book3" } { "addr": "address5", "book": "book1" } { "addr": "address77", "book": "book11" } { "addr": "address1", "book": "book1" } 

Usando la función agregada como a continuación:

 [ {$group: {_id : {book : '$book',address:'$addr'}, total:{$sum :1}}}, {$project : {book : '$_id.book', address : '$_id.address', total : '$total', _id : 0}} ] 

le dará un resultado como el siguiente:

  { "total" : 1, "book" : "book33", "address" : "address90" }, { "total" : 1, "book" : "book5", "address" : "address1" }, { "total" : 1, "book" : "book99", "address" : "address9" }, { "total" : 1, "book" : "book1", "address" : "address5" }, { "total" : 1, "book" : "book5", "address" : "address2" }, { "total" : 1, "book" : "book3", "address" : "address4" }, { "total" : 1, "book" : "book11", "address" : "address77" }, { "total" : 1, "book" : "book9", "address" : "address3" }, { "total" : 1, "book" : "book1", "address" : "address15" }, { "total" : 2, "book" : "book1", "address" : "address2" }, { "total" : 3, "book" : "book1", "address" : "address1" } 

No obtuve el formato de resultado esperado, así que siéntase libre de modificarlo a uno que necesite.