Devuelve solo elementos de sub-documento coincidentes dentro de una matriz anidada

La colección principal es minorista, que contiene una matriz para tiendas. Cada tienda contiene una variedad de ofertas (puedes comprar en esta tienda). Esta matriz 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 el tamaño 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 alguna salida así:

  { "_id" : ObjectId("56f277b1279871c20b8b4567"), "stores" : [ { "_id" : ObjectId("56f277b5279871c20b8b4783"), "offers" : [ { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size": [ "S", "L", "XL" ] } ] } } 

Pero el resultado de mi consulta contiene también la oferta que no coincide con el size XS, X y M.

¿Cómo puedo obligar a MongoDB a devolver solo las ofertas, que coinciden 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 una gran cantidad de ancho de banda filtrando esos detalles, entonces 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 desde 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 del conjunto de "stores" solamente. Entonces, si hubiera múltiples entradas de “tiendas”, entonces solo se devolvería “uno” de los elementos que contenían su condición coincidente. Pero eso no hace nada para el conjunto interno de "offers" , y como tal, toda “oferta” dentro del conjunto de "stores" combinadas todavía sería devuelta.

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 tiene MongoDB para 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 en su lugar simplemente filtrar la matriz en el 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 de 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" } } }} ]) 

Vamos a desglosar las explicaciones.

MongoDB 3.2.xy mayor

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 sumerge en cada "offers" dentro de "stores" para examinar y $filter ese contenido.

La comparación simple aquí es “¿El array "size" contiene el elemento que estoy buscando” . En este contexto lógico, lo más breve es utilizar la operación $setIsSubset para comparar una matriz (“conjunto”) de ["L"] con la matriz de destino. Cuando esa condición es true (contiene “L”), el elemento de matriz para "offers" se conserva y se devuelve en el resultado.

En el $filter nivel más alto, usted está buscando para ver si el resultado de ese $filter previo 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 como 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 .

Así que $map va a devolver todo el conjunto, pero la operación $cond solo decide si devolver el elemento o en su lugar un valor false . En la comparación de $setDifference con un único elemento “set” de [false] , se eliminarán todos false elementos false en la matriz devuelta.

En todos los demás aspectos, la lógica es la misma que la anterior.

MongoDB 2.2.xy superior

Por lo tanto, debajo de MongoDB 2.6, la única herramienta para trabajar con matrices es $unwind , y para este propósito, por sí solo, no debe usar el marco de agregación “solo” para este propósito.

De hecho, el proceso parece simple, simplemente “desarmando” cada matriz, filtrando las cosas que no necesitas y luego volviendo a unirlas. El cuidado principal se encuentra en las “dos” etapas de $group , con el “primero” para reconstruir el conjunto interno y el siguiente para reconstruir el conjunto externo. Existen distintos valores de _id en todos los niveles, por lo que 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 principal de uso no es hacer este tipo de filtrado por documento. De hecho, en las versiones modernas, solo debe usarse cuando un elemento de la (s) matriz (es) necesita convertirse en parte de la “clave de agrupación”.


Conclusión

Por lo tanto, no es un proceso simple obtener coincidencias en varios niveles de una matriz como esta, y de hecho puede ser extremadamente costoso si se implementa incorrectamente.

Solo se deben usar los dos listados modernos para este fin, ya que emplean una etapa de canalización “única” además de la $match “consulta” para realizar el “filtrado”. El efecto resultante es un poco más elevado que las formas estándar de .find() .

Sin embargo, en general, esos listados aún tienen una cierta complejidad para ellos, y de hecho, a menos que estés 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 eres 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” de objeto devuelto es mucho menos obtuso que usar la canalización de agregación para hacerlo. Y como se dijo, 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” de procesamiento en el servidor superará en gran medida la “ganancia” de reducir esa sobrecarga de red eliminando primero los elementos no coincidentes.

En todos los casos, obtienes el mismo resultado:

 { "_id" : ObjectId("56f277b1279871c20b8b4567"), "stores" : [ { "_id" : ObjectId("56f277b5279871c20b8b4783"), "offers" : [ { "_id" : ObjectId("56f277b1279871c20b8b4567"), "size" : [ "S", "L", "XL" ] } ] } ] } 

como su matriz está incorporada, 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 coincide con el tamaño y luego cambia la forma del documento a la forma anterior. Puede eliminar los pasos de $ group y ver cómo se imprime. ¡Pasarlo bien!