Основният проблем
Не е най-мъдрата идея да се опитате да направите това в рамката за агрегиране в момента и в обозримо близко бъдеще. Основният проблем, разбира се, идва от този ред в кода, който вече имате:
"items" : { "$push": "$$ROOT" }
И това означава точно това, тъй като това, което по същество трябва да се случи, е, че всички обекти в ключа за групиране трябва да бъдат натиснати в масив, за да се стигне до „горните N“ резултати във всеки по-късен код.
Това очевидно не се мащабира, тъй като в крайна сметка размерът на самия този масив може да надхвърли ограничението на BSON от 16MB и независимо от останалите данни в групирания документ. Основната уловка тук е, че не е възможно да се „ограничи натискането“ само до определен брой елементи. Има дългогодишен проблем с JIRA точно за такова нещо.
Само поради тази причина най-практичният подход към това е да се изпълняват индивидуални заявки за „най-добрите N“ елементи за всеки групиращ ключ. Те дори не трябва да са .aggregate()
оператори (в зависимост от данните) и наистина може да бъде всичко, което просто ограничава желаните от вас „най-големи N“ стойности.
Най-добър подход
Архитектурата ви изглежда е на node.js
с mongoose
, но всичко, което поддържа асинхронен IO и паралелно изпълнение на заявки, ще бъде най-добрият вариант. В идеалния случай нещо със собствена API библиотека, която поддържа комбиниране на резултатите от тези заявки в един отговор.
Например има този опростен примерен списък, използващ вашата архитектура и налични библиотеки (по-специално async
), което прави тези паралелни и комбинирани резултати точно:
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();
}
);
Това води до само първите 2 резултата за всеки търговец в изхода:
[
{
"_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
}
]
Това наистина е най-ефективният начин за обработка на това, въпреки че ще отнеме ресурси, тъй като все още има множество заявки. Но не са близо до ресурсите, изядени в конвейера за агрегация, ако се опитате да съхраните всички документи в масив и да го обработите.
Общият проблем, сега и близко бъдеще
В този ред е възможно, като се има предвид, че броят на документите не причинява нарушение на лимита на BSON, това може да бъде направено. Методите с текущата версия на MongoDB не са страхотни за това, но предстоящата версия (към писането клонът на dev 3.1.8 прави това) поне въвежда $slice
оператор към тръбопровода за агрегация. Така че, ако сте по-умни относно операцията за агрегиране и използвайте $sort
първо, след това вече сортираните елементи в масива могат лесно да бъдат избрани:
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();
}
);
Което дава същия основен резултат, тъй като първите 2 елемента се „изрязват“ от масива, след като са сортирани първи.
Това също всъщност е "възможно" в текущите издания, но със същите основни ограничения, тъй като това все още включва избутване на цялото съдържание в масив след първо сортиране на съдържанието. Просто е необходим "итеративен" подход. Можете да кодирате това, за да създадете тръбопровода за агрегиране за по-големи вписвания, но само показването на „две“ трябва да покаже, че не е наистина страхотна идея да опитате:
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();
}
);
И отново, докато е „възможно“ в по-ранните версии (това използва въведените от 2.6 функции за съкращаване, тъй като вече сте маркирали $$ROOT
), основните стъпки са съхраняване на масива и след това извеждане на всеки елемент „от стека“ с помощта на $first
и сравняване на това (и потенциално други) с елементи в масива, за да ги премахнете и след това да извадите елемента „следващият първи“ от този стек, докато най-накрая вашето „топ N“ бъде готово.
Заключение
Докато не дойде денят, в който има такава операция, която позволява на елементите в $push
акумулаторът за агрегация да бъде ограничен до определен брой, тогава това всъщност не е практична операция за агрегат.
Можете да го направите, ако данните, които имате в тези резултати, са достатъчно малки и може дори да са по-ефективни от обработката от страна на клиента, ако сървърите на базата данни са с достатъчна спецификация, за да осигурят реално предимство. Но има вероятност нито едното, нито другото да е така в повечето реални приложения с разумна употреба.
Най-добрият залог е да използвате опцията "паралелна заявка", демонстрирана първа. Винаги ще се мащабира добре и няма нужда да се "кодира" такава логика, че определено групиране може да не върне поне общите необходими "най-големи N" елементи и да разбере как да ги запази (много по-дълъг пример за това, което е пропуснато ), тъй като просто изпълнява всяка заявка и комбинира резултатите.
Използвайте паралелни заявки. Той ще бъде по-добър от кодирания подход, който имате, и ще надмине демонстрирания дълъг път подхода за агрегиране. Докато има поне по-добър вариант.