Modern
Från MongoDB 3.6 finns det en "ny" metod för detta genom att använda $lookup
att utföra en "self join" på ungefär samma sätt som den ursprungliga markörbearbetningen som visas nedan.
Eftersom du i den här utgåvan kan ange en "pipeline"
argument till $lookup
som en källa för "join", betyder detta i huvudsak att du kan använda $match
och $limit
för att samla in och "begränsa" posterna för arrayen:
db.messages.aggregate([
{ "$group": { "_id": "$conversation_ID" } },
{ "$lookup": {
"from": "messages",
"let": { "conversation": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
{ "$limit": 10 },
{ "$project": { "_id": 1 } }
],
"as": "msgs"
}}
])
Du kan valfritt lägga till ytterligare projektion efter $lookup
för att göra arrayobjekten helt enkelt till värdena snarare än dokument med en _id
nyckel, men det grundläggande resultatet är där genom att helt enkelt göra ovanstående.
Det finns fortfarande den utestående SERVER-9277 som faktiskt begär en "limit to push" direkt, men med $lookup
på detta sätt är ett gångbart alternativ under tiden.
OBS :Det finns också
$slice
som introducerades efter att ha skrivit det ursprungliga svaret och nämnts av "outstanding JIRA issue" i originalinnehållet. Även om du kan få samma resultat med små resultatuppsättningar, innebär det att du fortfarande "skjuter in allt" i arrayen och sedan begränsar den slutliga arrayutgången till önskad längd.Så det är den huvudsakliga skillnaden och varför det i allmänhet inte är praktiskt att
$slice
för stora resultat. Men kan givetvis användas omväxlande i de fall det är det.Det finns några fler detaljer om mongodb-gruppvärden i flera fält om endera alternativ användning.
Original
Som nämnts tidigare är detta inte omöjligt men definitivt ett fruktansvärt problem.
Om ditt största bekymmer faktiskt är att dina resulterande arrayer kommer att bli exceptionellt stora, är det bästa sättet att skicka in för varje distinkt "conversation_ID" som en individuell fråga och sedan kombinera dina resultat. I mycket MongoDB 2.6-syntax som kan behöva finjusteras beroende på vad din språkimplementering faktiskt är:
var results = [];
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID"
}}
]).forEach(function(doc) {
db.messages.aggregate([
{ "$match": { "conversation_ID": doc._id } },
{ "$limit": 10 },
{ "$group": {
"_id": "$conversation_ID",
"msgs": { "$push": "$_id" }
}}
]).forEach(function(res) {
results.push( res );
});
});
Men det beror helt på om det är det du försöker undvika. Så till det verkliga svaret:
Det första problemet här är att det inte finns någon funktion för att "begränsa" antalet objekt som "skjuts" in i en array. Det är verkligen något vi skulle vilja, men funktionen finns inte för närvarande.
Det andra problemet är att du inte kan använda $slice
även när du trycker in alla objekt i en array , eller någon liknande operatör i aggregeringspipelinen. Så det finns inget nuvarande sätt att få bara "topp 10"-resultaten från en producerad array med en enkel operation.
Men du kan faktiskt producera en uppsättning operationer för att effektivt "skiva" på dina grupperingsgränser. Det är ganska involverat, och till exempel här kommer jag att reducera arrayelementen "skivade" till endast "sex". Det främsta skälet här är att demonstrera processen och visa hur man gör detta utan att vara destruktiv med arrayer som inte innehåller den totala summan du vill "skiva" till.
Med ett exempel på dokument:
{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }
Du kan se där att när du grupperar efter dina förutsättningar får du en array med tio element och en annan med "fem". Vad du vill göra här reducera båda till de översta "sex" utan att "förstöra" arrayen som bara matchar "fem" element.
Och följande fråga:
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID",
"first": { "$first": "$_id" },
"msgs": { "$push": "$_id" },
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"seen": { "$eq": [ "$first", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"seen": { "$eq": [ "$second", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"seen": { "$eq": [ "$third", "$msgs" ] },
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"seen": { "$eq": [ "$forth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"seen": { "$eq": [ "$fifth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$fifth" },
"sixth": { "$first": "$msgs" },
}},
{ "$project": {
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"sixth": 1,
"pos": { "$const": [ 1,2,3,4,5,6 ] }
}},
{ "$unwind": "$pos" },
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [
{ "$eq": [ "$pos", 1 ] },
"$first",
{ "$cond": [
{ "$eq": [ "$pos", 2 ] },
"$second",
{ "$cond": [
{ "$eq": [ "$pos", 3 ] },
"$third",
{ "$cond": [
{ "$eq": [ "$pos", 4 ] },
"$forth",
{ "$cond": [
{ "$eq": [ "$pos", 5 ] },
"$fifth",
{ "$cond": [
{ "$eq": [ "$pos", 6 ] },
"$sixth",
false
]}
]}
]}
]}
]}
]
}
}
}},
{ "$unwind": "$msgs" },
{ "$match": { "msgs": { "$ne": false } }},
{ "$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}}
])
Du får de bästa resultaten i arrayen, upp till sex poster:
{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }
Som du kan se här, massor av kul.
Efter att du initialt har grupperat vill du i princip "poppa" $first
värde utanför stacken för arrayresultaten. För att göra denna process lite förenklad gör vi faktiskt detta i den inledande operationen. Så processen blir:
$unwind
arrayen- Jämför med de värden som redan har setts med en
$eq
jämställdhetsmatch $sort
resultaten att "flyta"false
osynliga värden till toppen (detta behåller fortfarande ordningen)$group
tillbaka igen och "poppa"$first
osynligt värde som nästa medlem i stacken. Även detta använder$cond
operatorn för att ersätta "sedda" värden i arraystacken medfalse
för att hjälpa till i utvärderingen.
Den sista åtgärden med $cond
finns det för att se till att framtida iterationer inte bara lägger till det sista värdet av arrayen om och om igen där "slice"-antalet är större än arraymedlemmarna.
Hela den processen måste upprepas för så många föremål som du vill "skiva". Eftersom vi redan hittat det "första" objektet i den initiala grupperingen betyder det n-1
iterationer för det önskade skivresultatet.
De sista stegen är egentligen bara en valfri illustration av att konvertera allt tillbaka till arrayer för resultatet som slutligen visas. Så egentligen bara att villkorligt pusha objekt eller false
tillbaka genom sin matchande position och slutligen "filtrera" bort alla false
värden så att ändmatriserna har "sex" respektive "fem" medlemmar.
Så det finns ingen standardoperatör för detta, och du kan inte bara "begränsa" pushen till 5 eller 10 eller vilka objekt som helst i arrayen. Men om du verkligen måste göra det, så är detta ditt bästa tillvägagångssätt.
Du kan möjligen närma dig detta med mapReduce och överge aggregeringsramverket tillsammans. Tillvägagångssättet jag skulle ta (inom rimliga gränser) skulle vara att effektivt ha en hash-karta i minnet på servern och samla arrayer till den, samtidigt som jag använder JavaScript-segment för att "begränsa" resultaten:
db.messages.mapReduce(
function () {
if ( !stash.hasOwnProperty(this.conversation_ID) ) {
stash[this.conversation_ID] = [];
}
if ( stash[this.conversation_ID.length < maxLen ) {
stash[this.conversation_ID].push( this._id );
emit( this.conversation_ID, 1 );
}
},
function(key,values) {
return 1; // really just want to keep the keys
},
{
"scope": { "stash": {}, "maxLen": 10 },
"finalize": function(key,value) {
return { "msgs": stash[key] };
},
"out": { "inline": 1 }
}
)
Så det bygger bara i princip upp "in-memory"-objektet som matchar de utsända "nycklarna" med en array som aldrig överskrider den maximala storleken du vill hämta från dina resultat. Dessutom bryr det sig inte ens om att "sända ut" objektet när den maximala stapeln är uppfylld.
Reduceringsdelen gör faktiskt inget annat än att i huvudsak bara reducera till "nyckel" och ett enda värde. Så ifall vår reducerare inte skulle bli anropad, vilket skulle vara sant om det bara fanns ett värde för en nyckel, tar finaliseringsfunktionen hand om att mappa "stash"-nycklarna till den slutliga utgången.
Effektiviteten av detta varierar beroende på storleken på utdata, och JavaScript-utvärdering är verkligen inte snabb, men möjligen snabbare än att bearbeta stora arrayer i en pipeline.
Rösta upp JIRA-frågorna för att faktiskt ha en "slice"-operator eller till och med en "limit" på "$push" och "$addToSet", vilket båda skulle vara praktiskt. Jag hoppas personligen att åtminstone vissa ändringar kan göras i $map
operatör för att exponera det "aktuella index"-värdet vid bearbetning. Det skulle effektivt tillåta "slicing" och andra operationer.
Du skulle verkligen vilja koda detta för att "generera" alla nödvändiga iterationer. Om svaret här får tillräckligt med kärlek och/eller annan tid i väntan som jag har i tuits, så kan jag lägga till lite kod för att visa hur man gör detta. Det är redan ett lagom långt svar.
Kod för att generera pipeline:
var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;
var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": [] } } };
for ( var x = 1; x <= maxLen; x++ ) {
fproj["$project"][""+x] = 1;
fproj["$project"]["pos"]["$const"].push( x );
var rec = {
"$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
};
if ( stack.length == 0 ) {
rec["$cond"].push( false );
} else {
lval = stack.pop();
rec["$cond"].push( lval );
}
stack.push( rec );
if ( x == 1) {
pipe.push({ "$group": {
"_id": key,
"1": { "$first": val },
"msgs": { "$push": val }
}});
} else {
pipe.push({ "$unwind": "$msgs" });
var proj = {
"$project": {
"msgs": 1
}
};
proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };
var grp = {
"$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
}
}
};
for ( n=x; n >= 1; n-- ) {
if ( n != x )
proj["$project"][""+n] = 1;
grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
}
pipe.push( proj );
pipe.push({ "$sort": { "seen": 1 } });
pipe.push(grp);
}
}
pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": stack[0] }
}
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}
});
Det bygger den grundläggande iterativa metoden upp till maxLen
med stegen från $unwind
till $group
. Också inbäddade i det finns detaljer om de slutliga projektionerna som krävs och det "kapslade" villkorliga uttalandet. Den sista är i grunden det tillvägagångssätt som använts i denna fråga:
Garanterar MongoDB:s $in-klausul?