sql >> Database >  >> NoSQL >> MongoDB

Mongodb aggregatie sorteren en beperken binnen groep

Het basisprobleem

Het is niet het meest verstandige idee om te proberen dit in de nabije toekomst in het aggregatieraamwerk te doen. Het grootste probleem komt natuurlijk van deze regel in de code die je al hebt:

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

En dat betekent precies dat, in die zin dat wat er in feite moet gebeuren, is dat alle objecten binnen de groeperingssleutel in een array moeten worden geduwd om bij de "top N"-resultaten in latere code te komen.

Dit schaalt duidelijk niet, want uiteindelijk kan de grootte van die array zelf heel goed de BSON-limiet van 16 MB overschrijden, ongeacht de rest van de gegevens in het gegroepeerde document. De belangrijkste vangst hier is dat het niet mogelijk is om "de push te beperken" tot slechts een bepaald aantal items. Er is een al lang bestaand JIRA-probleem over zoiets.

Alleen al om die reden is de meest praktische benadering hiervoor het uitvoeren van individuele zoekopdrachten voor de "top N"-items voor elke groeperingssleutel. Deze hoeven niet eens .aggregate() . te zijn statments (afhankelijk van de gegevens) en kan echt alles zijn dat de gewenste "top N" -waarden beperkt.

Beste aanpak

Uw architectuur lijkt op node.js met mongoose , maar alles dat asynchrone IO en parallelle uitvoering van query's ondersteunt, is de beste optie. Idealiter iets met een eigen API-bibliotheek die het combineren van de resultaten van die zoekopdrachten in één antwoord ondersteunt.

Er is bijvoorbeeld deze vereenvoudigde voorbeeldlijst met gebruikmaking van uw architectuur en beschikbare bibliotheken (met name async ) die deze parallelle en gecombineerde resultaten precies doet:

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

Dit resulteert in alleen de top 2 resultaten voor elke handelaar in de 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
  }
]

Het is echt de meest efficiënte manier om dit te verwerken, hoewel het middelen zal vergen, omdat het nog steeds meerdere zoekopdrachten zijn. Maar lang niet de bronnen die in de aggregatiepijplijn worden opgegeten als je probeert alle documenten in een array op te slaan en te verwerken.

Het aggregaatprobleem, nu en in de nabije toekomst

Wat dat betreft is het mogelijk, aangezien het aantal documenten geen overschrijding van de BSON-grens veroorzaakt, dat dit kan. Methoden met de huidige release van MongoDB zijn hier niet geweldig voor, maar de komende release (op het moment van schrijven doet 3.1.8 dev branch dit ) introduceert in ieder geval een $slice operator naar de aggregatiepijplijn. Dus als je slimmer bent over de aggregatiebewerking en een $sort . gebruikt eerst kunnen de reeds gesorteerde items in de array eenvoudig worden uitgezocht:

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

Wat hetzelfde basisresultaat oplevert, aangezien de bovenste 2 items uit de array worden "gesneden" zodra ze als eerste zijn gesorteerd.

Het is ook "mogelijk" in huidige releases, maar met dezelfde basisbeperkingen in die zin dat dit nog steeds inhoudt dat alle inhoud in een array moet worden gepusht nadat de inhoud eerst is gesorteerd. Er is alleen een "iteratieve" benadering voor nodig. Je kunt dit coderen om de aggregatiepijplijn te produceren voor grotere invoer, maar als je alleen 'twee' laat zien, zou het niet echt een goed idee moeten zijn om het te proberen:

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

En nogmaals, terwijl het "mogelijk" was in eerdere versies (dit gebruikt 2.6 geïntroduceerde functies om in te korten omdat je al tagt $$ROOT ), zijn de basisstappen het opslaan van de array en het vervolgens "van de stapel" halen van elk item met $first en dat (en mogelijk andere) te vergelijken met items in de array om ze te verwijderen en vervolgens het "volgende eerst" item van die stapel te halen totdat je "top N" uiteindelijk klaar is.

Conclusie

Tot de dag komt dat er zo'n operatie is dat de items in een $push aggregatieaccumulator te beperken tot een bepaald aantal, dan is dit niet echt een praktische bewerking voor aggregatie.

U kunt het doen, als de gegevens die u in deze resultaten hebt klein genoeg zijn, en het zou zelfs efficiënter kunnen zijn dan de verwerking aan de clientzijde als de databaseservers voldoende specificaties hebben om een ​​echt voordeel te bieden. Maar de kans is groot dat geen van beide het geval zal zijn in de meeste echte toepassingen van redelijk gebruik.

De beste gok is om eerst de "parallelle zoekopdracht" optie te gebruiken. Het zal altijd goed schalen, en het is niet nodig om zo'n logica te "coderen" dat een bepaalde groepering mogelijk niet ten minste de totale "top N" -items retourneert en uitwerkt hoe ze te behouden (veel langer voorbeeld van dat weggelaten ) omdat het eenvoudig elke zoekopdracht uitvoert en de resultaten combineert.

Gebruik parallelle zoekopdrachten. Het zal beter zijn dan de gecodeerde benadering die je hebt, en het zal de aggregatiebenadering die is aangetoond door een lange weg overtreffen. Tot er tenminste een betere optie is.



  1. dus je HBase is kapot

  2. Hoe kan ik een booleaans veld in één document wisselen met atomaire bewerking?

  3. Gegevensstroom opslaan van POST-verzoek in GridFS, express, mongoDB, node.js

  4. Hoe vraag je vanuit Mongoose pre hook in een Node.js / Express-app?