sql >> Base de Datos >  >> NoSQL >> MongoDB

Clasificación y límite agregados de Mongodb dentro del grupo

El problema básico

No es la idea más sabia intentar hacer esto en el marco de agregación actual en un futuro cercano previsible. El problema principal, por supuesto, proviene de esta línea en el código que ya tiene:

"items" : { "$push": "$$ROOT" }

Y eso significa exactamente eso, en el sentido de que lo que básicamente debe suceder es que todos los objetos dentro de la clave de agrupación deben insertarse en una matriz para llegar a los resultados "N superiores" en cualquier código posterior.

Claramente, esto no se escala, ya que eventualmente el tamaño de esa matriz en sí puede exceder el límite de BSON de 16 MB, e independientemente del resto de los datos en el documento agrupado. El problema principal aquí es que no es posible "limitar el empuje" a solo una cierta cantidad de elementos. Hay un problema de JIRA de larga data sobre tal cosa.

Solo por esa razón, el enfoque más práctico para esto es ejecutar consultas individuales para los elementos "N principales" para cada clave de agrupación. Estos ni siquiera necesitan ser .aggregate() declaraciones (dependiendo de los datos) y realmente puede ser cualquier cosa que simplemente limite los valores "top N" que desea.

Mejor enfoque

Su arquitectura parece estar en node.js con mongoose , pero cualquier cosa que admita E/S asíncrona y ejecución paralela de consultas será la mejor opción. Idealmente, algo con su propia biblioteca API que admita la combinación de los resultados de esas consultas en una sola respuesta.

Por ejemplo, existe esta lista de ejemplo simplificada que utiliza su arquitectura y las bibliotecas disponibles (en particular, async ) que hace estos resultados paralelos y combinados exactamente:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var data = [
  { "merchant": 1, "rating": 1 },
  { "merchant": 1, "rating": 2 },
  { "merchant": 1, "rating": 3 },
  { "merchant": 2, "rating": 1 },
  { "merchant": 2, "rating": 2 },
  { "merchant": 2, "rating": 3 }
];

var testSchema = new Schema({
  merchant: Number,
  rating: Number
});

