Récupérer uniquement l'élément interrogé dans un tableau d'objets dans la collection MongoDB

377

Supposons que vous ayez les documents suivants dans ma collection:

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

Faire une requête:

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

Ou

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

Renvoie le document correspondant (Document 1) , mais toujours avec TOUS les éléments du tableau dans shapes:

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

Cependant, je voudrais obtenir le document (Document 1) uniquement avec le tableau qui contient color=red:

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

Comment puis-je faire ceci?

Sebtm
la source

Réponses:

416

Le nouvel $elemMatchopérateur de projection de MongoDB 2.2 fournit une autre façon de modifier le document retourné pour ne contenir que le premiershapes élément correspondant :

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

Retour:

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

Dans 2.2, vous pouvez également le faire en utilisant le $ projection operator, où le $nom de champ dans un objet de projection représente l'index du premier élément de tableau correspondant du champ de la requête. Ce qui suit renvoie les mêmes résultats que ci-dessus:

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

Mise à jour MongoDB 3.2

À partir de la version 3.2, vous pouvez utiliser le nouvel $filteropérateur d'agrégation pour filtrer un tableau pendant la projection, ce qui présente l'avantage d'inclure toutes les correspondances, au lieu de la première uniquement.

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
    }}
])

Résultats:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]
JohnnyHK
la source
16
une solution si je veux qu'il renvoie tous les éléments qui lui correspondent au lieu du premier?
Steve Ng
J'ai peur d'utiliser Mongo 3.0.X :-(
charliebrownie
@charliebrownie Ensuite, utilisez l'une des autres réponses qui utilisent aggregate.
JohnnyHK
cette requête ne renvoie que les "formes" du tableau et elle ne renvoie pas d'autres champs. Quelqu'un sait aussi comment renvoyer d'autres champs?
Mark Thien
1
Cela fonctionne aussi:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paul
97

Le nouveau cadre d'agrégation dans MongoDB 2.2+ offre une alternative à Map / Reduce. L' $unwindopérateur peut être utilisé pour séparer votre shapestableau en un flux de documents pouvant être mis en correspondance:

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"
  }}
)

Résulte en:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}
Stennie
la source
7
@JohnnyHK: Dans ce cas, $elemMatchc'est une autre option. En fait, je suis arrivé ici par le biais d'une question du groupe Google où $ elemMatch ne fonctionnerait pas, car il ne renvoie que la première correspondance par document.
Stennie
1
Merci, je n'étais pas au courant de cette limitation, c'est bon à savoir. Désolé d'avoir supprimé mon commentaire auquel vous répondez, j'ai décidé de poster une autre réponse à la place et je ne voulais pas dérouter les gens.
JohnnyHK
3
@JohnnyHK: Pas de soucis, il y a maintenant trois réponses utiles à la question ;-)
Stennie
Pour d'autres chercheurs, en plus de cela, j'ai également essayé d'ajouter { $project : { shapes : 1 } }- ce qui semblait fonctionner et serait utile si les documents joints étaient volumineux et que vous vouliez simplement afficher les shapesvaleurs clés.
user1063287
2
@calmbird J'ai mis à jour l'exemple pour inclure une étape initiale de $ match. Si vous êtes intéressé par une suggestion de fonctionnalité plus efficace, je surveillerais / voterait SERVER-6612: Prend en charge la projection de plusieurs valeurs de tableau dans une projection comme le spécificateur de projection $ elemMatch dans le suivi des problèmes MongoDB.
Stennie
30

Une autre façon intéressante est d'utiliser $ redact , qui est l'une des nouvelles fonctionnalités d'agrégation de MongoDB 2.6 . Si vous utilisez 2.6, vous n'avez pas besoin d'un déroulement $ qui pourrait vous causer des problèmes de performances si vous avez de grands tableaux.

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

$redact "restreint le contenu des documents sur la base des informations stockées dans les documents eux-mêmes" . Il ne s'exécutera donc qu'à l' intérieur du document . Il analyse essentiellement votre document de haut en bas et vérifie s'il correspond à votre ifcondition $cond, s'il y a correspondance, il conservera le contenu ( $$DESCEND) ou supprimera ( $$PRUNE).

Dans l'exemple ci-dessus, $matchrenvoie d' abord le shapestableau entier , et $ redact le supprime jusqu'au résultat attendu.

Notez que cela {$not:"$color"}est nécessaire, car il numérisera également le document supérieur, et s'il $redactne trouve pas de colorchamp au niveau supérieur, cela retournera falsequi pourrait supprimer tout le document dont nous ne voulons pas.

anvarik
la source
1
réponse parfaite. Comme vous l'avez mentionné, $ unwind consommera beaucoup de RAM. Ce sera donc mieux en comparaison.
manojpt
J'ai un doute. Dans l'exemple, "formes" est un tableau. "$ Redact" va-t-il scanner tous les objets du tableau "formes" ?? Comment ce sera bon en termes de performances ??
manojpt
pas tout, mais le résultat de votre premier match. C'est la raison pour laquelle vous mettez $matchcomme première étape agrégée
anvarik
okkk .. si un index créé sur le champ "couleur", même alors, il va scanner tous les objets du tableau "formes" ??? Quel pourrait être le moyen efficace de faire correspondre plusieurs objets dans un tableau ???
manojpt
2
Brillant! Je ne comprends pas comment fonctionne $ eq ici. Je l'ai laissé à l'origine et cela n'a pas fonctionné pour moi. D'une manière ou d'une autre, il cherche dans le tableau de formes pour trouver la correspondance, mais la requête ne spécifie jamais dans quel tableau regarder. Comme, si les documents avaient des formes et, par exemple, des tailles; $ eq chercherait-il des correspondances dans les deux tableaux? $ Redact recherche-t-il simplement quelque chose dans le document qui correspond à la condition «si»?
Onosa
30

