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

Mongodb aggrega l'ordinamento e il limite all'interno del gruppo

Il problema di base

Non è l'idea più saggia là fuori per provare a farlo nel quadro di aggregazione attualmente in un prossimo futuro prevedibile. Il problema principale ovviamente deriva da questa riga nel codice che hai già:

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

E questo significa esattamente che, in pratica, ciò che deve accadere è che tutti gli oggetti all'interno della chiave di raggruppamento devono essere inseriti in un array per ottenere i risultati "top N" in qualsiasi codice successivo.

Questo chiaramente non si ridimensiona poiché alla fine la dimensione di quell'array stesso può molto plausibilmente superare il limite BSON di 16 MB e indipendentemente dal resto dei dati nel documento raggruppato. Il problema principale qui è che non è possibile "limitare la spinta" a un certo numero di elementi. C'è un problema JIRA di vecchia data su una cosa del genere.

Solo per questo motivo, l'approccio più pratico consiste nell'eseguire query individuali per gli elementi "top N" per ciascuna chiave di raggruppamento. Questi non devono nemmeno essere .aggregate() affermazioni (a seconda dei dati) e può davvero essere qualsiasi cosa che limiti semplicemente i valori "top N" desiderati.

Miglior approccio

La tua architettura sembra essere su node.js con mongoose , ma tutto ciò che supporta l'IO asincrono e l'esecuzione parallela di query sarà l'opzione migliore. Idealmente qualcosa con la propria libreria API che supporti la combinazione dei risultati di tali query in un'unica risposta.

Ad esempio, c'è questo elenco di esempio semplificato che utilizza la tua architettura e le librerie disponibili (in particolare async ) che esegue esattamente questo risultato parallelo e combinato:

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

Ciò comporta solo i primi 2 risultati per ciascun commerciante nell'output:

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

È davvero il modo più efficiente per elaborarlo anche se richiederà risorse poiché sono ancora più query. Ma non è neanche lontanamente vicino alle risorse consumate nella pipeline di aggregazione se tenti di archiviare tutti i documenti in un array ed elaborarlo.

Il problema dell'aggregato, presente e prossimo futuro

A tale riga, è possibile considerare che il numero di documenti non causa una violazione del limite BSON che ciò può essere fatto. I metodi con l'attuale versione di MongoDB non sono eccezionali per questo, ma la versione imminente (al momento della scrittura, 3.1.8 dev branch lo fa) almeno introduce una $slice operatore alla pipeline di aggregazione. Quindi, se sei più intelligente sull'operazione di aggregazione e usa un $sort prima, gli elementi già ordinati nell'array possono essere scelti facilmente:

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

Il che produce lo stesso risultato di base in quanto i primi 2 elementi vengono "tagliati" dall'array una volta che sono stati ordinati per primi.

In realtà è anche "possibile" nelle versioni attuali, ma con gli stessi vincoli di base in quanto ciò comporta ancora il push di tutto il contenuto in un array dopo aver prima ordinato il contenuto. Ci vuole solo un approccio "iterativo". Puoi codificarlo per produrre la pipeline di aggregazione per voci più grandi, ma solo mostrare "due" dovrebbe mostrare che non è una buona idea provare:

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

E ancora mentre "possibile" nelle versioni precedenti (questo utilizza le funzionalità introdotte 2.6 per abbreviare poiché hai già taggato $$ROOT ), i passaggi di base sono memorizzare l'array e quindi ottenere ogni elemento "fuori dallo stack" utilizzando $first e confrontandolo (e potenzialmente altri) con gli elementi all'interno dell'array per rimuoverli e quindi ottenere l'elemento "primo successivo" da quello stack fino a quando il tuo "top N" non sarà terminato.

Conclusione

Fino al giorno in cui esiste un'operazione del genere che consente gli elementi in un $push accumulatore di aggregazione da limitare ad un certo conteggio, quindi questa non è proprio un'operazione pratica per aggregazione.

Puoi farlo, se i dati che hai in questi risultati sono abbastanza piccoli e potrebbe anche essere più efficiente dell'elaborazione lato client se i server di database sono di specifiche sufficienti per fornire un vantaggio reale. Ma è probabile che nessuno dei due sia il caso nella maggior parte delle applicazioni reali di utilizzo ragionevole.

La soluzione migliore è utilizzare prima l'opzione "interrogazione parallela". Ridimensionerà sempre bene e non è necessario "codificare" una logica tale che un particolare raggruppamento potrebbe non restituire almeno i "primi N" elementi totali richiesti e capire come conservarli (esempio molto più lungo di quello omesso ) poiché esegue semplicemente ogni query e combina i risultati.

Usa query parallele. Sarà migliore dell'approccio codificato che hai e supererà di molto l'approccio di aggregazione dimostrato. Fino a quando non ci sarà almeno un'opzione migliore.