mongodb - anidadas - Devuelve solo elementos de subdocumentos coincidentes dentro de una matriz anidada
consultas en mongodb (2)
La colección principal es minorista, que contiene una matriz para tiendas. Cada tienda contiene una variedad de ofertas (puedes comprar en esta tienda). Este conjunto de ofertas tiene una variedad de tamaños. (Ver ejemplo a continuación)
Ahora trato de encontrar todas las ofertas, que están disponibles en la talla
L
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"XS",
"S",
"M"
]
},
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"S",
"L",
"XL"
]
}
]
}
}
db.getCollection(''retailers'').find({''stores.offers.size'': ''L''})
esta consulta:
db.getCollection(''retailers'').find({''stores.offers.size'': ''L''})
Espero una salida como esa:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size": [
"S",
"L",
"XL"
]
}
]
}
}
Pero la salida de mi consulta contiene también la oferta no coincidente con los
size
XS, X y M.
¿Cómo puedo obligar a MongoDB a devolver solo las ofertas, que coincidieron con mi consulta?
Saludos y gracias.
Entonces, la consulta que tiene realmente selecciona el "documento" como debería. Pero lo que está buscando es "filtrar las matrices" contenidas para que los elementos devueltos solo coincidan con la condición de la consulta.
La respuesta real es, por supuesto, que a menos que realmente esté ahorrando mucho ancho de banda al filtrar esos detalles, ni siquiera debería intentarlo, o al menos más allá de la primera coincidencia posicional.
MongoDB tiene un
operador posicional
$
que devolverá un elemento de matriz en el índice coincidente de una condición de consulta.
Sin embargo, esto solo devuelve el "primer" índice coincidente del elemento de matriz más "externo".
db.getCollection(''retailers'').find(
{ ''stores.offers.size'': ''L''},
{ ''stores.$'': 1 }
)
En este caso, significa la posición de la matriz
"stores"
solamente.
Por lo tanto, si hubiera varias entradas de "tiendas", solo se devolvería "uno" de los elementos que contenían su condición coincidente.
Pero
eso no hace nada para la matriz interna de
"offers"
y, como tal, toda "oferta" dentro de la matriz
"stores"
coincidente aún se devolvería.
MongoDB no tiene forma de "filtrar" esto en una consulta estándar, por lo que lo siguiente no funciona:
db.getCollection(''retailers'').find(
{ ''stores.offers.size'': ''L''},
{ ''stores.$.offers.$'': 1 }
)
Las únicas herramientas que MongoDB realmente tiene para hacer este nivel de manipulación es con el marco de agregación. Pero el análisis debería mostrarle por qué "probablemente" no debería hacer esto, y simplemente filtre la matriz en código.
En orden de cómo puede lograr esto por versión.
Primero con
MongoDB 3.2.x
con el uso de la operación
$filter
:
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", [] ]}
}
}
}}
])
Luego con
MongoDB 2.6.xy
superior con
$map
y
$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]
]
}
}}
])
Y finalmente en cualquier versión anterior a MongoDB 2.2.x donde se introdujo el marco de agregación.
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"
}
}
}}
])
Analicemos las explicaciones.
MongoDB 3.2.xy superior
En términos generales,
$filter
es el camino a seguir aquí, ya que está diseñado con el propósito en mente.
Como hay varios niveles de la matriz, debe aplicar esto en cada nivel.
Entonces, primero se está sumergiendo en cada
"offers"
dentro de las
"stores"
para examinar y
$filter
ese contenido.
La comparación simple aquí es
"¿La matriz
"size"
contiene el elemento que estoy buscando"
.
En este contexto lógico, lo más breve es usar la operación
$setIsSubset
para comparar una matriz ("set") de
["L"]
con la matriz de destino.
Cuando esa condición es
true
(contiene "L"), el elemento de matriz para
"offers"
se retiene y se devuelve en el resultado.
En el
$filter
nivel superior, entonces está buscando ver si el resultado de ese
$filter
anterior devolvió una matriz vacía
[]
para
"offers"
.
Si no está vacío, se devuelve el elemento o, de lo contrario, se elimina.
MongoDB 2.6.x
Esto es muy similar al proceso moderno, excepto que dado que no hay
$filter
en esta versión, puede usar
$map
para inspeccionar cada elemento y luego usar
$setDifference
para filtrar los elementos que se devolvieron como
false
.
Entonces
$map
devolverá toda la matriz, pero la operación
$cond
simplemente decide si devolverá el elemento o, en su lugar, un valor
false
.
En la comparación de
$setDifference
con un solo elemento "set" de
[false]
se eliminarían todos
false
elementos
false
en la matriz devuelta.
En todas las demás formas, la lógica es la misma que la anterior.
MongoDB 2.2.xy superior
Entonces, debajo de MongoDB 2.6, la única herramienta para trabajar con matrices es
$unwind
, y solo para este propósito no debe usar el marco de agregación "solo" para este propósito.
El proceso de hecho parece simple, simplemente "desarmando" cada matriz, filtrando las cosas que no necesita y luego volviéndolas a armar.
La atención principal se encuentra en las "dos" etapas del
$group
, con el "primero" para reconstruir la matriz interna y la siguiente para reconstruir la matriz externa.
Hay valores
_id
distintos en todos los niveles, por lo que estos solo deben incluirse en cada nivel de agrupación.
Pero el problema es que
$unwind
es
muy costoso
.
Aunque todavía tiene un propósito, su intención de uso principal es no hacer este tipo de filtrado por documento.
De hecho, en las versiones modernas, solo se debe usar cuando un elemento de la (s) matriz (s) debe formar parte de la "clave de agrupación".
Conclusión
Por lo tanto, no es un proceso simple obtener coincidencias en múltiples niveles de una matriz como esta, y de hecho puede ser extremadamente costoso si se implementa incorrectamente.
Solo los dos listados modernos deberían usarse para este propósito, ya que emplean una etapa de canalización "única" además de la "consulta"
$match
para hacer el "filtrado".
El efecto resultante es un poco más sobrecarga que las formas estándar de
.find()
.
Sin embargo, en general, esos listados aún tienen una cantidad de complejidad, y de hecho, a menos que realmente esté reduciendo drásticamente el contenido devuelto por dicho filtrado de una manera que mejore significativamente el ancho de banda utilizado entre el servidor y el cliente, entonces será mejor de filtrar el resultado de la consulta inicial y la proyección básica.
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);
})
Por lo tanto, trabajar con el procesamiento de consultas "post" del objeto devuelto es mucho menos obtuso que usar la canalización de agregación para hacer esto. Y como se indicó, la única diferencia "real" sería que está descartando los otros elementos en el "servidor" en lugar de eliminarlos "por documento" cuando se reciben, lo que puede ahorrar un poco de ancho de banda.
Pero a menos que esté haciendo esto en una versión moderna con
solo
$match
y
$project
, entonces el "costo" del procesamiento en el servidor superará en gran medida la "ganancia" de reducir la sobrecarga de la red quitando primero los elementos incomparables.
En todos los casos, obtienes el mismo resultado:
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
{
"_id" : ObjectId("56f277b5279871c20b8b4783"),
"offers" : [
{
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"size" : [
"S",
"L",
"XL"
]
}
]
}
]
}
a medida que su matriz está incrustada, no podemos usar $ elemMatch, en su lugar puede usar el marco de agregación para obtener sus resultados:
db.retailers.aggregate([
{$match:{"stores.offers.size": ''L''}}, //just precondition can be skipped
{$unwind:"$stores"},
{$unwind:"$stores.offers"},
{$match:{"stores.offers.size": ''L''}},
{$group:{
_id:{id:"$_id", "storesId":"$stores._id"},
"offers":{$push:"$stores.offers"}
}},
{$group:{
_id:"$_id.id",
stores:{$push:{_id:"$_id.storesId","offers":"$offers"}}
}}
]).pretty()
lo que hace esta consulta es desenrollar matrices (dos veces), luego hace coincidir el tamaño y luego cambia la forma del documento al formulario anterior. Puede eliminar $ group steps y ver cómo se imprime. ¡Pasarlo bien!