Агрегация накапливает внутренние объекты - PullRequest
0 голосов
/ 21 мая 2018

Я новичок в агрегации Монго, и мне нужна помощь в ее создании,

У меня есть коллекция следующего документа в качестве примера:

{
    "_id" : ObjectId("5afc2f06e1da131c9802071e"),
    "_class" : "Traveler",
    "name" : "John Due",
    "startTimestamp" : 1526476550933,
    "endTimestamp" : 1526476554823,
    "source" : "istanbul",
    "cities" : [ 
        {
            "_id" : "ef8f6b26328f-0663202f94faeaeb-3981",
            "name" : "Moscow",
            "timestamp" : 1526476550940,
            "timeSpent" : 3180
        },

        {
            "_id" : "ef8f6b26328f-0663202f94faeaeb-1122",
            "name" : "Cairo",
            "timestamp" : 1625476550940,
            "timeSpent" : 318000,
        },
         {
            "_id" : "ef8f6b26328f-0663202f94faeaeb-3981",
            "name" : "Moscow",
            "timestamp" : 15211276550940,
            "timeSpent" : 318011
        }


    ],
    "variables" : [ 
        {
            "_id" : "cd4318a83c9b-a8478d76bfd3e4b6-5967",
            "name" : "Customer Profile",
            "lastValue" : "",
            "values" : [],
            "additionalData" : {}
        }, 
        {
            "_id" : "366cb8c07996-c62c37a87a86d526-d3e7",
            "name" : "Target Telephony Queue",
            "lastValue" : "",
            "values" : [],
            "additionalData" : {}
        }, 
        {
            "_id" : "4ed84742da33-d70ba8a809b712f3-bdf4",
            "name" : "IMEI",
            "lastValue" : "",
            "values" : [],
            "additionalData" : {}
        }, 

        {
            "_id" : "c8103687c1c8-97d749e349d785c8-9154",
            "name" : "Budget",
            "defaultValue" : "",
            "lastValue" : "",
            "values" : [ 
                {
                    "value" : "3000",
                    "timestamp" : NumberLong(1526476550940),
                    "element" : "c8103687c1c8-97d749e349d785c8-9154"
                }
            ],
            "additionalData" : {}
        }
    ]
}

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

, поэтому результирующий документ будет похож на:

{
     "_id" : ObjectId("5afc2f06e1da131c9802071e"),
     "_class" : "Traveler",
     "name" : "John Due",
     "startTimestamp" : 1526476550933,
     "endTimestamp" : 1526476554823,
     "source" : "istanbul",
    "cities" : [ 
        {
            "_id" : "ef8f6b26328f-0663202f94faeaeb-3981",
            "name" : "Moscow",
            "visited":2
        },
        {
            "_id" : "ef8f6b26328f-0663202f94faeaeb-1122",
            "name" : "Cairo",
            "visited":1
        }
    ],
    "variables" : [ 
        {
                "_id" : "c8103687c1c8-97d749e349d785c8-9154",
                "name" : "Budget",
                "defaultValue" : "",
                "lastValue" : "",
                "values" : [ 
                    {
                        "value" : "3000",
                    }
                ],
            }
    ],
}

Спасибо за помощь

1 Ответ

0 голосов
/ 21 мая 2018

В качестве быстрой заметки вам необходимо изменить поле "value" внутри "values" на числовое, поскольку в настоящее время оно является строкой.Но ответим на вопрос:

Если у вас есть доступ к $reduce из MongoDB 3.4, то вы можете сделать что-то вроде этого:

db.collection.aggregate([
  { "$addFields": {
     "cities": {
       "$reduce": {
         "input": "$cities",
         "initialValue": [],
         "in": {
           "$cond": {
             "if": { "$ne": [{ "$indexOfArray": ["$$value._id", "$$this._id"] }, -1] },
             "then": {
               "$concatArrays": [
                 { "$filter": {
                   "input": "$$value",
                   "as": "v",
                   "cond": { "$ne": [ "$$this._id", "$$v._id" ] }
                 }},
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": {
                     "$add": [
                       { "$arrayElemAt": [
                         "$$value.visited",
                         { "$indexOfArray": [ "$$value._id", "$$this._id" ] }
                       ]},
                       1
                     ]
                   }
                 }]
               ]
             },
             "else": {
               "$concatArrays": [
                 "$$value",
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": 1
                 }]
               ]
             }
           }
         }
       }
     },
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "_id": "$$this._id",
           "name": "$$this.name",
           "defaultValue": "$$this.defaultValue",
           "lastValue": "$$this.lastValue",
           "value": { "$avg": "$$this.values.value" }
         }
       }
     }
  }}
])

