Recupere solo el elemento consultado en una matriz de objetos en la colección MongoDB

377

Supongamos que tiene los siguientes documentos en mi colección:

{  
   "_id":ObjectId("562e7c594c12942f08fe4192"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"blue"
      },
      {  
         "shape":"circle",
         "color":"red"
      }
   ]
},
{  
   "_id":ObjectId("562e7c594c12942f08fe4193"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"black"
      },
      {  
         "shape":"circle",
         "color":"green"
      }
   ]
}

Hacer consulta:

db.test.find({"shapes.color": "red"}, {"shapes.color": 1})

O

db.test.find({shapes: {"$elemMatch": {color: "red"}}}, {"shapes.color": 1})

Devuelve el documento coincidente (Documento 1) , pero siempre con TODOS los elementos de la matriz en shapes:

{ "shapes": 
  [
    {"shape": "square", "color": "blue"},
    {"shape": "circle", "color": "red"}
  ] 
}

Sin embargo, me gustaría obtener el documento (Documento 1) solo con la matriz que contiene color=red:

{ "shapes": 
  [
    {"shape": "circle", "color": "red"}
  ] 
}

¿Cómo puedo hacer esto?

Sebtm
fuente

Respuestas:

416

El nuevo $elemMatchoperador de proyección de MongoDB 2.2 proporciona otra forma de alterar el documento devuelto para contener solo el primershapes elemento coincidente :

db.test.find(
    {"shapes.color": "red"}, 
    {_id: 0, shapes: {$elemMatch: {color: "red"}}});

Devoluciones:

{"shapes" : [{"shape": "circle", "color": "red"}]}

En 2.2 también puede hacer esto usando $ projection operator, donde el $nombre del campo en un objeto de proyección representa el índice del primer elemento de matriz coincidente del campo de la consulta. Lo siguiente devuelve los mismos resultados que arriba:

db.test.find({"shapes.color": "red"}, {_id: 0, 'shapes.$': 1});

Actualización de MongoDB 3.2

A partir de la versión 3.2, puede usar el nuevo $filteroperador de agregación para filtrar una matriz durante la proyección, que tiene la ventaja de incluir todas las coincidencias, en lugar de solo la primera.

db.test.aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
    {$match: {'shapes.color': 'red'}},
    {$project: {
        shapes: {$filter: {
            input: '$shapes',
            as: 'shape',
            cond: {$eq: ['$$shape.color', 'red']}
        }},
        _id: 0
    }}
])

Resultados:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]
JohnnyHK
fuente
16
¿alguna solución si quiero que devuelva todos los elementos que coincidan en lugar de solo el primero?
Steve Ng
Me temo que estoy usando Mongo 3.0.X :-(
charliebrownie
@charliebrownie Luego usa una de las otras respuestas que usa aggregate.
JohnnyHK
esta consulta solo devuelve las "formas" de la matriz y no devolverá otros campos. ¿Alguien sabe cómo devolver otros campos también?
Mark Thien
1
Esto también funciona:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paul
97

El nuevo Marco de agregación en MongoDB 2.2+ proporciona una alternativa a Map / Reduce. El $unwindoperador se puede utilizar para separar su shapesmatriz en una secuencia de documentos que pueden coincidir:

db.test.aggregate(
  // Start with a $match pipeline which can take advantage of an index and limit documents processed
  { $match : {
     "shapes.color": "red"
  }},
  { $unwind : "$shapes" },
  { $match : {
     "shapes.color": "red"
  }}
)

Resultados en:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}
Stennie
fuente
77
@JohnnyHK: en este caso, $elemMatches otra opción. De hecho, llegué aquí a través de una pregunta de Google Group donde $ elemMatch no funcionaría porque solo devuelve la primera coincidencia por documento.
Stennie
1
Gracias, no estaba al tanto de esa limitación, así que es bueno saberlo. Perdón por eliminar mi comentario al que estás respondiendo, decidí publicar otra respuesta y no quería confundir a las personas.
JohnnyHK
3
@JohnnyHK: No se preocupe, ahora hay tres respuestas útiles para la pregunta ;-)
Stennie
Para otros buscadores, además de esto, también traté de agregar { $project : { shapes : 1 } }, lo que parecía funcionar y sería útil si los documentos adjuntos fueran grandes y solo quisiera ver los shapesvalores clave.
user1063287
2
@calmbird Actualicé el ejemplo para incluir una etapa inicial de $ match. Si está interesado en una sugerencia de función más eficiente, miraría / votaría SERVER-6612: soporte para proyectar múltiples valores de matriz en una proyección como el especificador de proyección $ elemMatch en el rastreador de problemas MongoDB.
Stennie
30

