Så den forespørgsel, du har, vælger faktisk "dokumentet", som den skal. Men det, du leder efter, er at "filtrere de arrays", der er indeholdt, så de returnerede elementer kun matcher betingelsen for forespørgslen.
Det rigtige svar er selvfølgelig, at medmindre du virkelig sparer en masse båndbredde ved at filtrere sådanne detaljer fra, så bør du ikke engang prøve, eller i det mindste ud over det første positionelle match.
MongoDB har en positionel $
operator, som returnerer et array-element ved det matchede indeks fra en forespørgselsbetingelse. Dette returnerer dog kun det "første" matchede indeks for det "ydre" mest array-element.
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$': 1 }
)
I dette tilfælde betyder det "stores"
kun array-position. Så hvis der var flere "butikker"-poster, ville kun "én" af de elementer, der indeholdt din matchede betingelse, blive returneret. Men , der ikke gør noget for den indre række af "offers"
, og som sådan alle "tilbud" i de matchede "stores"
array ville stadig blive returneret.
MongoDB har ingen mulighed for at "filtrere" dette i en standardforespørgsel, så følgende virker ikke:
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$.offers.$': 1 }
)
De eneste værktøjer, MongoDB faktisk har til at udføre dette niveau af manipulation, er med aggregeringsrammen. Men analysen skulle vise dig, hvorfor du "sandsynligvis" ikke burde gøre dette, og i stedet bare filtrere arrayet i kode.
I rækkefølgen af, hvordan du kan opnå dette pr. version.
Først med MongoDB 3.2.x ved at bruge $filter
operation:
db.getCollection('retailers').aggregate([
{ "$match": { "stores.offers.size": "L" } },
{ "$project": {
"stores": {
"$filter": {
"input": {
"$map": {
"input": "$stores",
"as": "store",
"in": {
"_id": "$$store._id",
"offers": {
"$filter": {
"input": "$$store.offers",
"as": "offer",
"cond": {
"$setIsSubset": [ ["L"], "$$offer.size" ]
}
}
}
}
}
},
"as": "store",
"cond": { "$ne": [ "$$store.offers", [] ]}
}
}
}}
])
Derefter med MongoDB 2.6.x og derover med $map
og $setDifference
:
db.getCollection('retailers').aggregate([
{ "$match": { "stores.offers.size": "L" } },
{ "$project": {
"stores": {
"$setDifference": [
{ "$map": {
"input": {
"$map": {
"input": "$stores",
"as": "store",
"in": {
"_id": "$$store._id",
"offers": {
"$setDifference": [
{ "$map": {
"input": "$$store.offers",
"as": "offer",
"in": {
"$cond": {
"if": { "$setIsSubset": [ ["L"], "$$offer.size" ] },
"then": "$$offer",
"else": false
}
}
}},
[false]
]
}
}
}
},
"as": "store",
"in": {
"$cond": {
"if": { "$ne": [ "$$store.offers", [] ] },
"then": "$$store",
"else": false
}
}
}},
[false]
]
}
}}
])
Og endelig i enhver version over MongoDB 2.2.x hvor aggregeringsrammen blev indført.
db.getCollection('retailers').aggregate([
{ "$match": { "stores.offers.size": "L" } },
{ "$unwind": "$stores" },
{ "$unwind": "$stores.offers" },
{ "$match": { "stores.offers.size": "L" } },
{ "$group": {
"_id": {
"_id": "$_id",
"storeId": "$stores._id",
},
"offers": { "$push": "$stores.offers" }
}},
{ "$group": {
"_id": "$_id._id",
"stores": {
"$push": {
"_id": "$_id.storeId",
"offers": "$offers"
}
}
}}
])
Lad os nedbryde forklaringerne.
MongoDB 3.2.x og nyere
Så generelt set $filter
er vejen at gå her, da det er designet med formålet for øje. Da der er flere niveauer af arrayet, skal du anvende dette på hvert niveau. Så først dykker du ned i hver "offers"
i "stores"
for at undersøge og $filter
det indhold.
Den simple sammenligning her er "Er "size"
array indeholder det element, jeg leder efter" . I denne logiske sammenhæng er den korte ting at gøre at bruge $setIsSubset
operation for at sammenligne et array ("sæt") af ["L"]
til målarrayet. Hvor denne betingelse er true
( den indeholder "L") derefter array-elementet for "offers"
beholdes og returneres i resultatet.
På det højere niveau $filter
, leder du derefter efter at se, om resultatet fra det tidligere $filter
returnerede et tomt array []
for "offers"
. Hvis det ikke er tomt, returneres elementet, eller på anden måde fjernes det.
MongoDB 2.6.x
Dette er meget lig den moderne proces bortset fra, at da der ikke er noget $filter
i denne version kan du bruge $map
for at inspicere hvert element og derefter bruge $setDifference
for at bortfiltrere alle elementer, der blev returneret som false
.
Så $map
vil returnere hele arrayet, men $cond
operationen bestemmer bare, om elementet skal returneres eller i stedet for en false
værdi. I sammenligningen af $setDifference
til et enkelt element "sæt" af [false]
alle false
elementer i det returnerede array ville blive fjernet.
På alle andre måder er logikken den samme som ovenfor.
MongoDB 2.2.x og nyere
Så under MongoDB 2.6 er det eneste værktøj til at arbejde med arrays $unwind
, og alene til dette formål bør du ikke bruge aggregeringsrammen "bare" til dette formål.
Processen ser faktisk simpel ud, ved blot at "adskille" hvert array, filtrere de ting fra, du ikke har brug for, og derefter sætte det sammen igen. Den primære behandling er i "to" $group
stadier, med den "første" til at genopbygge det indre array, og den næste til at genopbygge det ydre array. Der er forskellige _id
værdier på alle niveauer, så disse skal blot inkluderes på alle grupperingsniveauer.
Men problemet er, at $unwind
er meget dyrt . Selvom det stadig har et formål, er dets hovedhensigt ikke at udføre denne form for filtrering pr. dokument. Faktisk i moderne udgivelser bør det kun bruges, når et element i arrayet(erne) skal blive en del af selve "grupperingsnøglen".
Konklusion
Så det er ikke en simpel proces at få matches på flere niveauer af et array som dette, og det kan faktisk være ekstremt dyrt hvis implementeret forkert.
Kun de to moderne fortegnelser bør nogensinde bruges til dette formål, da de anvender et "enkelt" pipelinetrin ud over "forespørgslen" $match
for at udføre "filtreringen". Den resulterende effekt er lidt mere overhead end standardformerne for .find()
.
Generelt har disse fortegnelser dog stadig en del kompleksitet i sig, og med mindre du virkelig drastisk reducerer det indhold, der returneres af en sådan filtrering på en måde, der giver en betydelig forbedring af den båndbredde, der bruges mellem serveren og klienten, så er du bedre filtrering af resultatet af den indledende forespørgsel og grundlæggende projektion.
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$': 1 }
).forEach(function(doc) {
// Technically this is only "one" store. So omit the projection
// if you wanted more than "one" match
doc.stores = doc.stores.filter(function(store) {
store.offers = store.offers.filter(function(offer) {
return offer.size.indexOf("L") != -1;
});
return store.offers.length != 0;
});
printjson(doc);
})
Så arbejdet med det returnerede objekt "efter"-forespørgselsbehandling er langt mindre besværligt end at bruge aggregeringspipelinen til at gøre dette. Og som nævnt ville den eneste "rigtige" forskel være, at du kasserer de andre elementer på "serveren" i modsætning til at fjerne dem "pr. dokument", når de modtages, hvilket kan spare lidt båndbredde.
Men medmindre du gør dette i en moderne udgivelse med kun $match
og $project
, så vil "omkostningerne" ved behandling på serveren i høj grad opveje "gevinsten" ved at reducere netværksomkostningerne ved først at fjerne de uovertrufne elementer.
I alle tilfælde får du det samme resultat:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size" : [
"S",
"L",
"XL"
]
}
]
}
]
}