Если выИмея MongoDB 3.6, вы можете немного исправить это с помощью $mergeObjects:

db.collection.aggregate([
  { "$addFields": {
     "cities": {
       "$reduce": {
         "input": "$cities",
         "initialValue": [],
         "in": {
           "$cond": {
             "if": { "$ne": [{ "$indexOfArray": ["$$value._id", "$$this._id"] }, -1] },
             "then": {
               "$concatArrays": [
                 { "$filter": {
                   "input": "$$value",
                   "as": "v",
                   "cond": { "$ne": [ "$$this._id", "$$v._id" ] }
                 }},
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": {
                     "$add": [
                       { "$arrayElemAt": [
                         "$$value.visited",
                         { "$indexOfArray": [ "$$value._id", "$$this._id" ] }
                       ]},
                       1
                     ]
                   }
                 }]
               ]
             },
             "else": {
               "$concatArrays": [
                 "$$value",
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": 1
                 }]
               ]
             }
           }
         }
       }
     },
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "$mergeObjects": [
             "$$this",
             { "values": { "$avg": "$$this.values.value" } }
           ]
         }
       }
     }
  }}
])

Но это более или менее то же самое, за исключением того, что мы оставляем additionalData

Возвращаясь немного раньше, тогда вы всегда можете $unwind "cities" накопить:

db.collection.aggregate([
  { "$unwind": "$cities" },
  { "$group": {
     "_id": { 
       "_id": "$_id",
       "cities": {
         "_id": "$cities._id",
         "name": "$cities.name"
       }
     },
     "_class": { "$first": "$class" },
     "name": { "$first": "$name" },
     "startTimestamp": { "$first": "$startTimestamp" },
     "endTimestamp" : { "$first": "$endTimestamp" },
     "source" : { "$first": "$source" },
     "variables": { "$first": "$variables" },
     "visited": { "$sum": 1 }
  }},
  { "$group": {
     "_id": "$_id._id",
     "_class": { "$first": "$class" },
     "name": { "$first": "$name" },
     "startTimestamp": { "$first": "$startTimestamp" },
     "endTimestamp" : { "$first": "$endTimestamp" },
     "source" : { "$first": "$source" },
     "cities": {
       "$push": {
         "_id": "$_id.cities._id",
         "name": "$_id.cities.name",
         "visited": "$visited"
       }
     },
     "variables": { "$first": "$variables" },
  }},
  { "$addFields": {
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "_id": "$$this._id",
           "name": "$$this.name",
           "defaultValue": "$$this.defaultValue",
           "lastValue": "$$this.lastValue",
           "value": { "$avg": "$$this.values.value" }
         }
       }
     }
  }}
])

Все возвращают (почти) одно и то же:

{
        "_id" : ObjectId("5afc2f06e1da131c9802071e"),
        "_class" : "Traveler",
        "name" : "John Due",
        "startTimestamp" : 1526476550933,
        "endTimestamp" : 1526476554823,
        "source" : "istanbul",
        "cities" : [
                {
                        "_id" : "ef8f6b26328f-0663202f94faeaeb-1122",
                        "name" : "Cairo",
                        "visited" : 1
                },
                {
                        "_id" : "ef8f6b26328f-0663202f94faeaeb-3981",
                        "name" : "Moscow",
                        "visited" : 2
                }
        ],
        "variables" : [
                {
                        "_id" : "c8103687c1c8-97d749e349d785c8-9154",
                        "name" : "Budget",
                        "defaultValue" : "",
                        "lastValue" : "",
                        "value" : 3000
                }
        ]
}

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

Операторы, такие как $reduce разрешить выражения «накопления» для массивов, поэтому мы можем использовать его здесь, чтобы сохранить «сокращенный» массив, который мы проверяем на уникальное значение "_id", используя $indexOfArray, чтобы увидеть, существует ли ужеэто накопленный элемент, который соответствует.Результат -1 означает, что его там нет.

Чтобы создать «уменьшенный массив», мы берем "initialValue" из [] в качестве пустого массива и затем добавляем к нему через * 1045.*.Весь этот процесс решается через «троичный» оператор $cond, который учитывает условие "if", и "then" либо «соединяет» выход $filter втекущий $$value, чтобы исключить текущую запись индекса _id, с, конечно, другим «массивом», представляющим особый объект.

