Recupera solo l'elemento richiesto in una matrice di oggetti nella raccolta MongoDB


377

Supponi di avere i seguenti documenti nella mia collezione:

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

Esegui query:

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

O

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

Restituisce il documento corrispondente (Documento 1) , ma sempre con TUTTI gli elementi dell'array in shapes:

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

Tuttavia, vorrei ottenere il documento (Documento 1) solo con l'array che contiene color=red:

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

Come posso fare questo?

Risposte:


416

Il nuovo $elemMatchoperatore di proiezione di MongoDB 2.2 fornisce un altro modo per modificare il documento restituito in modo da contenere solo il primoshapes elemento corrispondente :

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

Ritorna:

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

In 2.2 è possibile farlo anche usando $ projection operator, dove il $nome del campo in un oggetto di proiezione rappresenta l'indice del primo elemento di array corrispondente del campo dalla query. Quanto segue restituisce gli stessi risultati di cui sopra:

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

Aggiornamento MongoDB 3.2

A partire dalla versione 3.2, è possibile utilizzare il nuovo $filteroperatore di aggregazione per filtrare un array durante la proiezione, che ha il vantaggio di includere tutte le corrispondenze, anziché solo la prima.

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

risultati:

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

16
qualche soluzione se voglio che restituisca tutti gli elementi che la corrispondono invece del solo?
Steve Ng,

Temo di usare Mongo 3.0.X :-(
charliebrownie il

@charliebrownie Quindi usa una delle altre risposte che usano aggregate.
JohnnyHK,

questa query restituisce solo l'array "forme" e non restituisce altri campi. Qualcuno sa come restituire anche altri campi?
Mark Thien,

1
Questo funziona anche:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paolo

97

Il nuovo Aggregation Framework in MongoDB 2.2+ offre un'alternativa a Map / Reduce. L' $unwindoperatore può essere utilizzato per separare l' shapesarray in un flusso di documenti che possono essere abbinati:

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

Risultati in:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}

7
@JohnnyHK: in questo caso, $elemMatchè un'altra opzione. In realtà sono arrivato qui tramite una domanda di Google Group in cui $ elemMatch non funzionava perché restituiva solo la prima corrispondenza per documento.
Stennie,

1
Grazie, non ero a conoscenza di tale limitazione, quindi è bene saperlo. Scusami per aver eliminato il mio commento a cui stai rispondendo, ho deciso di pubblicare un'altra risposta e non volevo confondere le persone.
JohnnyHK,

3
@JohnnyHK: non preoccuparti, ora ci sono tre risposte utili alla domanda ;-)
Stennie,

Per altri ricercatori, oltre a questo, ho anche provato ad aggiungere { $project : { shapes : 1 } }- che sembrava funzionare e sarebbe stato utile se i documenti allegati fossero di grandi dimensioni e tu volessi solo visualizzare i shapesvalori chiave.
user1063287

2
@calmbird Ho aggiornato l'esempio per includere una fase iniziale $ match. Se sei interessato a un suggerimento di funzionalità più efficiente, guarderei / voterei SERVER-6612: supporto per la proiezione di più valori di array in una proiezione come l'identificatore di proiezione $ elemMatch nel tracker di problemi MongoDB.
Stennie,

30

Un altro modo interessante è usare $ redact , che è una delle nuove funzionalità di aggregazione di MongoDB 2.6 . Se si utilizza 2.6, non è necessario un $ unfind che potrebbe causare problemi di prestazioni in presenza di array di grandi dimensioni.

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

$redact "limita il contenuto dei documenti in base alle informazioni memorizzate nei documenti stessi" . Quindi verrà eseguito solo all'interno del documento . Fondamentalmente scansiona il documento dall'alto verso il basso e controlla se corrisponde alla tua ifcondizione in cui si trova $cond, se c'è corrispondenza manterrà il contenuto ( $$DESCEND) o rimuoverà ( $$PRUNE).

Nell'esempio sopra, per prima cosa $matchrestituisce l'intero shapesarray e $ redact lo riduce fino al risultato previsto.

Si noti che {$not:"$color"}è necessario, perché eseguirà anche la scansione del documento principale e, se $redactnon trova un colorcampo al livello superiore, ciò restituirà falseche potrebbe rimuovere l'intero documento che non desideriamo.


1
risposta perfetta Come hai detto, $ unwind consumerà molta RAM. Quindi questo sarà migliore se confrontato.
manojpt

Ho un dubbio. Nell'esempio, "forme" è un array. "$ Redact" eseguirà la scansione di tutti gli oggetti nell'array "shapes" ?? Come andrà bene rispetto alle prestazioni ??
manojpt

non tutto, ma il risultato della tua prima partita. Questo è il motivo per cui hai messo $matchcome primo stadio aggregato
anvarik il

okkk .. se un indice creato sul campo "color", anche allora scansionerà tutti gli oggetti nell'array "shapes" ??? Quale potrebbe essere il modo efficiente di abbinare più oggetti in un array ???
manojpt

