Rufen Sie nur das abgefragte Element in einem Objektarray in der MongoDB-Auflistung ab


377

Angenommen, Sie haben die folgenden Dokumente in meiner Sammlung:

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

Fragen Sie ab:

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

Oder

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

Gibt übereinstimmendes Dokument (Dokument 1) zurück , jedoch immer mit ALLEN Array-Elementen in shapes:

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

Ich möchte das Dokument (Dokument 1) jedoch nur mit dem Array erhalten, das Folgendes enthält color=red:

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

Wie kann ich das machen?

Antworten:


416

Der neue $elemMatchProjektionsoperator von MongoDB 2.2 bietet eine weitere Möglichkeit, das zurückgegebene Dokument so zu ändern, dass es nur das erste übereinstimmende shapesElement enthält:

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

Kehrt zurück:

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

In 2.2 können Sie auch das tun dies mit $ projection operator, in dem die $in einem Projektionsobjekt Feldnamen den Index des ersten Anpassungs Feldelementes Feld repräsentiert aus der Abfrage. Das Folgende liefert die gleichen Ergebnisse wie oben:

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

MongoDB 3.2 Update

Ab Version 3.2 können Sie den neuen $filterAggregationsoperator verwenden, um ein Array während der Projektion zu filtern. Dies hat den Vorteil, dass alle Übereinstimmungen anstelle nur der ersten berücksichtigt werden.

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

Ergebnisse:

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

15
Gibt es eine Lösung, wenn ich möchte, dass alle Elemente zurückgegeben werden, die mit ihm übereinstimmen, anstatt nur das erste?
Steve Ng

Ich fürchte, ich benutze Mongo 3.0.X :-(
Charliebrownie

@charliebrownie Verwenden Sie dann eine der anderen Antworten, die verwendet werden aggregate.
JohnnyHK

Diese Abfrage gibt nur das Array "Formen" zurück und keine anderen Felder. Weiß jemand, wie man auch andere Felder zurückgibt?
Mark Thien

1
Dies funktioniert auch:db.test.find({}, {shapes: {$elemMatch: {color: "red"}}});
Paul

97

Das neue Aggregation Framework in MongoDB 2.2+ bietet eine Alternative zu Map / Reduce. Mit dem $unwindOperator können Sie Ihr shapesArray in einen Stream von Dokumenten aufteilen, die abgeglichen werden können:

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

Ergebnisse in:

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

6
@ JohnnyHK: In diesem Fall $elemMatchist eine andere Option. Ich bin tatsächlich über eine Google Group-Frage hierher gekommen, bei der $ elemMatch nicht funktionieren würde, da nur die erste Übereinstimmung pro Dokument zurückgegeben wird.
Stennie

1
Danke, ich war mir dieser Einschränkung nicht bewusst, das ist also gut zu wissen. Es tut mir leid, dass Sie meinen Kommentar gelöscht haben, auf den Sie antworten. Ich habe beschlossen, stattdessen eine andere Antwort zu veröffentlichen, und wollte die Leute nicht verwirren.
JohnnyHK

3
@ JohnnyHK: Keine Sorge, es gibt jetzt drei nützliche Antworten auf die Frage ;-)
Stennie

Für andere Suchende habe ich zusätzlich versucht, etwas hinzuzufügen { $project : { shapes : 1 } }- was zu funktionieren schien und hilfreich wäre, wenn die beiliegenden Dokumente groß wären und Sie nur die shapesSchlüsselwerte anzeigen wollten .
user1063287

2
@calmbird Ich habe das Beispiel aktualisiert, um eine anfängliche $ match-Phase einzuschließen. Wenn Sie an einem effizienteren Funktionsvorschlag interessiert sind, würde ich SERVER-6612: Unterstützung der Projektion mehrerer Array-Werte in einer Projektion wie dem Projektionsspezifizierer $ elemMatch im MongoDB-Issue-Tracker beobachten / verbessern .
Stennie

30

