Sebagai catatan singkat, Anda perlu mengubah "value"
bidang di dalam "values"
menjadi numerik, karena saat ini string. Tapi pada jawabannya:
Jika Anda memiliki akses ke $reduce
dari MongoDB 3.4, maka Anda sebenarnya dapat melakukan sesuatu seperti ini:
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" }
}
}
}
}}
])
Jika Anda memiliki MongoDB 3.6, Anda dapat membersihkannya sedikit dengan $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" } }
]
}
}
}
}}
])
Tapi kurang lebih sama kecuali kita menyimpan additionalData
Kembali sedikit sebelum itu, maka Anda selalu dapat $unwind
"cities"
untuk mengumpulkan:
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" }
}
}
}
}}
])
Semua mengembalikan (hampir) hal yang sama:
{
"_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
}
]
}
Dua formulir pertama tentu saja merupakan hal yang paling optimal untuk dilakukan karena hanya bekerja "di dalam" dokumen yang sama setiap saat.
Operator seperti $reduce
izinkan ekspresi "akumulasi" pada array, sehingga kita dapat menggunakannya di sini untuk menyimpan array "dikurangi" yang kita uji untuk "_id"
yang unik nilai menggunakan $indexOfArray
untuk melihat apakah sudah ada akumulasi item yang cocok. Hasil dari -1
berarti tidak ada.
Untuk membuat "array tereduksi" kita mengambil "initialValue"
dari []
sebagai array kosong dan kemudian menambahkannya melalui $concatArrays
. Semua proses itu diputuskan melalui "ternary" $cond
operator yang menganggap "if"
kondisi dan "then"
baik "bergabung" dengan output $filter
pada $$value
saat ini untuk mengecualikan indeks saat ini _id
entri, dengan tentu saja "array" lain yang mewakili objek tunggal.
Untuk "objek" itu kita kembali menggunakan $indexOfArray
untuk benar-benar mendapatkan indeks yang cocok karena kita tahu bahwa item "ada", dan menggunakannya untuk mengekstrak "visited"
saat ini nilai dari entri tersebut melalui $arrayElemAt
dan $add
untuk meningkatkannya.
Di "else"
kasus kita cukup menambahkan "array" sebagai "objek" yang hanya memiliki default "visited"
nilai 1
. Menggunakan kedua kasus tersebut secara efektif mengakumulasikan nilai unik dalam larik ke output.
Di versi terakhir, kami hanya $unwind
array dan gunakan berturut-turut $group
tahapan untuk pertama-tama "menghitung" pada entri dalam yang unik, lalu "membangun ulang array" ke dalam bentuk yang serupa.
Menggunakan $unwind
terlihat jauh lebih sederhana, tetapi karena apa yang sebenarnya dilakukan adalah mengambil salinan dokumen untuk setiap entri array, maka ini sebenarnya menambah banyak biaya untuk pemrosesan. Dalam versi modern umumnya ada operator array yang berarti Anda tidak perlu menggunakan ini kecuali niat Anda adalah untuk "berakumulasi di seluruh dokumen". Jadi, jika Anda benar-benar perlu $group
pada nilai kunci dari "dalam" sebuah array, maka di situlah Anda benar-benar perlu menggunakannya.
Adapun "variables"
maka kita cukup menggunakan $filter
lagi di sini untuk mendapatkan "Budget"
yang cocok pintu masuk. Kami melakukan ini sebagai masukan ke $map
operator yang memungkinkan "membentuk kembali" konten array. Kami terutama menginginkannya agar Anda dapat mengambil konten "values"
( setelah Anda membuat semuanya numerik ) dan gunakan $avg
operator, yang diberikan bahwa "notasi jalur bidang" langsung membentuk nilai larik karena sebenarnya ia dapat mengembalikan hasil dari input semacam itu.
Itu umumnya membuat tur hampir SEMUA "operator array" utama untuk pipa agregasi (tidak termasuk operator "set") semuanya dalam satu tahap pipa.
Juga jangan pernah lupa bahwa Anda selalu ingin $match
dengan Operator Kueri
reguler sebagai "tahap pertama" dari saluran agregasi apa pun untuk memilih dokumen yang Anda butuhkan. Idealnya menggunakan indeks.
Alternatif
Alternatif bekerja melalui dokumen dalam kode klien. Biasanya tidak direkomendasikan karena semua metode di atas menunjukkan bahwa mereka benar-benar "mengurangi" konten seperti yang dikembalikan dari server, seperti umumnya titik "agregasi server".
Ini "mungkin" dimungkinkan karena sifat "berbasis dokumen" bahwa kumpulan hasil yang lebih besar mungkin membutuhkan lebih banyak waktu menggunakan $unwind
dan pemrosesan klien bisa menjadi opsi, tetapi saya akan menganggapnya jauh lebih mungkin
Di bawah ini adalah daftar yang menunjukkan penerapan transformasi ke aliran kursor saat hasil dikembalikan dengan melakukan hal yang sama. Ada tiga versi transformasi yang didemonstrasikan, menunjukkan "persis" logika yang sama seperti di atas, sebuah implementasi dengan lodash
metode akumulasi, dan akumulasi "alami" pada Map
implementasi:
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()
}
})()