Otra forma interesante es usar $ redact , que es una de las nuevas características de agregación de MongoDB 2.6 . Si está utilizando 2.6, no necesita un desenrollado $ que podría causar problemas de rendimiento si tiene matrices grandes.

db.test.aggregate([
    { $match: { 
         shapes: { $elemMatch: {color: "red"} } 
    }},
    { $redact : {
         $cond: {
             if: { $or : [{ $eq: ["$color","red"] }, { $not : "$color" }]},
             then: "$$DESCEND",
             else: "$$PRUNE"
         }
    }}]);

$redact "restringe el contenido de los documentos en función de la información almacenada en los propios documentos" . Por lo tanto, se ejecutará solo dentro del documento . Básicamente escanea su documento de arriba a abajo y verifica si coincide con su ifcondición $cond, si existe una coincidencia, mantendrá el contenido ( $$DESCEND) o eliminará ( $$PRUNE).

En el ejemplo anterior, primero $matchdevuelve toda la shapesmatriz y $ redact la despoja al resultado esperado.

Tenga en cuenta que {$not:"$color"}es necesario, porque también escaneará el documento superior, y si $redactno encuentra un colorcampo en el nivel superior, esto devolverá falseque podría eliminar todo el documento que no queremos.

anvarik
fuente
1
Respuesta perfecta. Como mencionó, $ unwind consumirá mucha RAM. Entonces esto será mejor en comparación.
manojpt
Tengo una duda. En el ejemplo, "formas" es una matriz. ¿"$ Redact" escaneará todos los objetos en la matriz de "formas"? ¿Cómo será esto bueno con respecto al rendimiento?
manojpt
no todo, sino el resultado de tu primer partido. Esa es la razón por la que pones $matchcomo tu primera etapa agregada
anvarik
okkk .. si un índice creado en el campo "color", incluso entonces, ¿escaneará todos los objetos en la matriz de "formas"? ¿Cuál podría ser la forma eficiente de hacer coincidir varios objetos en una matriz?
manojpt
2
¡Brillante! No entiendo cómo funciona $ eq aquí. Lo dejé originalmente y esto no funcionó para mí. De alguna manera, busca en la matriz de formas para encontrar la coincidencia, pero la consulta nunca especifica en qué matriz buscar. Por ejemplo, si los documentos tienen formas y, por ejemplo, tamaños; ¿$ eq buscaría coincidencias en ambas matrices? ¿$ Redact solo está buscando algo dentro del documento que coincida con la condición 'if'?
Onosa
30

Precaución: esta respuesta proporciona una solución que era relevante en ese momento , antes de que se introdujeran las nuevas características de MongoDB 2.2 y versiones posteriores. Vea las otras respuestas si está utilizando una versión más reciente de MongoDB.

El parámetro del selector de campo está limitado a propiedades completas. No se puede usar para seleccionar parte de una matriz, solo toda la matriz. Intenté usar el operador posicional $ , pero eso no funcionó.

La forma más fácil es simplemente filtrar las formas en el cliente .

Si realmente necesita la salida correcta directamente de MongoDB, puede usar un mapa-reducir para filtrar las formas.

