web-dev-qa-db-fra.com

Filtre d'agrégation après la recherche $

Comment puis-je ajouter un filtre après une recherche $ ou existe-t-il une autre méthode pour le faire?

Mon test de collecte de données est:

{ "_id" : ObjectId("570557d4094a4514fc1291d6"), "id" : 100, "value" : "0", "contain" : [ ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d7"), "id" : 110, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d8"), "id" : 120, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d9"), "id" : 121, "value" : "2", "contain" : [ 100, 120 ] }

Je sélectionne id 100 et agrège les enfants:

db.test.aggregate([ {
  $match : {
    id: 100
  }
}, {
  $lookup : {
    from : "test",
    localField : "id",
    foreignField : "contain",
    as : "childs"
  }
}]);

Je reviens:

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d9"),
      "id":121,
      "value":"2",
      "contain":[ 100, 120 ]
    }
  ]
}

Mais je veux uniquement les enfants qui correspondent à "valeur: 1"

Au final j'attends ce résultat:

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    }
  ]
}
20

La question ici concerne en fait quelque chose de différent et n'a pas besoin de $lookup du tout. Mais pour ceux qui arrivent ici uniquement à partir du titre de "filtrage après recherche $", voici les techniques pour vous:

MongoDB 3.6 - Sous-pipeline

db.test.aggregate([
    { "$match": { "id": 100 } },
    { "$lookup": {
      "from": "test",
      "let": { "id": "$id" },
      "pipeline": [
        { "$match": {
          "value": "1",
          "$expr": { "$in": [ "$$id", "$contain" ] }
        }}
      ],
      "as": "childs"
    }}
])

Plus tôt - $ lookup + $ unwind + $ match coalescence

db.test.aggregate([
    { "$match": { "id": 100 } },
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$unwind": "$childs" },
    { "$match": { "childs.value": "1" } },
    { "$group": {
        "_id": "$_id",
        "id": { "$first": "$id" },
        "value": { "$first": "$value" },
        "contain": { "$first": "$contain" },
        "childs": { "$Push": "$childs" }
     }}
])

Si vous vous demandez pourquoi voudriez-vous $unwind au lieu d'utiliser $filter sur le tableau, puis lisez Recherche agrégée $ La taille totale des documents dans le pipeline correspondant dépasse la taille maximale du document pour tous les détails expliquant pourquoi cela est généralement nécessaire et beaucoup plus optimal.

Pour les versions de MongoDB 3.6 et ultérieures, le "sous-pipeline" le plus expressif est généralement ce que vous voulez "filtrer" les résultats de la collection étrangère avant que quoi que ce soit ne soit retourné dans le tableau.

Mais revenons à la réponse qui décrit en fait pourquoi la question posée n'a pas besoin du tout de "jointure" ....


Original

En utilisant $lookup comme ceci n'est pas la manière la plus "efficace" de faire ce que vous voulez ici. Mais plus à ce sujet plus tard.

Comme concept de base, utilisez simplement $filter sur le tableau résultant:

db.test.aggregate([ 
    { "$match": { "id": 100 } }, 
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$project": {
        "id": 1,
        "value": 1,
        "contain": 1,
        "childs": {
           "$filter": {
               "input": "$childs",
               "as": "child",
               "cond": { "$eq": [ "$$child.value", "1" ] }
           }
        }
    }}
]);

Ou utiliser $redact à la place:

db.test.aggregate([ 
    { "$match": { "id": 100 } }, 
    { "$lookup": {
        "from": "test",
        "localField": "id",
        "foreignField": "contain",
        "as": "childs"
    }},
    { "$redact": {
        "$cond": {
           "if": {
              "$or": [
                { "$eq": [ "$value", "0" ] },
                { "$eq": [ "$value", "1" ] }
              ]
           },
           "then": "$$DESCEND",
           "else": "$$Prune"
        }
    }}
]);

Les deux obtiennent le même résultat:

{  
  "_id":ObjectId("570557d4094a4514fc1291d6"),
  "id":100,
  "value":"0",
  "contain":[ ],
  "childs":[ {  
      "_id":ObjectId("570557d4094a4514fc1291d7"),
      "id":110,
      "value":"1",
      "contain":[ 100 ]
    },
    {  
      "_id":ObjectId("570557d4094a4514fc1291d8"),
      "id":120,
      "value":"1",
      "contain":[ 100 ]
    }
  ]
}

