#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.