Группируйте вложенные поля с агрегацией и возвращайте результаты с другими полями mongo db

#node.js #mongodb #aggregation-framework #grouping #pipeline

Вопрос:

Мне нужно изменить структуру некоторого поля в моем документе MongoDB.

Вот образец:

 [
    {

        _id: "ObjectId('997v2ha1cv9b0036fa648zx3')",
        title: "Adidas Predator",
        size: "8",
        colors: [
            {
                hex: "005FFF",
                name: "Blue"
            },
            {
                hex: "FF003A",
                name: "Red"
            },
            {
                hex: "FFFE00",
                name: "Yellow"
            },
            {
                hex: "07FF00",
                name: "Green"
            },

        ],
        extras: [
            {
                description: "laces",
                type: "exterior"
            },
            {
                description: "sole",
                type: "interior"
            },
            {
                description: "logo"
            },
            {
                description: "stud",
                type: "exterior"
            }
        ],
        media: {
            images: [
                {
                    url: "http://link.com",
                    type: "exterior"
                },
                {
                    url: "http://link3.com",
                    type: "interior"
                },
                {
                    url: "http://link2.com",
                    type: "interior"
                },
                {
                    url: "http://link4.com",
                    type: "exterior"
                }
            ]
        }
    }
];
 

Моя цель-сгруппировать некоторые поля:
цвета должны быть и массив только с цветами,
дополнительно должен быть массив с 3 объектами каждый для «типа» (внутренний, внешний, нулевой)
то же самое относится и к изображениям, находящимся внутри носителя

Вот чего я ожидал:

 {
    _id: "ObjectId('997b5aa1cv9b0036fa648ab5')",
    title: "Adidas Predator",
    size: "8",
    colors: ["Blue", "Red", "Yellow", "Green"],
    extras: [
        {type: exterior, description: ["laces", "stud"]},
        {type: interior, description: ["sole"]},
        {type: null, description: ["logo"]}
    ],
    images: [
        {type: exterior, url: ["http://link.com", "http://link4.com"]},
        {type: interior, url: ["http://link2.com", "http://link3.com"]},
    ]
};
 

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

 
db.collection.aggregate([
    {
        $project: {
            title: 1,
            size: 1,
            colors: "$colors.name",
            extras: 1,
            media: "$media.images"
        },
    },
    {
        $unwind: "$media"
    },
    {
        $group: {
            _id: {
                type: "$media.type",
                url: "$media.url",
            },
        },
    },
    {
        $group: {
            _id: "$_id.type",
            url: {
                $push: "$_id.url"
            },
        },
    },
]);
 

В результате получается:

 [
    {
        _id: "exterior",
        url: [
            "http://link.com",
            "http://link4.com"
        ]
    },
    {
        _id: "interior",
        url: [
            "http://link3.com",
            "http://link2.com"
        ]
    }
];
 

Если я сделаю то же самое с дополнительными функциями, я получу ту же (правильную) структуру.
Как я могу показать все данные вместе, как в ожидаемой структуре?
Спасибо за совет.

Ответ №1:

Стратегия будет заключаться в том, чтобы поддерживать требуемые родительские поля по всему конвейеру, используя $first только начальное значение, это не очень красиво, но это работает:

 db.collection.aggregate([
  {
    "$addFields": {
      colors: {
        $map: {
          input: "$colors",
          as: "color",
          in: "$color.name"
        }
      }
    }
  },
  {
    $unwind: "$extras"
  },
  {
    "$addFields": {
      imageUrls: {
        $map: {
          input: {
            $filter: {
              input: "$media.images",
              as: "image",
              cond: {
                $eq: [
                  "$image.type",
                  "$extras.type"
                ]
              }
            }
          },
          as: "image",
          in: "$image.url"
        }
      }
    }
  },
  {
    $group: {
      _id: {
        _id: "$_id",
        extraType: "$extras.type"
      },
      extraDescriptions: {
        "$addToSet": "$extras.description"
      },
      imageUrls: {
        "$first": "$imageUrls"
      },
      colors: {
        $first: "$colors"
      },
      size: {
        $first: "$size"
      },
      title: {
        $first: "$title"
      }
    }
  },
  {
    $group: {
      _id: "$_id._id",
      colors: {
        $first: "$colors"
      },
      size: {
        $first: "$size"
      },
      title: {
        $first: "$title"
      },
      images: {
        $push: {
          type: {
            "$ifNull": [
              "$_id.extraType",
              null
            ]
          },
          url: "$imageUrls"
        }
      },
      extras: {
        $push: {
          type: {
            "$ifNull": [
              "$_id.extraType",
              null
            ]
          },
          description: "$extraDescriptions"
        }
      }
    }
  }
])
 

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

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

1. Прежде всего, спасибо за ответ. Мне интересно, есть ли способ использовать вместо этого некоторую агрегацию этапов, например $project

2. Вы имеете в виду проект, а не группу?

3. Я имею в виду использовать больше этапов, таких как группа, вместо группировки с помощью карты, а затем использовать проект в конце конвейера, чтобы показать результат

4. Я предполагаю, что теоретически это возможно, используя $reduce и $filter , но это было бы и кошмаром для написания, и намного, намного хуже с точки зрения эффективности, я не уверен, почему вы хотите пойти этим путем?

Ответ №2:

Вы можете попробовать оператор $function, чтобы определить пользовательскую функцию агрегации или выражение в JavaScript.

  • $project чтобы отобразить необходимые поля и получить массив colors имени
  • $function , напишите свою логику JS, если вам нужно, вы можете отсортировать эту логику группы, она вернет результат с 2 полями ( extras , images )
  • $project чтобы отобразить необходимые поля и отделить extras images поле и от результата
 db.collection.aggregate([
  {
    $project: {
      title: 1,
      size: 1,
      colors: "$colors.name",
      result: {
        $function: {
          body: function(extras, images) {
            function groupBy(objectArray, k, v) {
              var results = [], res = objectArray.reduce((acc, obj) => {
                  if (!acc[obj[k]]) acc[obj[k]] = [];
                  acc[obj[k]].push(obj[v]);
                  return acc;
              }, {});
              for (var o in res) {
                results.push({ [k]: o === 'undefined' ? null : o, [v]: res[o] })
              }
              return results;
            }
            return {
              extras: groupBy(extras, 'type', 'description'),
              images: groupBy(images, 'type', 'url')
            }
          },
          args: ["$extras", "$media.images"],
          lang: "js"
        }
      }
    }
  },
  {
    $project: {
      title: 1,
      size: 1,
      colors: 1,
      extras: "$result.extras",
      images: "$result.images"
    }
  }
])
 

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

важный:
Выполнение JavaScript внутри выражения агрегации может снизить производительность. Используйте $function оператора только в том случае, если предоставленные операторы конвейера не могут удовлетворить потребности вашего приложения.

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

1. Спасибо. Я согласен с разделом «важно», который вы написали. Может быть, ту же операцию можно выполнить с помощью некоторых операторов mongo вместо js?

2. вот почему я добавил последний вариант. я не думаю, что это возможно сделать только с некоторыми операторами mongodb. я бы предположил, что вы также можете выполнять такого рода операции на языке программирования.