En bout de ligne, c'est que $lookup lui-même ne peut pas "encore" interroger pour sélectionner uniquement certaines données. Ainsi, tout "filtrage" doit se produire après le $lookup

Mais vraiment pour ce type de "self join", il vaut mieux ne pas utiliser $lookup tout en évitant les frais généraux d'une lecture supplémentaire et d'une "fusion de hachage". Récupérez simplement les éléments associés et $group à la place:

db.test.aggregate([
  { "$match": { 
    "$or": [
      { "id": 100 },
      { "contain.0": 100, "value": "1" }
    ]
  }},
  { "$group": {
    "_id": {
      "$cond": {
        "if": { "$eq": [ "$value", "0" ] },
        "then": "$id",
        "else": { "$arrayElemAt": [ "$contain", 0 ] }
      }
    },
    "value": { "$first": { "$literal": "0"} },
    "childs": {
      "$Push": {
        "$cond": {
          "if": { "$ne": [ "$value", "0" ] },
          "then": "$$ROOT",
          "else": null
        }
      }
    }
  }},
  { "$project": {
    "value": 1,
    "childs": {
      "$filter": {
        "input": "$childs",
        "as": "child",
        "cond": { "$ne": [ "$$child", null ] }
      }
    }
  }}
])

Ce qui ne sort que légèrement différent car j'ai délibérément supprimé les champs superflus. Ajoutez-les en vous si vous voulez vraiment:

{
  "_id" : 100,
  "value" : "0",
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [ 100 ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [ 100 ]
    }
  ]
}

Donc, le seul vrai problème ici est le "filtrage" de tout null résultat du tableau, créé lorsque le document actuel était le parent dans le traitement des éléments en $Push .


Ce que vous semblez également manquer ici, c'est que le résultat que vous recherchez n'a pas du tout besoin d'agrégation ou de "sous-requêtes". La structure que vous avez conclue ou éventuellement trouvée ailleurs est "conçue" afin que vous puissiez obtenir un "nœud" et tous ses "enfants" dans une seule requête.

Cela signifie simplement que la "requête" est tout ce qui est vraiment nécessaire, et la collecte de données (qui est tout ce qui se passe car aucun contenu n'est réellement "réduit") est juste une fonction de l'itération du résultat du curseur:

var result = {};

db.test.find({
  "$or": [
    { "id": 100 },
    { "contain.0": 100, "value": "1" }
  ]
}).sort({ "contain.0": 1 }).forEach(function(doc) {
  if ( doc.id == 100 ) {
    result = doc;
    result.childs = []
  } else {
    result.childs.Push(doc)
  }
})

printjson(result);

Cela fait exactement la même chose:

{
  "_id" : ObjectId("570557d4094a4514fc1291d6"),
  "id" : 100,
  "value" : "0",
  "contain" : [ ],
  "childs" : [
    {
      "_id" : ObjectId("570557d4094a4514fc1291d7"),
      "id" : 110,
      "value" : "1",
      "contain" : [
              100
      ]
    },
    {
      "_id" : ObjectId("570557d4094a4514fc1291d8"),
      "id" : 120,
      "value" : "1",
      "contain" : [
              100
      ]
    }
  ]
}

Et sert de preuve que tout ce que vous avez vraiment besoin de faire ici est d'émettre la requête "unique" pour sélectionner à la fois le parent et les enfants. Les données renvoyées sont exactement les mêmes et tout ce que vous faites sur le serveur ou le client consiste à "masser" dans un autre format collecté.

Il s'agit de l'un de ces cas où vous pouvez "vous laisser prendre" en pensant à la façon dont vous avez fait les choses dans une base de données "relationnelle" et ne pas vous rendre compte que puisque la façon dont les données sont stockées a "changé", vous n'avez plus besoin d'utiliser la même approche.

C'est exactement ce que le point de l'exemple de documentation "Structures d'arbre de modèle avec des références enfants" dans sa structure, où il fait il est facile de sélectionner les parents et les enfants dans une même requête.

61
Neil Lunn