Подтвердить что ты не робот

Фильтр агрегации после $lookup

Как добавить фильтр после $lookup или есть ли какой-нибудь другой способ сделать это?

Мой тест сбора данных:

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

Я выбираю id 100 и объединяю дочерние элементы:

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

Я возвращаюсь:

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

Но я хочу, чтобы только дочерние элементы соответствовали "value: 1"

В конце я ожидаю этот результат:

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

Ответ 1

Вопрос здесь на самом деле о чем-то другом и вообще не нуждается в $lookup. Но для тех, кто прибывает сюда исключительно из названия "фильтрация после $ lookup", эти методы для вас:

MongoDB 3.6 - Подотрубопровод

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

Ранее - объединение $ lookup + $ unwind + $ match

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

Если вы задаетесь вопросом, зачем вам $unwind а не использовать $filter для массива, тогда читайте Aggregate $ lookup. Общий размер документов в соответствующем конвейере превышает максимальный размер документа для всех деталей, почему это обычно необходимо и гораздо более оптимально.

В выпусках MongoDB 3.6 и более поздних версиях более выразительный "суб-конвейер" - это, как правило, то, что вы хотите "отфильтровать" результаты сторонней коллекции, прежде чем что-либо вообще будет возвращено в массив.

Вернемся к ответу, который фактически описывает, почему задаваемый вопрос вообще не нуждается в присоединении....


оригинал

Использование $lookup как это не самый "эффективный" способ сделать то, что вы хотите здесь. Но об этом позже.

В качестве основной концепции, просто используйте $filter для результирующего массива:

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

Или используйте вместо этого $redact:

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

Оба получают одинаковый результат:

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

Суть в том, что сам $lookup не может "пока" запрашивать только определенные данные. Таким образом, вся "фильтрация" должна произойти после $lookup

Но на самом деле для такого типа "самостоятельного соединения" лучше не использовать $lookup вообще и полностью избегать дополнительных затрат на чтение и "слияние хешей". Просто получите связанные элементы и $group вместо этого:

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

Который только немного отличается, потому что я намеренно удалил посторонние поля. Добавьте их в себя, если вы действительно хотите:

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

Таким образом, единственной реальной проблемой здесь является "фильтрация" любого null результата из массива, созданного, когда текущий документ был parent при обработке элементов для $push.


Похоже, вам здесь также не хватает того, что искомый результат не нуждается в агрегировании или "подзапросах". Структура, которую вы заключили или, возможно, нашли в другом месте, "спроектирована" так, чтобы вы могли получить "узел" и все его "потомки" в одном запросе.

Это означает, что просто "запрос" - это все, что действительно необходимо, а сбор данных (то есть все, что происходит, так как контент на самом деле не "сокращается") - это просто функция повторения результата курсора:

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

Это делает то же самое:

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

И служит доказательством того, что все, что вам действительно нужно сделать, - это выполнить "одиночный" запрос, чтобы выбрать и родителя, и потомка. Возвращенные данные точно такие же, и все, что вы делаете на сервере или клиенте, это "массирует" в другой собранный формат.

Это один из тех случаев, когда вы можете "догнать" себя, думая о том, как вы работали в "реляционной" базе данных, и не понимать, что, поскольку способ хранения данных "изменился", вам больше не нужно использовать тот же подход.

Это именно то, что указывает на пример документации "Структуры дерева моделей с дочерними ссылками" в его структуре, где он позволяет легко выбирать родителей и детей в одном запросе.