Для этого «объекта» мы снова используем $indexOfArray чтобы получить соответствующий индекс, поскольку мы знаем, что элемент «есть», и использовать его для извлечения текущего значения "visited" из этой записи через $arrayElemAt и $add к нему для приращения.

В случае "else" мы просто добавляем «массив» как «объект», который просто имеет значение по умолчанию "visited" 1.Использование обоих этих случаев эффективно накапливает уникальные значения в массиве для вывода.

В последней версии мы просто $unwind массив и используем последовательные $groupэтапы для того, чтобы сначала «сосчитать» уникальные внутренние записи, а затем «перестроить массив» в похожую форму.

Использование $unwind выглядит намного проще, нотак как на самом деле он берет копию документа для каждой записи массива, то это фактически добавляет значительные накладные расходы на обработку.В современных версиях, как правило, есть операторы массивов, которые означают, что вам не нужно использовать это, если вы не намерены «накапливать документы».Так что если вам действительно нужно $group для значения ключа из «массива», то вам нужно его использовать.

Что касается "variables", тогда мы можем просто снова использовать $filter, чтобы получить соответствующую запись "Budget".Мы делаем это как входные данные для оператора $map, который позволяет «переформировать» содержимое массива.В основном мы хотим, чтобы вы могли взять содержимое "values" (как только вы сделаете все это числовым) и использовать оператор $avg, который передает эту форму «обозначения пути поля» непосредственно взначения массива, потому что он на самом деле может возвращать результат от такого ввода.

Это, как правило, делает обзор почти всех основных "операторов массива" для конвейера агрегации (исключая операторы "набора") всемв пределах одной стадии конвейера.

Также не забывайте, что вы всегда хотите $match с обычными операторами запросов в качестве "самой первой стадии""любого конвейера агрегации, чтобы просто выбрать нужные документы.Идеально использовать индекс.


Альтернативы

Альтернативы работают через документы в коде клиента.Как правило, это не рекомендуется, поскольку все вышеприведенные методы показывают, что они на самом деле «уменьшают» контент, возвращаемый с сервера, как это обычно является точкой «агрегации серверов».

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

Ниже приведен список, демонстрирующий применение преобразования кпоток курсора в качестве результата возвращается, делая то же самое.Существуют три продемонстрированные версии преобразования, показывающие «точно» ту же логику, что и выше, реализацию с lodash методами накопления и «естественное» накопление в реализации Map:

const { MongoClient } = require('mongodb');
const { chain } = require('lodash');

const uri = 'mongodb://localhost:27017';
const opts = { useNewUrlParser: true };

const log = data => console.log(JSON.stringify(data, undefined, 2));

const transform = ({ cities, variables, ...d }) => ({
  ...d,
  cities: cities.reduce((o,{ _id, name }) =>
    (o.map(i => i._id).indexOf(_id) != -1)
      ? [
          ...o.filter(i => i._id != _id),
          { _id, name, visited: o.find(e => e._id === _id).visited + 1 }
        ]
      : [ ...o, { _id, name, visited: 1 } ]
  , []).sort((a,b) => b.visited - a.visited),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))
});

const alternate = ({ cities, variables, ...d }) => ({
  ...d,
  cities: chain(cities)
    .groupBy("_id")
    .toPairs()
    .map(([k,v]) =>
      ({
        ...v.reduce((o,{ _id, name }) => ({ ...o, _id, name }),{}),
        visited: v.length
      })
    )
    .sort((a,b) => b.visited - a.visited)
    .value(),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))

});

const natural = ({ cities, variables, ...d }) => ({
  ...d,
  cities: [
    ...cities
      .reduce((o,{ _id, name }) => o.set(_id,
        [ ...(o.has(_id) ? o.get(_id) : []), { _id, name } ]), new Map())
      .entries()
  ]
  .map(([k,v]) =>
    ({
      ...v.reduce((o,{ _id, name }) => ({ ...o, _id, name }),{}),
      visited: v.length
    })
  )
  .sort((a,b) => b.visited - a.visited),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))

});

(async function() {

  try {

    const client = await MongoClient.connect(uri, opts);

    let db = client.db('test');
    let coll = db.collection('junk');

    let cursor = coll.find().map(natural);

    while (await cursor.hasNext()) {
      let doc = await cursor.next();
      log(doc);
    }

    client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()
...