Gibt nur übereinstimmende Unterdokumentelemente innerhalb eines verschachtelten Arrays zurück


76

Die Hauptkollektion ist der Einzelhändler, der ein Array für Geschäfte enthält. Jedes Geschäft enthält eine Reihe von Angeboten (Sie können in diesem Geschäft kaufen). Dieses Angebot Array hat eine Reihe von Größen. (Siehe Beispiel unten)

Jetzt versuche ich alle Angebote zu finden, die in der Größe verfügbar sind L.

{
    "_id" : ObjectId("56f277b1279871c20b8b4567"),
    "stores" : [
        {
        "_id" : ObjectId("56f277b5279871c20b8b4783"),
        "offers" : [
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "XS",
                    "S",
                    "M"
                ]
            },
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "S",
                    "L",
                    "XL"
                ]
            }
        ]
    }
}

Ich habe diese Abfrage versucht: db.getCollection('retailers').find({'stores.offers.size': 'L'})

Ich erwarte eine solche Ausgabe:

 {
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
    {
        "_id" : ObjectId("56f277b5279871c20b8b4783"),
        "offers" : [
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "S",
                    "L",
                    "XL"
                ]
            }
        ]
    }
}

Die Ausgabe meiner Abfrage enthält aber auch das nicht übereinstimmende Angebot mit sizeXS, X und M.

Wie kann ich MongoDB zwingen, nur die Angebote zurückzugeben, die meiner Anfrage entsprechen?

Grüße und danke.


Meinten Sie so etwas? db.getCollection('retailers').find({'stores.offers.size': 'L'}, {'stores.offers': 1}). Aber dann enthält die Antwort auch falsche Angebote
Vico

Sollte ich ein Aggregat mit $matchund $unwindfür mein Problem verwenden?
Vico

Antworten:


135

Die Abfrage, die Sie haben, wählt also das "Dokument" genau so aus, wie es sollte. Sie suchen jedoch nach "Filtern der enthaltenen Arrays", sodass die zurückgegebenen Elemente nur der Bedingung der Abfrage entsprechen.

Die eigentliche Antwort lautet natürlich: Wenn Sie nicht wirklich viel Bandbreite sparen, indem Sie solche Details herausfiltern, sollten Sie es nicht einmal versuchen oder zumindest über die erste Positionsübereinstimmung hinaus.

MongoDB verfügt über einen Positionsoperator $, der ein Array-Element am übereinstimmenden Index aus einer Abfragebedingung zurückgibt. Dies gibt jedoch nur den "ersten" übereinstimmenden Index des "äußersten" Array-Elements zurück.

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
)

In diesem Fall bedeutet dies nur die "stores"Array-Position. Wenn also mehrere "Speicher" -Einträge vorhanden wären, würde nur "eines" der Elemente zurückgegeben, die Ihre übereinstimmende Bedingung enthielten. Dies bedeutet jedoch nichts für das innere Array von "offers", und als solches würde jedes "Angebot" innerhalb des übereinstimmenden "stores"Arrays immer noch zurückgegeben.

MongoDB hat keine Möglichkeit, dies in einer Standardabfrage zu "filtern", daher funktioniert Folgendes nicht:

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$.offers.$': 1 }
)

Das einzige Werkzeug, das MongoDB tatsächlich benötigt, um diese Manipulationsebene auszuführen, ist das Aggregationsframework. Die Analyse sollte Ihnen jedoch zeigen, warum Sie dies "wahrscheinlich" nicht tun sollten, und stattdessen nur das Array im Code filtern.


In der Reihenfolge, wie Sie dies pro Version erreichen können.

Zuerst mit MongoDB 3.2.x mit der $filterOperation:

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", [] ]}
      }
    }
  }}
])

Dann mit MongoDB 2.6.x und höher mit $mapund $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]
      ]
    }
  }}
])

Und schließlich in jeder Version über MongoDB 2.2.x, in der das Aggregationsframework eingeführt wurde.

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

Lassen Sie uns die Erklärungen aufschlüsseln.

MongoDB 3.2.x und höher

Im Allgemeinen $filterist dies der richtige Weg, da es auf den Zweck ausgelegt ist. Da es mehrere Ebenen des Arrays gibt, müssen Sie dies auf jeder Ebene anwenden. Also zunächst sind Sie tauchen in jeden "offers"innerhalb "stores"zu examime und $filterdiese Inhalte.

Der einfache Vergleich hier lautet "Enthält das "size"Array das gesuchte Element?" . In diesem logischen Kontext besteht die kurze Aufgabe darin, die $setIsSubsetOperation zum Vergleichen eines Arrays ("Set") ["L"]mit dem Zielarray zu verwenden. Wenn diese Bedingung erfüllt ist true(sie enthält "L"), wird das Array-Element für "offers"beibehalten und im Ergebnis zurückgegeben.

In der höheren Ebene $filtersuchen Sie dann, ob das Ergebnis dieses vorherigen $filterein leeres Array []für zurückgegeben hat "offers". Wenn es nicht leer ist, wird das Element zurückgegeben oder auf andere Weise entfernt.

MongoDB 2.6.x.

Dies ist dem modernen Prozess sehr ähnlich, außer dass Sie, da es $filterin dieser Version keine gibt $map, jedes Element überprüfen und dann $setDifferencealle Elemente herausfiltern können, die als zurückgegeben wurden false.

