Als kurze Anmerkung:Sie müssen Ihren "Wert"
ändern Feld innerhalb des "values"
numerisch sein, da es derzeit eine Zeichenfolge ist. Aber weiter zur Antwort:
Wenn Sie Zugriff auf $reduce
haben
von MongoDB 3.4, dann können Sie tatsächlich so etwas tun:
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" }
}
}
}
}}
])
Wenn Sie MongoDB 3.6 haben, können Sie das mit $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" } }
]
}
}
}
}}
])
Aber es ist mehr oder weniger dasselbe, außer dass wir die additionalData
behalten
Gehen wir davor ein wenig zurück, dann können Sie sich jederzeit $entspannen
die "Städte"
zu akkumulieren:
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" }
}
}
}
}}
])
Alle geben (fast) dasselbe zurück:
{
"_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
}
]
}
Die ersten beiden Formen sind natürlich am besten geeignet, da sie einfach immer "innerhalb" desselben Dokuments arbeiten.
Operatoren wie $reduce
"Akkumulation"-Ausdrücke für Arrays zulassen, sodass wir es hier verwenden können, um ein "reduziertes" Array zu behalten, das wir auf die eindeutige "_id"
testen Wert mit $indexOfArray
um zu sehen, ob es bereits einen passenden gesammelten Artikel gibt. Ein Ergebnis von -1
bedeutet, dass es nicht da ist.
Um ein "reduziertes Array" zu konstruieren, nehmen wir den "initialValue"
von []
als leeres Array und fügen Sie es dann über $concatArrays
. All dieser Prozess wird über den „ternären“ $cond entschieden
Operator, der das "if"
berücksichtigt Bedingung und "dann"
entweder "verbindet" die Ausgabe von $filter
auf dem aktuellen $$value
um den aktuellen Index _id
auszuschließen Eintrag, mit natürlich einem weiteren "Array", das das einzelne Objekt darstellt.
Für dieses "Objekt" verwenden wir wieder den $indexOfArray
um tatsächlich den übereinstimmenden Index zu erhalten, da wir wissen, dass das Element "vorhanden" ist, und diesen verwenden, um den aktuellen "visited"
zu extrahieren Wert aus diesem Eintrag über $arrayElemAt
und $add
hinzu, um zu inkrementieren.
Im "else"
In diesem Fall fügen wir einfach ein "Array" als "Objekt" hinzu, das nur einen Standard "visited"
hat Wert von 1
. Die Verwendung dieser beiden Fälle akkumuliert effektiv eindeutige Werte innerhalb des auszugebenden Arrays.
In letzterer Version verwenden wir nur $unwind
das Array und verwenden Sie nacheinander $group
Phasen, um zuerst mit den eindeutigen inneren Einträgen zu "zählen" und dann das Array in die ähnliche Form "wieder aufzubauen".
Verwenden von $unwind
sieht viel einfacher aus, aber da es tatsächlich eine Kopie des Dokuments für jeden Array-Eintrag erstellt, fügt dies der Verarbeitung erheblichen Overhead hinzu. In modernen Versionen gibt es im Allgemeinen Array-Operatoren, was bedeutet, dass Sie diese nicht verwenden müssen, es sei denn, Sie beabsichtigen, "dokumentenübergreifend zu akkumulieren". Wenn Sie also tatsächlich $group
benötigen
auf einen Wert eines Schlüssels "innerhalb" eines Arrays, dann müssen Sie ihn tatsächlich dort verwenden.
Wie für die "Variablen"
dann können wir einfach den $filter
verwenden
nochmal hier um das passende "Budget"
zu erhalten Eintrag. Wir tun dies als Eingabe für $map
-Operator, der eine "Umformung" des Array-Inhalts ermöglicht. Das wollen wir hauptsächlich, damit Sie den Inhalt der "Werte"
übernehmen können ( sobald Sie alles numerisch gemacht haben ) und verwenden Sie den $avg
-Operator, der diese "Feldpfad-Notation" direkt an die Array-Werte liefert, da er tatsächlich ein Ergebnis aus einer solchen Eingabe zurückgeben kann.
Das macht im Allgemeinen die Tour durch so ziemlich ALLE wichtigen "Array-Operatoren" für die Aggregations-Pipeline (mit Ausnahme der "Set"-Operatoren) alle innerhalb einer einzigen Pipeline-Phase.
Vergessen Sie auch nie, dass Sie fast immer möchten $match
mit regulären Abfrageoperatoren
als "allererste Stufe" jeder Aggregationspipeline, um nur die benötigten Dokumente auszuwählen. Idealerweise mit einem Index.
Alternativen
Stellvertreter arbeiten die Dokumente im Client-Code durch. Dies wird im Allgemeinen nicht empfohlen, da alle oben genannten Methoden zeigen, dass sie den vom Server zurückgegebenen Inhalt tatsächlich „reduzieren“, wie es im Allgemeinen der Punkt von „Server-Aggregationen“ ist.
Aufgrund der "dokumentbasierten" Natur "kann" es möglich sein, dass größere Ergebnismengen mit $unwind
erheblich mehr Zeit in Anspruch nehmen und Client-Verarbeitung wäre eine Option, aber ich würde es für viel wahrscheinlicher halten
Nachfolgend finden Sie eine Auflistung, die die Anwendung einer Transformation auf den Cursor-Stream zeigt, da Ergebnisse zurückgegeben werden, die dasselbe tun. Es gibt drei demonstrierte Versionen der Transformation, die "exakt" dieselbe Logik wie oben zeigen, eine Implementierung mit lodash
Methoden zur Akkumulation und eine "natürliche" Akkumulation auf der Map
Implementierung:
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()
}
})()