var Test = mongoose.model( 'Test', testSchema, 'test' );

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      async.each(data,function(item,callback) {
        Test.create(item,callback);
      },callback);
    },
    function(callback) {
      async.waterfall(
        [
          function(callback) {
            Test.distinct("merchant",callback);
          },
          function(merchants,callback) {
            async.concat(
              merchants,
              function(merchant,callback) {
                Test.find({ "merchant": merchant })
                  .sort({ "rating": -1 })
                  .limit(2)
                  .exec(callback);
              },
              function(err,results) {
                console.log(JSON.stringify(results,undefined,2));
                callback(err);
              }
            );
          }
        ],
        callback
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Esto da como resultado solo los 2 mejores resultados para cada comerciante en la salida:

[
  {
    "_id": "560d153669fab495071553ce",
    "merchant": 1,
    "rating": 3,
    "__v": 0
  },
  {
    "_id": "560d153669fab495071553cd",
    "merchant": 1,
    "rating": 2,
    "__v": 0
  },
  {
    "_id": "560d153669fab495071553d1",
    "merchant": 2,
    "rating": 3,
    "__v": 0
  },
  {
    "_id": "560d153669fab495071553d0",
    "merchant": 2,
    "rating": 2,
    "__v": 0
  }
]

Realmente es la forma más eficiente de procesar esto, aunque requerirá recursos ya que todavía son múltiples consultas. Pero ni cerca de los recursos consumidos en la canalización de agregación si intenta almacenar todos los documentos en una matriz y procesarlos.

El Problema de los Agregados, ahora y en el futuro cercano

En esa línea, es posible considerando que la cantidad de documentos no genera un incumplimiento en el límite de BSON que esto se puede hacer. Los métodos con la versión actual de MongoDB no son excelentes para esto, pero la próxima versión (al momento de escribir esto, la rama de desarrollo 3.1.8 hace esto) al menos presenta un $slice operador a la canalización de agregación. Entonces, si es más inteligente acerca de la operación de agregación y usa un $sort primero, luego los elementos ya ordenados en la matriz se pueden seleccionar fácilmente:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var data = [
  { "merchant": 1, "rating": 1 },
  { "merchant": 1, "rating": 2 },
  { "merchant": 1, "rating": 3 },
  { "merchant": 2, "rating": 1 },
  { "merchant": 2, "rating": 2 },
  { "merchant": 2, "rating": 3 }
];

var testSchema = new Schema({
  merchant: Number,
  rating: Number
});

var Test = mongoose.model( 'Test', testSchema, 'test' );

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      async.each(data,function(item,callback) {
        Test.create(item,callback);
      },callback);
    },
    function(callback) {
      Test.aggregate(
        [
          { "$sort": { "merchant": 1, "rating": -1 } },
          { "$group": {
            "_id": "$merchant",
            "items": { "$push": "$$ROOT" }
          }},
          { "$project": {
            "items": { "$slice": [ "$items", 2 ] }
          }}
        ],
        function(err,results) {
          console.log(JSON.stringify(results,undefined,2));
          callback(err);
        }
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Lo que produce el mismo resultado básico que los 2 elementos principales se "cortan" de la matriz una vez que se ordenaron primero.

En realidad, también es "posible" en las versiones actuales, pero con las mismas restricciones básicas en el sentido de que todavía implica insertar todo el contenido en una matriz después de ordenar el contenido primero. Solo se necesita un enfoque "iterativo". Puede codificar esto para producir la canalización de agregación para entradas más grandes, pero solo mostrar "dos" debería mostrar que no es una buena idea intentarlo:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var data = [
  { "merchant": 1, "rating": 1 },
  { "merchant": 1, "rating": 2 },
  { "merchant": 1, "rating": 3 },
  { "merchant": 2, "rating": 1 },
  { "merchant": 2, "rating": 2 },
  { "merchant": 2, "rating": 3 }
];

var testSchema = new Schema({
  merchant: Number,
  rating: Number
});

var Test = mongoose.model( 'Test', testSchema, 'test' );

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      async.each(data,function(item,callback) {
        Test.create(item,callback);
      },callback);
    },
    function(callback) {
      Test.aggregate(
        [
          { "$sort": { "merchant": 1, "rating": -1 } },
          { "$group": {
            "_id": "$merchant",
            "items": { "$push": "$$ROOT" }
          }},
          { "$unwind": "$items" },
          { "$group": {
            "_id": "$_id",
            "first": { "$first": "$items" },
            "items": { "$push": "$items" }
          }},
          { "$unwind": "$items" },
          { "$redact": {
            "$cond": [
              { "$eq": [ "$items", "$first" ] },
              "$$PRUNE",
              "$$KEEP"
            ]
          }},
          { "$group": {
            "_id": "$_id",
            "first": { "$first": "$first" },
            "second": { "$first": "$items" }
          }},
          { "$project": {
            "items": {
              "$map": {
                "input": ["A","B"],
                "as": "el",
                "in": {
                  "$cond": [
                    { "$eq": [ "$$el", "A" ] },
                    "$first",
                    "$second"
                  ]
                }
              }
            }
          }}
        ],
        function(err,results) {
          console.log(JSON.stringify(results,undefined,2));
          callback(err);
        }
      );
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Y nuevamente, aunque "posible" en versiones anteriores (esto está usando las funciones introducidas en 2.6 para acortar, ya que ya etiquetaste $$ROOT ), los pasos básicos son almacenar la matriz y luego sacar cada elemento "de la pila" usando $first y comparar eso (y potencialmente otros) con los elementos dentro de la matriz para eliminarlos y luego sacar el elemento "siguiente primero" de esa pila hasta que finalmente termine su "N superior".

Conclusión

Hasta que llegue el día en que exista tal operación que permita los artículos en un $push acumulador de agregación se limite a un cierto conteo, entonces esta no es realmente una operación práctica para agregar.

Puede hacerlo, si los datos que tiene en estos resultados son lo suficientemente pequeños, e incluso podría ser más eficiente que el procesamiento del lado del cliente si los servidores de la base de datos tienen las especificaciones suficientes para proporcionar una ventaja real. Pero lo más probable es que ninguno de los dos sea el caso en la mayoría de las aplicaciones reales de uso razonable.

La mejor apuesta es usar la opción de "consulta paralela" demostrada primero. Siempre va a escalar bien, y no hay necesidad de "codificar" tal lógica que una agrupación en particular podría no devolver al menos el total de "N principales" elementos requeridos y averiguar cómo retenerlos (ejemplo mucho más largo de eso omitido ) ya que simplemente realiza cada consulta y combina los resultados.

Utilice consultas paralelas. Va a ser mejor que el enfoque codificado que tiene, y va a superar el enfoque de agregación demostrado por mucho. Hasta que haya una mejor opción al menos.