Podstawowy problem
Nie jest najmądrzejszym pomysłem, aby spróbować zrobić to w ramach agregacji w chwili obecnej w dającej się przewidzieć niedalekiej przyszłości. Główny problem wynika oczywiście z tego wiersza w kodzie, który już masz:
"items" : { "$push": "$$ROOT" }
A to oznacza dokładnie to, co w zasadzie musi się wydarzyć, to to, że wszystkie obiekty w kluczu grupującym muszą zostać wepchnięte do tablicy, aby dostać się do "najwyższego N" wyników w dowolnym późniejszym kodzie.
To oczywiście nie skaluje się, ponieważ ostatecznie rozmiar samej tablicy może bardzo łatwo przekroczyć limit BSON wynoszący 16 MB i niezależnie od reszty danych w zgrupowanym dokumencie. Głównym haczykiem jest to, że nie można „ograniczyć nacisku” tylko do określonej liczby przedmiotów. W tej sprawie istnieje od dawna problem z JIRA.
Już z tego powodu najbardziej praktycznym podejściem do tego jest uruchamianie indywidualnych zapytań dla „najwyższych N” pozycji dla każdego klucza grupowania. Nie muszą to nawet być .aggregate()
instrukcje ( w zależności od danych ) i może to być naprawdę wszystko, co po prostu ogranicza żądane "najwyższe wartości".
Najlepsze podejście
Twoja architektura wygląda na node.js
z mongoose
, ale wszystko, co obsługuje asynchroniczne IO i równoległe wykonywanie zapytań, będzie najlepszą opcją. Idealnie coś z własną biblioteką API, która obsługuje łączenie wyników tych zapytań w jedną odpowiedź.
Na przykład jest ten uproszczony przykładowy listing wykorzystujący twoją architekturę i dostępne biblioteki (w szczególności async
), który robi dokładnie to równoległe i połączone wyniki:
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();
}
);
Daje to w wyniku tylko 2 najlepsze wyniki dla każdego sprzedawcy:
[
{
"_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
}
]
Jest to naprawdę najbardziej wydajny sposób na przetworzenie tego, chociaż będzie wymagał zasobów, ponieważ nadal jest to wiele zapytań. Ale nigdzie w pobliżu zasobów zjedzonych w potoku agregacji, jeśli spróbujesz przechowywać wszystkie dokumenty w tablicy i je przetworzyć.
Zagregowany problem, teraz i w najbliższej przyszłości
Do tej linii jest to możliwe biorąc pod uwagę, że ilość dokumentów nie powoduje przekroczenia limitu BSON, że można to zrobić. Metody z obecnym wydaniem MongoDB nie są do tego dobre, ale nadchodzące wydanie (od momentu pisania, 3.1.8 dev branch to robi) przynajmniej wprowadza $slice
operatora do potoku agregacji. Więc jeśli jesteś mądrzejszy w operacji agregacji i użyj $sort
najpierw można łatwo wybrać już posortowane elementy w tablicy:
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();
}
);
Co daje ten sam podstawowy wynik, ponieważ 2 górne elementy są „wycinane” z tablicy, gdy zostały posortowane jako pierwsze.
Jest to również „możliwe” w bieżących wydaniach, ale z tymi samymi podstawowymi ograniczeniami, polegającymi na tym, że nadal wiąże się to z wpychaniem całej zawartości do tablicy po uprzednim posortowaniu zawartości. To po prostu wymaga podejścia „iteracyjnego”. Możesz to zakodować, aby utworzyć potok agregacji dla większych wpisów, ale samo wyświetlenie „dwa” powinno pokazać, że nie jest to dobry pomysł:
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();
}
);
I znowu, podczas gdy "możliwe" we wcześniejszych wersjach (jest to użycie wprowadzonych funkcji 2.6, aby skrócić, ponieważ już tagujesz $$ROOT
), podstawowe kroki to przechowywanie tablicy, a następnie pobieranie każdego elementu "ze stosu" za pomocą $first
i porównywanie tego (i potencjalnie innych) z elementami w tablicy, aby je usunąć, a następnie pobrać element „następny pierwszy” z tego stosu, aż w końcu zostanie wykonane „górne N”.
Wniosek
Dopóki nie nadejdzie dzień, w którym istnieje taka operacja, która pozwala na przedmioty w $push
akumulator agregacji jest ograniczony do określonej liczby, to nie jest to tak naprawdę praktyczna operacja dla agregacji.
Możesz to zrobić, jeśli dane, które masz w tych wynikach, są wystarczająco małe, a może nawet być bardziej wydajne niż przetwarzanie po stronie klienta, jeśli serwery bazy danych mają wystarczającą specyfikację, aby zapewnić rzeczywistą przewagę. Ale są szanse, że nie będzie tak w większości rzeczywistych zastosowań o rozsądnym użyciu.
Najlepiej jest użyć najpierw pokazanej opcji "zapytanie równoległe". Zawsze będzie się dobrze skalować i nie ma potrzeby "kodowania" takiej logiki, że konkretne grupowanie może nie zwracać przynajmniej wszystkich wymaganych "N" pozycji i wymyślić, jak je zachować (znacznie dłuższy przykład tego pominiętego ), ponieważ po prostu wykonuje każde zapytanie i łączy wyniki.
Użyj zapytań równoległych. Będzie to lepsze niż podejście kodowane, które posiadasz, i znacznie przewyższy podejście agregacyjne zademonstrowane na dłuższą metę. Dopóki nie będzie przynajmniej lepszej opcji.