Като бърза бележка, трябва да промените своята "стойност"
поле вътре в "стойности"
да бъде числова, тъй като в момента е низ. Но към отговора:
Ако имате достъп до $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
"градовете"
за натрупване:
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“
от []
като празен масив и след това добавете към него чрез $concatArraysкод>
. Целият този процес се решава чрез "троичния" $cond
оператор, който разглежда "if"
условие и "тогава"
или се „присъединява“ към изхода на $filter
върху текущата $$value
за да изключите текущия индекс _id
запис, разбира се с друг "масив", представляващ единствения обект.
За този „обект“ отново използваме $indexOfArrayкод>
действително да получим съответстващия индекс, тъй като знаем, че елементът „е там“, и да го използваме, за да извлечем текущия „visited“
стойност от този запис чрез $arrayElemAt
и $add
към него, за да се увеличи.
В „друго“
в случай, че просто добавяме "масив" като "обект", който просто има "посетен"
по подразбиране стойност 1
. Използването на тези два случая ефективно натрупва уникални стойности в масива за извеждане.
В последната версия ние просто $unwind
масива и използвайте последователни $group
етапи, за да "отчитаме" първо уникалните вътрешни записи и след това да "реконструираме масива" в подобна форма.
Използване на $unwind
изглежда много по-просто, но тъй като това, което всъщност прави, е да вземе копие на документа за всеки запис в масив, тогава това всъщност добавя значителни разходи към обработката. В съвременните версии обикновено има оператори за масиви, което означава, че не е необходимо да използвате това, освен ако намерението ви не е да "натрупвате в документи". Така че, ако наистина трябва да $group
на стойност на ключ от „вътрешността“ на масив, тогава това е мястото, където всъщност трябва да го използвате.
Що се отнася до "променливите"
тогава можем просто да използваме $filter
отново тук, за да получите съответстващия „Бюджет“
влизане. Правим това като вход към $map
оператор, който позволява "преоформяне" на съдържанието на масива. Искаме това главно, за да можете да вземете съдържанието на „стойностите“
(след като направите всичко числово) и използвайте $avg
оператор, който се доставя, че "нотацията на пътя на полето" се формира директно към стойностите на масива, защото всъщност може да върне резултат от такъв вход.
Това обикновено прави обиколката на почти ВСИЧКИ основни „оператори на масиви“ за тръбопровода за агрегиране (с изключение на операторите „set“) в рамките на един етап на конвейера.
Също така никога не забравяйте, че почти винаги искате да $match
с обикновени оператори на заявки
като „първия етап“ от всеки тръбопровод за агрегиране, за да изберете само документите, от които се нуждаете. В идеалния случай използване на индекс.
Заместници
Заместниците работят с документите в клиентския код. Като цяло не би било препоръчително, тъй като всички методи по-горе показват, че всъщност „намаляват“ съдържанието, върнато от сървъра, както обикновено е смисълът на „сървърните агрегации“.
„Може“ да е възможно поради „базирания на документ“ характер по-големите набори от резултати да отнемат значително повече време при използване на $unwind
и клиентската обработка може да бъде опция, но бих я сметнал за много по-вероятна
По-долу е даден списък, който демонстрира прилагането на трансформация към потока на курсора, тъй като резултатите се връщат, правейки същото. Има три демонстрирани версии на трансформацията, показващи "точно" същата логика като по-горе, реализация с lodash
методи за натрупване и "естествено" натрупване на Картата
изпълнение:
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()
}
})()