sql >> Database teknologi >  >> NoSQL >> MongoDB

Returner kun matchede underdokumentelementer inden for et indlejret array

Så den forespørgsel, du har, vælger faktisk "dokumentet", som den skal. Men det, du leder efter, er at "filtrere de arrays", der er indeholdt, så de returnerede elementer kun matcher betingelsen for forespørgslen.

Det rigtige svar er selvfølgelig, at medmindre du virkelig sparer en masse båndbredde ved at filtrere sådanne detaljer fra, så bør du ikke engang prøve, eller i det mindste ud over det første positionelle match.

MongoDB har en positionel $ operator, som returnerer et array-element ved det matchede indeks fra en forespørgselsbetingelse. Dette returnerer dog kun det "første" matchede indeks for det "ydre" mest array-element.

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

I dette tilfælde betyder det "stores" kun array-position. Så hvis der var flere "butikker"-poster, ville kun "én" af de elementer, der indeholdt din matchede betingelse, blive returneret. Men , der ikke gør noget for den indre række af "offers" , og som sådan alle "tilbud" i de matchede "stores" array ville stadig blive returneret.

MongoDB har ingen mulighed for at "filtrere" dette i en standardforespørgsel, så følgende virker ikke:

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

De eneste værktøjer, MongoDB faktisk har til at udføre dette niveau af manipulation, er med aggregeringsrammen. Men analysen skulle vise dig, hvorfor du "sandsynligvis" ikke burde gøre dette, og i stedet bare filtrere arrayet i kode.

I rækkefølgen af, hvordan du kan opnå dette pr. version.

Først med MongoDB 3.2.x ved at bruge $filter operation:

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

Derefter med MongoDB 2.6.x og derover med $map og $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]
      ]
    }
  }}
])

Og endelig i enhver version over MongoDB 2.2.x hvor aggregeringsrammen blev indført.

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

Lad os nedbryde forklaringerne.

MongoDB 3.2.x og nyere

Så generelt set $filter er vejen at gå her, da det er designet med formålet for øje. Da der er flere niveauer af arrayet, skal du anvende dette på hvert niveau. Så først dykker du ned i hver "offers" i "stores" for at undersøge og $filter det indhold.

Den simple sammenligning her er "Er "size" array indeholder det element, jeg leder efter" . I denne logiske sammenhæng er den korte ting at gøre at bruge $setIsSubset operation for at sammenligne et array ("sæt") af ["L"] til målarrayet. Hvor denne betingelse er true ( den indeholder "L") derefter array-elementet for "offers" beholdes og returneres i resultatet.

På det højere niveau $filter , leder du derefter efter at se, om resultatet fra det tidligere $filter returnerede et tomt array [] for "offers" . Hvis det ikke er tomt, returneres elementet, eller på anden måde fjernes det.

MongoDB 2.6.x

Dette er meget lig den moderne proces bortset fra, at da der ikke er noget $filter i denne version kan du bruge $map for at inspicere hvert element og derefter bruge $setDifference for at bortfiltrere alle elementer, der blev returneret som false .

$map vil returnere hele arrayet, men $cond operationen bestemmer bare, om elementet skal returneres eller i stedet for en false værdi. I sammenligningen af ​​$setDifference til et enkelt element "sæt" af [false] alle false elementer i det returnerede array ville blive fjernet.

På alle andre måder er logikken den samme som ovenfor.

MongoDB 2.2.x og nyere

Så under MongoDB 2.6 er det eneste værktøj til at arbejde med arrays $unwind , og alene til dette formål bør du ikke bruge aggregeringsrammen "bare" til dette formål.

Processen ser faktisk simpel ud, ved blot at "adskille" hvert array, filtrere de ting fra, du ikke har brug for, og derefter sætte det sammen igen. Den primære behandling er i "to" $group stadier, med den "første" til at genopbygge det indre array, og den næste til at genopbygge det ydre array. Der er forskellige _id værdier på alle niveauer, så disse skal blot inkluderes på alle grupperingsniveauer.

Men problemet er, at $unwind er meget dyrt . Selvom det stadig har et formål, er dets hovedhensigt ikke at udføre denne form for filtrering pr. dokument. Faktisk i moderne udgivelser bør det kun bruges, når et element i arrayet(erne) skal blive en del af selve "grupperingsnøglen".

Konklusion

Så det er ikke en simpel proces at få matches på flere niveauer af et array som dette, og det kan faktisk være ekstremt dyrt hvis implementeret forkert.

Kun de to moderne fortegnelser bør nogensinde bruges til dette formål, da de anvender et "enkelt" pipelinetrin ud over "forespørgslen" $match for at udføre "filtreringen". Den resulterende effekt er lidt mere overhead end standardformerne for .find() .

Generelt har disse fortegnelser dog stadig en del kompleksitet i sig, og med mindre du virkelig drastisk reducerer det indhold, der returneres af en sådan filtrering på en måde, der giver en betydelig forbedring af den båndbredde, der bruges mellem serveren og klienten, så er du bedre filtrering af resultatet af den indledende forespørgsel og grundlæggende projektion.

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

Så arbejdet med det returnerede objekt "efter"-forespørgselsbehandling er langt mindre besværligt end at bruge aggregeringspipelinen til at gøre dette. Og som nævnt ville den eneste "rigtige" forskel være, at du kasserer de andre elementer på "serveren" i modsætning til at fjerne dem "pr. dokument", når de modtages, hvilket kan spare lidt båndbredde.

Men medmindre du gør dette i en moderne udgivelse med kun $match og $project , så vil "omkostningerne" ved behandling på serveren i høj grad opveje "gevinsten" ved at reducere netværksomkostningerne ved først at fjerne de uovertrufne elementer.

I alle tilfælde får du det samme resultat:

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


  1. Azure Redis-cache - timeouts på GET-opkald

  2. Oprettelse af en visningsfunktion uden at returnere et svar i Flask

  3. NodeJS + MongoDB:Henter data fra indsamling med findOne ()

  4. Hvad er Hadoop Reducer Class i MapReduce?