Devuelve solo elementos de sub-documento coincidentes dentro de un array anidado


La colección principal es retailer, que contiene una matriz para tiendas. Cada tienda contiene una serie de ofertas (se puede comprar en esta tienda). Esta matriz de ofertas tiene una matriz de tamaños. (Véase el 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"
                ]
            }
        ]
    }
}

He intentado esta consulta: db.getCollection('retailers').find({'stores.offers.size': 'L'})

Espero alguna 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 size XS, X y M.

Cómo ¿puede obligar a MongoDB a devolver solo las ofertas que coincidan con mi consulta?

Saludos y gracias.

Author: Blakes Seven, 2016-03-26

2 answers

Así que la consulta que tienes selecciona el "documento" como debería. Pero lo que está buscando es "filtrar los arrays" contenidos para que los elementos devueltos solo coincidan con la condición de la consulta.

La verdadera respuesta es, por supuesto, que a menos que realmente esté ahorrando mucho ancho de banda al filtrar tales detalles, ni siquiera debería intentarlo, o al menos más allá de la primera coincidencia posicional.

MongoDB tiene un operador posicional $ que devuelve un elemento array en el índice coincidente desde una condición de consulta. Sin embargo, esto solo devuelve el índice "first" coincidente del elemento "outer" most array.

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
)

En este caso, significa solo la posición del array "stores". Por lo tanto, si hubiera varias entradas "stores", 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, cada "oferta" dentro de la matriz matchd "stores" seguiría ser devuelto.

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 en su lugar simplemente filtrar la matriz en código.


En orden de cómo se puede lograr esto por versión.

Primero con MongoDB 3.2.x con el uso de la $filter operación:

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.x y encima 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 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.x y superior

Por Lo general, $filter es el camino a seguir aquí, ya que está diseñado con el propósito en mente. Ya hay varios niveles de la matriz, es necesario aplicar esto en cada nivel. Así que primero estás buceando 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 corto a hacer es utilizar el $setIsSubset operación para comparar un array ("set") de ["L"] con el array de destino. Donde esa condición es true (contiene " L " ) entonces la matriz el elemento para "offers" se conserva y se devuelve en el resultado.

En el nivel superior $filter, entonces está buscando ver si el resultado de ese anterior $filter devolvió un array vacío [] para "offers". Si no está vacío, el elemento se devuelve o se elimina.

MongoDB 2.6.x

Esto es muy similar al proceso moderno, excepto que ya que no hay $filter en esta versión se puede utilizar $map para inspeccionar cada elemento y luego usar $setDifference para filtrar cualquier elemento que haya sido devuelto como false.

Así que $map va a devolver toda la matriz, pero la operación $cond solo decide si devolver el elemento o en su lugar un valor false. En la comparación de $setDifference a un solo elemento "set" de [false] todos los elementos false en el array devuelto serían eliminados.

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

MongoDB 2.2.x y arriba

Así que por debajo de MongoDB 2.6 el único herramienta para trabajar con matrices es $unwind, y solo para este propósito debería no usar el marco de agregación "just" para este propósito.

El proceso de hecho parece simple, simplemente "desmontando" cada matriz, filtrando las cosas que no necesita y luego poniéndolas de nuevo juntas. El cuidado principal está en los "dos" $group etapas, con el "primero" para reconstruir la matriz interna, y el siguiente para reconstruir la matriz externa. Hay distintos valores _id en todos los niveles, por lo que estos solo tienen que ser incluidos en todos los niveles de agrupación.

Pero el problema es que $unwind es muy costoso. Aunque todavía tiene un propósito, su principal intención de uso no es hacer este tipo de filtrado por documento. De hecho, en las versiones modernas, su uso solo debería ser cuando un elemento de la matriz(s) necesita convertirse en parte de la "clave de agrupación" en sí.


Conclusión

Así que no es un proceso simple obtener coincidencias en múltiples niveles de un array como este, 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 poco más elevado que las formas estándar de .find().

En general, sin embargo, esos listados todavía tienen una cantidad de complejidad para ellos, y de hecho, a menos que realmente reduciendo drásticamente el contenido devuelto por dicho filtrado de una manera que hace una mejora significativa en el ancho de banda utilizado entre el servidor y el cliente, entonces es mejor 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);
})

Así que trabajar con el procesamiento de consulta "post" del objeto devuelto es mucho menos obtuso que usar la canalización de agregación para hacer esto. Y como se ha dicho la única difrencia "real" sería que estás 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 al eliminar los elementos incomparables primero.

En todos los casos, se obtiene el mismo resultado:{[46]]}

{
        "_id" : ObjectId("56f277b1279871c20b8b4567"),
        "stores" : [
                {
                        "_id" : ObjectId("56f277b5279871c20b8b4783"),
                        "offers" : [
                                {
                                        "_id" : ObjectId("56f277b1279871c20b8b4567"),
                                        "size" : [
                                                "S",
                                                "L",
                                                "XL"
                                        ]
                                }
                        ]
                }
        ]
}
 86
Author: Blakes Seven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-08 18:18:42

Como su matriz está incrustada, no podemos usar $elemMatch, en su lugar puede usar aggregation framework 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 group group y ver cómo se imprime. ¡Diviértete!

 9
Author: profesor79,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-26 00:35:21