Eine andere interessante Möglichkeit ist die Verwendung von $ redact , einer der neuen Aggregationsfunktionen von MongoDB 2.6 . Wenn Sie 2.6 verwenden, benötigen Sie kein $ unwind, was bei großen Arrays zu Leistungsproblemen führen kann.

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

$redact "schränkt den Inhalt der Dokumente auf der Grundlage der in den Dokumenten selbst gespeicherten Informationen ein" . Es wird also nur innerhalb des Dokuments ausgeführt . Grundsätzlich wird Ihr Dokument von oben nach unten gescannt und überprüft, ob es mit Ihrer aktuellen ifBedingung übereinstimmt. Wenn eine Übereinstimmung vorliegt $cond, wird entweder der Inhalt beibehalten ( $$DESCEND) oder entfernt ( $$PRUNE).

Im obigen Beispiel wird zuerst $matchdas gesamte shapesArray zurückgegeben, und $ redact reduziert es auf das erwartete Ergebnis.

Beachten Sie, dass dies {$not:"$color"}erforderlich ist, da auch das oberste Dokument gescannt wird. Wenn auf der obersten Ebene $redactkein colorFeld gefunden wird false, wird möglicherweise das gesamte Dokument entfernt, das wir nicht möchten.


1
perfekte Antwort. Wie Sie bereits erwähnt haben, verbraucht $ unwind viel RAM. Das wird also im Vergleich besser.
Manojpt

Ich habe einen Zweifel. Im Beispiel ist "Formen" ein Array. Scannt "$ redact" alle Objekte im Array "Shapes"? Wie wird dies in Bezug auf die Leistung gut sein?
Manojpt

Nicht alles, aber das Ergebnis Ihres ersten Spiels. Das ist der Grund, warum Sie $matchals Ihre erste
Gesamtstufe

okkk .. wenn ein Index für das Feld "Farbe" erstellt wird, werden auch dann alle Objekte im Array "Formen" gescannt ??? Welches könnte der effiziente Weg sein, um mehrere Objekte in einem Array abzugleichen?
Manojpt

2
Brillant! Ich verstehe nicht, wie $ eq hier funktioniert. Ich habe es ursprünglich weggelassen und das hat bei mir nicht funktioniert. Irgendwie sieht es im Array von Formen aus, um die Übereinstimmung zu finden, aber die Abfrage gibt nie an, in welchem ​​Array gesucht werden soll. Zum Beispiel, wenn die Dokumente Formen und beispielsweise Größen hatten; Würde $ eq in beiden Arrays nach Übereinstimmungen suchen? Sucht $ redact nur nach etwas im Dokument, das der 'if'-Bedingung entspricht?
Onosa

30

Achtung: Diese Antwort bietet eine Lösung, die zu diesem Zeitpunkt relevant war , bevor die neuen Funktionen von MongoDB 2.2 und höher eingeführt wurden. Weitere Antworten finden Sie, wenn Sie eine neuere Version von MongoDB verwenden.

Der Feldauswahlparameter ist auf vollständige Eigenschaften beschränkt. Es kann nicht verwendet werden, um einen Teil eines Arrays auszuwählen, sondern nur das gesamte Array. Ich habe versucht, den $ positional-Operator zu verwenden , aber das hat nicht funktioniert.

Am einfachsten ist es, nur die Formen im Client zu filtern .

Wenn Sie wirklich die richtige Ausgabe direkt von MongoDB benötigen , können Sie die Formen mithilfe einer Kartenreduzierung filtern.

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

Besser ist es, wenn Sie das passende Array-Element abfragen, indem $sliceSie das signifikante Objekt in einem Array zurückgeben.

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

$sliceist hilfreich, wenn Sie den Index des Elements kennen, aber manchmal möchten Sie, dass das Array-Element Ihren Kriterien entspricht. Sie können das übereinstimmende Element mit dem $Operator zurückgeben.


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

AUSGÄNGE

