Агрегация поиска MongoDB$, приводящая к вложенному массиву

#mongodb #aggregation-framework

Вопрос:

У меня есть две коллекции, которые находятся в отношениях «многие к одному» (http-службы нескольких хостов часто обслуживают «одно и то же», например, балансировка нагрузки на уровне DNS). Я пытаюсь построить запрос, возвращающий соответствующие документы (из двух коллекций), объединенные в одну.

коллекция хостов:

 {
    "_id" : ObjectId("60aa2485332483cb4f5e7122"),
    "ip" : "1.2.3.4",
    "services" : [
        {
            "proto" : "tcp",
            "port" : "22",
            "status" : "open",
            "reason" : "syn-ack",
            "ttl" : 53,
        },
        {
            "proto" : "tcp",
            "port" : "80",
            "status" : "open",
            "reason" : "syn-ack",
            "ttl" : 51,
            "http" : [
                ObjectId("60aa64c67d0bf23ce47c530c")
            ]
        }
    ],
    "version" : 4,
    "last_scanned" : 1621573240.730579,
 

коллекция https:

 {
    "_id" : ObjectId("60aa64c67d0bf23ce47c530c"),
    "vhost" : "test.com",
    "paths" : [
        {
            "path" : "/admin",
            "code" : 200
        },
        {
            "path" : "/stuff",
            "code" : 200
        }
    ]
}
 

Я хотел бы написать поиск, в котором вывод представляет собой комбинацию этих двух коллекций. До сих пор мне удавалось поместить https-документ в массив верхнего уровня на хостах:

 db.hosts.aggregate([                                                                                                                                       
  {                                                                             
    $lookup:                                                                    
        {                                                                       
            from: "https",                                                      
            localField: "services.http",                                        
            foreignField: "_id",                                                
            as: 'http'                                                 
        }                                                                       
  }                                                                             
]).pretty()
 

Что заканчивается так:

 {
    "_id" : ObjectId("60aa2485332483cb4f5e7122"),
    "ip" : "1.2.3.4",
    "services" : [
        {
            "proto" : "tcp",
            "port" : "22",
            "status" : "open",
            "reason" : "syn-ack",
            "ttl" : 53,
        },
        {
            "proto" : "tcp",
            "port" : "80",
            "status" : "open",
            "reason" : "syn-ack",
            "ttl" : 51,
            "http" : [
                ObjectId("60aa64c67d0bf23ce47c530c")
            ]
        }
    ],
    "http" : [
        {
            "_id" : ObjectId("60aa64c67d0bf23ce47c530c"),
            "vhost" : "test.com",
            "paths" : [
                {
                    "path" : "/admin",
                    "code" : 200
                },
                {
                    "path" : "/stuff",
                    "code" : 200
                }
            ]
        }
    ]
    "version" : 4,
    "last_scanned" : 1621573240.730579
    ]
}
 

Проблема в том, что я не могу переместить поле «http» в то место, где его идентификатор объекта был найден путем поиска (services.$.http). Я пытался изменить поле » как » в $lookup различными способами, но безуспешно.

Можно ли вообще указывать на более низкие уровни вложенного документа с помощью «как»? Есть ли обходные пути для достижения этой цели?

Ответ №1:

  • $unwind деконструировать массив услуг
  • $lookup с https и установить as как services.http
  • $group по _id и реконструировать services массив и задать другие обязательные поля
 db.hosts.aggregate([
  { $unwind: "$services" },
  {
    $lookup: {
      from: "https",
      localField: "services.http",
      foreignField: "_id",
      as: "services.http"
    }
  },
  {
    $group: {
      _id: "$_id",
      ip: { $first: "$ip" },
      services: { $push: "$services" },
      version: { $first: "$version" },
      last_scanned: { $first: "$last_scanned" }
    }
  }
]).pretty()
 

Игровая площадка


Второй вариант без $unwind ,

  • $lookup с https коллекцией
  • $map для повторения цикла services массива
  • $filter для повторения цикла http результата, полученного из поиска
  • $ifNull вернет пустое [], если поле равно нулю / не найдено
  • $mergeObjects для объединения текущего объекта services и отфильтрованного http массива
  • http результат массива сейчас не нужен, поэтому удалите его с помощью $$REMOVE
 db.hosts.aggregate([
  {
    $lookup: {
      from: "https",
      localField: "services.http",
      foreignField: "_id",
      as: "http"
    }
  },
  {
    $addFields: {
      services: {
        $map: {
          input: "$services",
          as: "s",
          in: {
            $mergeObjects: [
              "$s",
              {
                http: {
                  $filter: {
                    input: "$http",
                    cond: {
                      $in: ["$this._id", { $ifNull: ["$s.http", []] }]
                    }
                  }
                }
              }
            ]
          }
        }
      },
      http: "$REMOVE"
    }
  }
])
 

Игровая площадка

Комментарии:

1. Спасибо, похоже, это делает то, что я хочу. Я был на правильном пути, просто неправильно использовал группу. Возвращаясь к дальнейшему эксперименту…

2. Единственным недостатком, который я вижу, является то, что каждое поле в разделе «хосты» необходимо добавить в группу$, поэтому гибкость документа зависит от конвейера агрегирования. Это не большая проблема, банкомат, но есть ли какой-нибудь способ избежать этого?

3. нет другого варианта использования, когда мы используем $unwind и $gruoup, но есть вариант без $unwind, это вызовет проблемы с производительностью при большом количестве данных.

4. смотрите, я добавил второй вариант без $unwind.