sql >> Base de Datos >  >> NoSQL >> MongoDB

Devuelve solo los elementos del subdocumento coincidentes dentro de una matriz anidada

Entonces, la consulta que tiene en realidad selecciona el "documento" como debería. Pero lo que busca es "filtrar las matrices" contenidas 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 esos detalles, ni siquiera debería intentarlo, o al menos más allá de la primera coincidencia posicional.

MongoDB tiene un $ posicional operador 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, se refiere a las "stores" solo la posición de la matriz. Entonces, 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 por la matriz interna de "offers" , y como tal cada "oferta" dentro de las "stores" coincidentes aún se devolvería la matriz.

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 hacer este nivel de manipulación es 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 del $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 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 superior 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.x y superior

En términos generales, $filter es el camino a seguir aquí ya que está diseñado con el propósito en mente. Dado que hay múltiples niveles de la matriz, debe aplicar esto en cada nivel. Así que primero estás sumergiéndote en cada "offers" dentro de "stores" para examinar y $filter ese contenido.

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

En el nivel superior $filter , entonces está buscando para 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 como no hay $filter en esta versión puedes 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 la matriz completa, pero el $cond la operación simplemente decide si devolver el elemento o en su lugar un false valor. En la comparación de $setDifference a un solo elemento "conjunto" de [false] todo false los elementos en la matriz devuelta serían eliminados.

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

MongoDB 2.2.x y superior

Entonces, debajo de MongoDB 2.6, la única herramienta para trabajar con matrices es $unwind , y solo con este propósito, no use 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 necesita y luego volviéndolas a armar. El cuidado principal está en los "dos" $group etapas, con la "primera" para reconstruir la matriz interna y la siguiente para reconstruir la matriz externa. Hay distintos _id valores 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 principal intención de uso es no 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" en sí misma.

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 las dos listas modernas deben 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 general que las formas estándar de .find() .

Sin embargo, en general, esos listados aún tienen cierta complejidad y, de hecho, a menos que 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 está mejor. de filtrar el resultado de la consulta inicial y 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 consulta "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 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"
                                        ]
                                }
                        ]
                }
        ]
}