{

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

12

Die Syntax für find in mongodb lautet

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

und die zweite Abfrage, die Sie geschrieben haben, das heißt

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

In diesem Fall haben Sie den $elemMatchOperator im Abfrageteil verwendet. Wenn Sie diesen Operator im Projektionsteil verwenden, erhalten Sie das gewünschte Ergebnis. Sie können Ihre Anfrage als aufschreiben

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

Dadurch erhalten Sie das gewünschte Ergebnis.


1
Das funktioniert bei mir. Es scheint jedoch, dass "shapes.color":"red"im Abfrageparameter (dem ersten Parameter der Suchmethode) keine Notwendigkeit besteht. Sie können es durch ersetzen {}und die gleichen Ergebnisse erzielen.
Erik Olson

2
@ErikOlson Ihr Vorschlag ist in dem obigen Fall richtig, in dem wir alle Dokumente mit roter Farbe finden und die Projektion nur auf sie anwenden müssen. Angenommen, jemand muss alle Dokumente mit der Farbe Blau herausfinden, sollte jedoch nur das Element dieses Shapes-Arrays mit der Farbe Rot zurückgeben. In diesem Fall kann die obige Abfrage auch von jemand anderem referenziert werden.
Vicky

Dies scheint am einfachsten zu sein, aber ich kann es nicht zum Laufen bringen. Es wird nur das erste übereinstimmende Unterdokument zurückgegeben.
Newman

8

Vielen Dank an JohnnyHK .

Hier möchte ich nur eine komplexere Verwendung hinzufügen.

// 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

Sie müssen nur die Abfrage ausführen

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

Ausgabe dieser Abfrage ist

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

Wie erwartet wird das genaue Feld des Arrays angezeigt, das der Farbe entspricht: 'rot'.


3

zusammen mit $ project ist es angemessener, andere weise übereinstimmende Elemente zusammen mit anderen Elementen im Dokument zu kombinieren.

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

Können Sie bitte beschreiben, dass dies mit einem Eingabe- und Ausgabesatz erreicht wird?
Alexander Mills

2

Ebenso können Sie für das Vielfache finden

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

Diese Antwort ist in der Tat die bevorzugte 4.x-Methode: $matchReduzieren Sie den Platz, $filterbehalten Sie dann das gewünschte Feld bei und überschreiben Sie das Eingabefeld (verwenden Sie die Ausgabe von $filteron field, shapesum darauf $projectzurückzugreifen shapes. Stilhinweis: Verwenden Sie den Feldnamen am besten nicht als das asArgument, weil das später zu Verwirrung mit $$shapeund führen kann $shape. Ich bevorzuge zzals das asFeld, weil es wirklich auffällt.
Buzz Moschetti

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

1
Willkommen bei Stack Overflow! Vielen Dank für das Code-Snippet, das möglicherweise nur begrenzte, sofortige Hilfe bietet. Eine richtige Erklärung würde ihren langfristigen Wert erheblich verbessern, indem sie beschreibt, warum dies eine gute Lösung für das Problem ist, und es für zukünftige Leser mit anderen ähnlichen Fragen nützlicher machen. Bitte bearbeiten Sie Ihre Antwort, um eine Erklärung hinzuzufügen, einschließlich der von Ihnen getroffenen Annahmen.
September

1

Verwenden Sie die Aggregationsfunktion und $project, um ein bestimmtes Objektfeld im Dokument abzurufen

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

Ergebnis:

{
    "_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

Obwohl die Frage vor 9,6 Jahren gestellt wurde, war dies für zahlreiche Menschen, von denen ich einer von ihnen bin, eine immense Hilfe. Vielen Dank an alle für all Ihre Fragen, Hinweise und Antworten. Ich habe festgestellt, dass die folgende Methode auch zum Projizieren anderer Felder im übergeordneten Dokument verwendet werden kann. Dies kann für jemanden hilfreich sein.

Für das folgende Dokument musste herausgefunden werden, ob für einen Mitarbeiter (emp # 7839) die Urlaubshistorie für das Jahr 2020 festgelegt wurde. Die Urlaubshistorie wird als eingebettetes Dokument im übergeordneten Mitarbeiterdokument implementiert.

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
                }
        ]
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.