Attention: Cette réponse fournit une solution qui était pertinente à l'époque , avant l'introduction des nouvelles fonctionnalités de MongoDB 2.2 et versions ultérieures. Consultez les autres réponses si vous utilisez une version plus récente de MongoDB.

Le paramètre de sélection de champ est limité aux propriétés complètes. Il ne peut pas être utilisé pour sélectionner une partie d'un tableau, uniquement le tableau entier. J'ai essayé d'utiliser l' opérateur $ positional , mais cela n'a pas fonctionné.

Le moyen le plus simple consiste à simplement filtrer les formes dans le client .

Si vous avez vraiment besoin de la sortie correcte directement depuis MongoDB, vous pouvez utiliser une carte-réduire pour filtrer les formes.

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
la source
24

Mieux, vous pouvez interroger l'élément de tableau correspondant en utilisant $sliceest-il utile de renvoyer l'objet significatif dans un tableau.

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

$sliceest utile lorsque vous connaissez l'index de l'élément, mais que vous souhaitez parfois que l'élément de tableau corresponde à vos critères. Vous pouvez renvoyer l'élément correspondant avec l' $opérateur.

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

LES SORTIES

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}
Virel Patel
la source
12

La syntaxe pour trouver dans mongodb est

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

et la deuxième requête que vous avez écrite, c'est

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

en cela, vous avez utilisé l' $elemMatchopérateur dans la partie requête, alors que si vous utilisez cet opérateur dans la partie projection, vous obtiendrez le résultat souhaité. Vous pouvez écrire votre requête sous la forme

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

Cela vous donnera le résultat souhaité.

Vicky
la source
1
Cela fonctionne pour moi. Cependant, il apparaît que "shapes.color":"red"dans le paramètre de requête (le premier paramètre de la méthode find) n'est pas nécessaire. Vous pouvez le remplacer par {}et obtenir les mêmes résultats.
Erik Olson
2
@ErikOlson Votre suggestion est juste dans le cas ci-dessus, où nous devons trouver tout le document qui avec la couleur rouge et appliquer la projection uniquement sur eux. Mais disons que si quelqu'un a besoin de découvrir tous les documents de couleur bleue, il ne doit renvoyer que les éléments de ce tableau de formes de couleur rouge. Dans ce cas, la requête ci-dessus peut également être référencée par quelqu'un d'autre ..
Vicky
Cela semble être le plus simple, mais je ne peux pas le faire fonctionner. Il ne renvoie que le premier sous-document correspondant.
newman
8

Merci à JohnnyHK .

Ici, je veux juste ajouter une utilisation plus complexe.

// 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"
    }
]}
Tourbillon
la source
7

Vous avez juste besoin d'exécuter la requête

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

la sortie de cette requête est

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

comme vous vous y attendiez, le champ exact du tableau correspond à la couleur: «rouge».

Vaibhav Patil
la source
3

avec $ project, il sera plus approprié que d'autres éléments d'appariement judicieux soient regroupés avec d'autres éléments dans le document.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)
shakthydoss
la source
pouvez-vous décrire que cela s'accomplit avec un ensemble d'entrée et de sortie?
Alexander Mills
2

De même, vous pouvez trouver pour les multiples

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
la source
Cette réponse est en effet la méthode 4.x préférée: $matchpour réduire l'espace, puis $filterpour conserver ce que vous voulez, en écrasant le champ de saisie (utilisez la sortie de $filtersur le champ shapespour $projectrevenir sur shapes. Note de style: mieux vaut ne pas utiliser le nom du champ comme l' asargument car cela peut prêter à confusion plus tard avec $$shapeet $shape. Je préfère zzcomme asterrain car il se démarque vraiment.
Buzz Moschetti
1
db.test.find( {"shapes.color": "red"}, {_id: 0})
Poonam Agrawal
la source
1
Bienvenue dans Stack Overflow! Merci pour l'extrait de code, qui pourrait fournir une aide immédiate limitée. Une explication appropriée améliorerait considérablement sa valeur à long terme en décrivant pourquoi il s'agit d'une bonne solution au problème, et la rendrait plus utile aux futurs lecteurs ayant d'autres questions similaires. Veuillez modifier votre réponse pour ajouter des explications, y compris les hypothèses que vous avez faites.
sepehr
1

Utiliser la fonction d'agrégation et $projectpour obtenir un champ d'objet spécifique dans le document

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

résultat:

{
    "_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
la source
0

Bien que la question ait été posée il y a 9,6 ans, cela a été d'une immense aide pour de nombreuses personnes, dont moi-même. Merci à tous pour toutes vos questions, conseils et réponses. Reprenant l'une des réponses ici .. J'ai trouvé que la méthode suivante peut également être utilisée pour projeter d'autres champs dans le document parent. Cela peut être utile à quelqu'un.

Pour le document suivant, la nécessité était de savoir si un employé (emp # 7839) avait son historique de congés défini pour l'année 2020. L'historique des congés est implémenté en tant que document incorporé dans le document Employé parent.

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
la source