2
Brillante! Non capisco come $ eq funzioni qui. L'ho lasciato inizialmente e questo non ha funzionato per me. In qualche modo, cerca nell'array di forme per trovare la corrispondenza, ma la query non specifica mai quale array cercare. Ad esempio, se i documenti avevano forme e, ad esempio, dimensioni; $ eq cercherebbe le partite in entrambi gli array? $ Redact sta solo cercando qualcosa all'interno del documento che corrisponda alla condizione 'if'?
Onosa,

30

Attenzione: questa risposta fornisce una soluzione che era rilevante in quel momento , prima dell'introduzione delle nuove funzionalità di MongoDB 2.2 e versioni successive. Vedi le altre risposte se stai usando una versione più recente di MongoDB.

Il parametro selettore di campo è limitato alle proprietà complete. Non può essere utilizzato per selezionare una parte di un array, solo l'intero array. Ho provato a usare l' operatore $ posizionale , ma non ha funzionato.

Il modo più semplice è semplicemente filtrare le forme nel client .

Se hai davvero bisogno dell'output corretto direttamente da MongoDB, puoi utilizzare una riduzione mappa per filtrare le forme.

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()

24

Meglio è possibile eseguire una query nell'elemento array corrispondente usando $sliceè utile restituire l'oggetto significativo in un array.

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

$sliceè utile quando si conosce l'indice dell'elemento, ma a volte si desidera che qualsiasi elemento dell'array corrisponda ai propri criteri. È possibile restituire l'elemento corrispondente con l' $operatore.


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

USCITE

{

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

12

La sintassi per find in mongodb è

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

e la seconda query che hai scritto, cioè

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

in questo caso hai utilizzato l' $elemMatchoperatore nella parte della query, mentre se usi questo operatore nella parte della proiezione otterrai il risultato desiderato. Puoi scrivere la tua richiesta come

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

Questo ti darà il risultato desiderato.


1
Questo funziona per me. Tuttavia, sembra che "shapes.color":"red"nel parametro query (il primo parametro del metodo find) non sia necessario. Puoi sostituirlo con {}e ottenere gli stessi risultati.
Erik Olson,

2
@ErikOlson Il tuo suggerimento è giusto nel caso precedente, in cui dobbiamo trovare tutto il documento con colore rosso e applicare la proiezione solo su di essi. Ma diciamo se qualcuno ha bisogno di scoprire tutto il documento che ha il colore blu ma dovrebbe restituire solo quell'elemento dell'array di forme che ha il colore rosso. In questo caso la query sopra può essere referenziata anche da qualcun altro.
Vicky,

Questo sembra essere il più semplice, ma non riesco a farlo funzionare. Restituisce solo il primo documento secondario corrispondente.
Newman,

8

Grazie a JohnnyHK .

Qui voglio solo aggiungere un uso più complesso.

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

7

Hai solo bisogno di eseguire query

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

l'output di questa query è

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

come previsto, si otterrà il campo esatto dall'array che corrisponde al colore: "rosso".


3

insieme a $ project sarà più appropriato che altri elementi di corrispondenza saggi vengano clubbati insieme ad altri elementi nel documento.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)

puoi descrivere che ciò si ottiene con un set di input e output?
Alexander Mills,

2

Allo stesso modo puoi trovare per il multiplo

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

Questa risposta è davvero il modo 4.x preferito: $matchridurre lo spazio, quindi $filtermantenere ciò che si desidera, sovrascrivendo il campo di input (utilizzare l'output del $filtercampo on shapesper $projecttornare indietro shapes. Nota di stile: meglio non usare il nome del campo come l' asargomento perché ciò può causare confusione in seguito con $$shapee $shape. Preferisco zzil ascampo perché si distingue davvero.
Buzz Moschetti

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

1
Benvenuto in Stack Overflow! Grazie per lo snippet di codice, che potrebbe fornire un aiuto limitato e immediato. Una spiegazione adeguata migliorerebbe notevolmente il suo valore a lungo termine descrivendo perché questa è una buona soluzione al problema e la renderebbe più utile ai futuri lettori con altre domande simili. Modifica la tua risposta per aggiungere alcune spiegazioni, inclusi i presupposti che hai formulato.
sepehr,

1

Utilizzare la funzione di aggregazione e $projectper ottenere un campo oggetto specifico nel documento

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

risultato:

{
    "_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")
        }
    ]
}

0

Sebbene la domanda sia stata posta 9,6 anni fa, questo è stato di grande aiuto per numerose persone, essendo io una di queste. Grazie a tutti per tutte le vostre domande, suggerimenti e risposte. Raccogliendo da una delle risposte qui ... Ho scoperto che il seguente metodo può essere utilizzato anche per proiettare altri campi nel documento principale. Questo può essere utile a qualcuno.

Per il seguente documento, era necessario scoprire se un dipendente (emp # 7839) ha impostato la cronologia delle ferie per l'anno 2020. La cronologia delle ferie è implementata come documento incorporato nel documento Dipendente principale.

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
                }
        ]
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.