В качестве быстрой заметки вам необходимо изменить поле "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()
}
})()