Quindi la query che hai effettivamente seleziona il "documento" proprio come dovrebbe. Ma quello che stai cercando è "filtrare gli array" contenuti in modo che gli elementi restituiti corrispondano solo alle condizioni della query.
La vera risposta è ovviamente che, a meno che tu non stia davvero risparmiando molta larghezza di banda filtrando tali dettagli, non dovresti nemmeno provare, o almeno oltre la prima corrispondenza posizionale.
MongoDB ha un $
posizionale operatore che restituirà un elemento dell'array in corrispondenza dell'indice corrispondente da una condizione di query. Tuttavia, questo restituisce solo il "primo" indice corrispondente dell'elemento più "esterno" dell'array.
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$': 1 }
)
In questo caso, significa il "stores"
solo posizione dell'array. Quindi, se ci fossero più voci "negozi", verrebbe restituito solo "uno" degli elementi che contenevano la tua condizione corrispondente. Ma , che non fa nulla per l'array interno di "offers"
e come tale ogni "offerta" all'interno dei "stores"
corrispondenti l'array verrebbe comunque restituito.
MongoDB non ha modo di "filtrarlo" in una query standard, quindi quanto segue non funziona:
db.getCollection('retailers').find(
{ 'stores.offers.size': 'L'},
{ 'stores.$.offers.$': 1 }
)
Gli unici strumenti che MongoDB ha effettivamente per eseguire questo livello di manipolazione è con il framework di aggregazione. Ma l'analisi dovrebbe mostrarti perché "probabilmente" non dovresti farlo e invece filtrare semplicemente l'array nel codice.
In ordine di come puoi ottenere questo per versione.
Innanzitutto con MongoDB 3.2.x utilizzando il $filter
operazione:
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", [] ]}
}
}
}}
])
Quindi con MongoDB 2.6.x e superiori con $map
e $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]
]
}
}}
])
E infine in qualsiasi versione precedente a MongoDB 2.2.x dove è stato introdotto il framework di aggregazione.
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"
}
}
}}
])
Analizziamo le spiegazioni.
MongoDB 3.2.xe versioni successive
Quindi, in generale, $filter
è la strada da percorrere qui poiché è progettato con lo scopo in mente. Poiché ci sono più livelli dell'array, è necessario applicarlo a ciascun livello. Quindi prima ti immergi in ogni "offers"
all'interno di "stores"
per esaminare e $filter
quel contenuto.
Il semplice confronto qui è "Does the "size"
array contengono l'elemento che sto cercando" . In questo contesto logico, la cosa più breve da fare è usare $setIsSubset
operazione per confrontare un array ("set") di ["L"]
all'array di destinazione. Dove tale condizione è true
( contiene "L" ) quindi l'elemento dell'array per "offers"
viene conservato e restituito nel risultato.
Nel livello superiore $filter
, stai quindi cercando di vedere se il risultato di quel $filter
precedente ha restituito un array vuoto []
per "offers"
. Se non è vuoto, l'elemento viene restituito o altrimenti viene rimosso.
MongoDB 2.6.x
Questo è molto simile al processo moderno tranne per il fatto che non esiste $filter
in questa versione puoi usare $map
per ispezionare ogni elemento e quindi utilizzare $setDifference
per filtrare tutti gli elementi che sono stati restituiti come false
.
Quindi $map
restituirà l'intero array, ma il $cond
l'operazione decide solo se restituire l'elemento o invece un false
valore. Nel confronto di $setDifference
a un singolo elemento "set" di [false]
tutto false
gli elementi nell'array restituito verrebbero rimossi.
In tutti gli altri modi, la logica è la stessa di cui sopra.
MongoDB 2.2.xe versioni successive
Quindi sotto MongoDB 2.6 l'unico strumento per lavorare con gli array è $unwind
, e solo per questo scopo non utilizzare il framework di aggregazione "just" per questo scopo.
Il processo sembra davvero semplice, semplicemente "smontando" ogni array, filtrando le cose che non ti servono e poi rimontandolo. La cura principale è nel "due" $group
fasi, con il "primo" per ricostruire l'array interno e il successivo per ricostruire l'array esterno. Esistono _id
distinti valori a tutti i livelli, quindi devono essere inclusi a tutti i livelli di raggruppamento.
Ma il problema è che $unwind
è molto costoso . Sebbene abbia ancora uno scopo, il suo principale intento di utilizzo non è quello di eseguire questo tipo di filtraggio per documento. In effetti, nelle versioni moderne, dovrebbe essere utilizzato solo quando un elemento degli array deve diventare parte della "chiave di raggruppamento" stessa.
Conclusione
Quindi non è un processo semplice ottenere corrispondenze a più livelli di un array come questo, e infatti può essere estremamente costoso se implementato in modo errato.
Solo i due elenchi moderni dovrebbero essere utilizzati per questo scopo, poiché utilizzano una fase di pipeline "singola" oltre alla "query" $match
per fare il "filtraggio". L'effetto risultante è un po' più sovraccarico rispetto alle forme standard di .find()
.
In generale, tuttavia, tali elenchi hanno ancora una certa complessità e, in effetti, a meno che non si stia riducendo drasticamente il contenuto restituito da tale filtraggio in un modo che renda un miglioramento significativo della larghezza di banda utilizzata tra il server e il client, allora è meglio di filtrare il risultato della query iniziale e della proiezione di base.
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);
})
Quindi lavorare con l'elaborazione della query "post" dell'oggetto restituito è molto meno ottuso rispetto all'utilizzo della pipeline di aggregazione per farlo. E come affermato, l'unica differenza "reale" sarebbe che stai scartando gli altri elementi sul "server" invece di rimuoverli "per documento" una volta ricevuti, il che potrebbe far risparmiare un po' di larghezza di banda.
Ma a meno che tu non lo stia facendo in una versione moderna con solo $match
e $project
, quindi il "costo" di elaborazione sul server supererà di gran lunga il "guadagno" derivante dalla riduzione del sovraccarico di rete eliminando prima gli elementi non corrispondenti.
In tutti i casi ottieni lo stesso risultato:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size" : [
"S",
"L",
"XL"
]
}
]
}
]
}