function map() {
  filteredShapes = [];

  this.shapes.forEach(function (s) {
    if (s.color === "red") {
      filteredShapes.push(s);
    }
  });

  emit(this._id, { shapes: filteredShapes });
}

function reduce(key, values) {
  return values[0];
}

res = db.test.mapReduce(map, reduce, { query: { "shapes.color": "red" } })

db[res.result].find()
Niels van der Rest
fuente
24

Es mejor que pueda consultar el elemento de matriz coincidente utilizando si $slicees útil devolver el objeto significativo en una matriz.

db.test.find({"shapes.color" : "blue"}, {"shapes.$" : 1})

$slicees útil cuando conoce el índice del elemento, pero a veces desea cualquier elemento de matriz que coincida con sus criterios. Puede devolver el elemento coincidente con el $operador.

Narendran
fuente
19
 db.getCollection('aj').find({"shapes.color":"red"},{"shapes.$":1})

SALIDAS

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}
Patel viral
fuente
12

La sintaxis para encontrar en mongodb es

    db.<collection name>.find(query, projection);

y la segunda consulta que has escrito, que es

    db.test.find(
    {shapes: {"$elemMatch": {color: "red"}}}, 
    {"shapes.color":1})

en esto ha utilizado el $elemMatchoperador en la parte de consulta, mientras que si utiliza este operador en la parte de proyección, obtendrá el resultado deseado. Puede escribir su consulta como

     db.users.find(
     {"shapes.color":"red"},
     {_id:0, shapes: {$elemMatch : {color: "red"}}})

Esto te dará el resultado deseado.

Vicky
fuente
1
Esto funciona para mi. Sin embargo, parece que "shapes.color":"red"en el parámetro de consulta (el primer parámetro del método find) no es necesario. Puede reemplazarlo {}y obtener los mismos resultados.
Erik Olson
2
@ErikOlson Su sugerencia es correcta en el caso anterior, donde necesitamos encontrar todo el documento con color rojo y aplicar la proyección solo en ellos. Pero digamos que si alguien necesita encontrar todos los documentos que tienen color azul, pero debería devolver solo aquellos elementos de esa matriz de formas que tienen color rojo. En este caso, la consulta anterior también puede ser referenciada por otra persona ..
Vicky
Esto parece ser lo más fácil, pero no puedo hacer que funcione. Solo devuelve el primer subdocumento coincidente.
newman
8

Gracias a JohnnyHK .

Aquí solo quiero agregar un uso más complejo.