Es wird also $mapdas gesamte Array zurückgegeben, aber die $condOperation entscheidet nur, ob das Element oder stattdessen ein falseWert zurückgegeben wird. Beim Vergleich $setDifferencemit einem einzelnen Element würde "Menge" [false]aller falseElemente im zurückgegebenen Array entfernt.

In allen anderen Fällen ist die Logik dieselbe wie oben.

MongoDB 2.2.x und höher

So unter MongoDB 2.6 das einzige Werkzeug für mit Arrays arbeiten ist $unwind, und zu diesem Zweck allein sollten Sie nicht die Aggregation Rahmen „nur“ für diesen Zweck verwenden.

Der Prozess erscheint in der Tat einfach, indem Sie einfach jedes Array "auseinander nehmen", die Dinge herausfiltern, die Sie nicht benötigen, und es dann wieder zusammensetzen. Die Hauptaufgabe liegt in den "zwei" $groupPhasen, wobei die "erste" das innere Array neu erstellt und die nächste das äußere Array neu erstellt. Auf _idallen Ebenen gibt es unterschiedliche Werte, daher müssen diese nur auf jeder Gruppierungsebene berücksichtigt werden.

Aber das Problem ist , dass $unwindist sehr teuer . Obwohl es immer noch einen Zweck hat, besteht seine Hauptverwendungsabsicht darin, diese Art der Filterung nicht pro Dokument durchzuführen. Tatsächlich sollte es in modernen Releases nur verwendet werden, wenn ein Element des Arrays Teil des "Gruppierungsschlüssels" selbst werden muss.


Fazit

Es ist also kein einfacher Prozess, Übereinstimmungen auf mehreren Ebenen eines Arrays wie dieses zu erhalten, und tatsächlich kann es bei falscher Implementierung äußerst kostspielig sein .

Zu diesem Zweck sollten immer nur die beiden modernen Listings verwendet werden, da sie zusätzlich zur "Abfrage" eine "einzelne" Pipeline-Stufe $matchverwenden, um die "Filterung" durchzuführen. Der resultierende Effekt ist wenig mehr Aufwand als die Standardformen von .find().

Im Allgemeinen sind diese Einträge jedoch immer noch sehr komplex. Wenn Sie den durch eine solche Filterung zurückgegebenen Inhalt nicht drastisch reduzieren, um die zwischen Server und Client verwendete Bandbreite erheblich zu verbessern, sind Sie besser des Filterns des Ergebnisses der anfänglichen Abfrage und der Grundprojektion.

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

Die Arbeit mit der Abfrageverarbeitung des zurückgegebenen Objekts "post" ist daher weitaus weniger stumpf als die Verwendung der Aggregationspipeline, um dies zu tun. Und wie bereits erwähnt, besteht der einzige "echte" Unterschied darin, dass Sie die anderen Elemente auf dem "Server" verwerfen, anstatt sie beim Empfang "pro Dokument" zu entfernen, was möglicherweise ein wenig Bandbreite spart.

Wenn Sie dies jedoch nicht in einer modernen Version mit nur $match und tun, $projectüberwiegen die "Kosten" für die Verarbeitung auf dem Server erheblich den "Gewinn" bei der Reduzierung des Netzwerk-Overheads, indem zuerst die nicht übereinstimmenden Elemente entfernt werden.

In allen Fällen erhalten Sie das gleiche Ergebnis:

{
        "_id" : ObjectId("56f277b1279871c20b8b4567"),
        "stores" : [
                {
                        "_id" : ObjectId("56f277b5279871c20b8b4783"),
                        "offers" : [
                                {
                                        "_id" : ObjectId("56f277b1279871c20b8b4567"),
                                        "size" : [
                                                "S",
                                                "L",
                                                "XL"
                                        ]
                                }
                        ]
                }
        ]
}

1
Ich habe etwas sehr Ähnliches implementiert (mit dem Unterschied, dass ich in diesem Beispiel eine genaue Array-Übereinstimmung für "Größe" finden musste) und es ist tatsächlich nicht effizient, wenn nur ein paar Dokumente (nicht 1000 oder Millionen) benötigt werden über 5 Sekunden zu berechnen. Mal sehen, ob die Nachbearbeitung effizienter wäre.
Am

1
oder was ist, wenn die Größe als separate Sammlung anstelle eines verschachtelten Arrays beibehalten wird? würde das nicht die Leistung verbessern und gleichzeitig erstaunliche Abfragefunktionen bieten
PirateApp

Danke, der Schlüssel zu mir ist "Unwind" -Operator. Jetzt hab ich es verstanden.
Stefano Scarpanti

Vielen Dank für Ihre Antwort! Es hat mir auch geholfen :)
Andrew T

12

Da Ihr Array eingebettet ist, können wir $ elemMatch nicht verwenden. Stattdessen können Sie das Aggregationsframework verwenden, um Ihre Ergebnisse zu erhalten:

db.retailers.aggregate([
{$match:{"stores.offers.size": 'L'}}, //just precondition can be skipped
{$unwind:"$stores"},
{$unwind:"$stores.offers"},
{$match:{"stores.offers.size": 'L'}},
{$group:{
    _id:{id:"$_id", "storesId":"$stores._id"},
    "offers":{$push:"$stores.offers"}
}},
{$group:{
    _id:"$_id.id",
    stores:{$push:{_id:"$_id.storesId","offers":"$offers"}}
}}
]).pretty()

Bei dieser Abfrage werden Arrays (zweimal) abgewickelt, die Größe angepasst und das Dokument in das vorherige Formular umgeformt. Sie können $ group-Schritte entfernen und sehen, wie sie gedruckt werden. Viel Spaß!

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.