// Document 
{ 
"_id" : 1
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 

{ 
"_id" : 2
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 


// The Query   
db.contents.find({
    "_id" : ObjectId(1),
    "shapes.color":"red"
},{
    "_id": 0,
    "shapes" :{
       "$elemMatch":{
           "color" : "red"
       } 
    }
}) 


//And the Result

{"shapes":[
    {
       "shape" : "square",
       "color" : "red"
    }
]}
Remolino
fuente
7

Solo necesitas ejecutar la consulta

db.test.find(
{"shapes.color": "red"}, 
{shapes: {$elemMatch: {color: "red"}}});

la salida de esta consulta es

{
    "_id" : ObjectId("562e7c594c12942f08fe4192"),
    "shapes" : [ 
        {"shape" : "circle", "color" : "red"}
    ]
}

como esperaba, dará el campo exacto de la matriz que coincide con el color: 'rojo'.

Vaibhav Patil
fuente
3

junto con $ project, será más apropiado que otros elementos acertados coincidan con otros elementos del documento.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)
shakthydoss
fuente
¿Puedes describir que esto se logra con un conjunto de entrada y salida?
Alexander Mills
2

Del mismo modo puedes encontrar para el múltiple

db.getCollection('localData').aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
  {$match: {'shapes.color': {$in : ['red','yellow'] } }},
  {$project: {
     shapes: {$filter: {
        input: '$shapes',
        as: 'shape',
        cond: {$in: ['$$shape.color', ['red', 'yellow']]}
     }}
  }}
])
ashishSober
fuente
Esta respuesta es, de hecho, la forma preferida 4.x: $matchpara reducir el espacio, luego $filterpara mantener lo que desea, sobrescribiendo el campo de entrada (use la salida de $filteron field shapespara $projectvolver a hacerlo shapes. Nota de estilo: mejor no usar el nombre del campo como el asargumento porque eso puede llevar a confusión más adelante con $$shapey $shape. Prefiero zzcomo el ascampo porque realmente se destaca.
Buzz Moschetti
1
db.test.find( {"shapes.color": "red"}, {_id: 0})
Poonam Agrawal
fuente
1
¡Bienvenido a Stack Overflow! Gracias por el fragmento de código, que puede proporcionar una ayuda limitada e inmediata. Una explicación adecuada mejoraría enormemente su valor a largo plazo al describir por qué esta es una buena solución al problema, y ​​la haría más útil para futuros lectores con otras preguntas similares. Edite su respuesta para agregar alguna explicación, incluidas las suposiciones que ha hecho.
Sepehr
1

Use la función de agregación y $projectpara obtener un campo de objeto específico en el documento

db.getCollection('geolocations').aggregate([ { $project : { geolocation : 1} } ])

resultado:

{
    "_id" : ObjectId("5e3ee15968879c0d5942464b"),
    "geolocation" : [ 
        {
            "_id" : ObjectId("5e3ee3ee68879c0d5942465e"),
            "latitude" : 12.9718313,
            "longitude" : 77.593551,
            "country" : "India",
            "city" : "Chennai",
            "zipcode" : "560001",
            "streetName" : "Sidney Road",
            "countryCode" : "in",
            "ip" : "116.75.115.248",
            "date" : ISODate("2020-02-08T16:38:06.584Z")
        }
    ]
}
KARTHIKEYAN.A
fuente
0

Aunque la pregunta se hizo hace 9,6 años, esto ha sido de gran ayuda para muchas personas, siendo yo una de ellas. Gracias a todos por todas sus consultas, sugerencias y respuestas. Retomando una de las respuestas aquí ... Descubrí que el siguiente método también se puede utilizar para proyectar otros campos en el documento principal. Esto puede ser útil para alguien.

Para el siguiente documento, la necesidad era averiguar si un empleado (emp # 7839) tiene su historial de licencias establecido para el año 2020. El historial de licencias se implementa como un documento incrustado dentro del documento del Empleado principal.

db.employees.find( {"leave_history.calendar_year": 2020}, 
    {leave_history: {$elemMatch: {calendar_year: 2020}},empno:true,ename:true}).pretty()


{
        "_id" : ObjectId("5e907ad23997181dde06e8fc"),
        "empno" : 7839,
        "ename" : "KING",
        "mgrno" : 0,
        "hiredate" : "1990-05-09",
        "sal" : 100000,
        "deptno" : {
                "_id" : ObjectId("5e9065f53997181dde06e8f8")
        },
        "username" : "none",
        "password" : "none",
        "is_admin" : "N",
        "is_approver" : "Y",
        "is_manager" : "Y",
        "user_role" : "AP",
        "admin_approval_received" : "Y",
        "active" : "Y",
        "created_date" : "2020-04-10",
        "updated_date" : "2020-04-10",
        "application_usage_log" : [
                {
                        "logged_in_as" : "AP",
                        "log_in_date" : "2020-04-10"
                },
                {
                        "logged_in_as" : "EM",
                        "log_in_date" : ISODate("2020-04-16T07:28:11.959Z")
                }
        ],
        "leave_history" : [
                {
                        "calendar_year" : 2020,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                },
                {
                        "calendar_year" : 2021,
                        "pl_used" : 0,
                        "cl_used" : 0,
                        "sl_used" : 0
                }
        ]
}
Ali
fuente