Come nota veloce, devi cambiare il tuo "value"
campo all'interno del "values"
essere numerico, poiché attualmente è una stringa. Ma alla risposta:
Se hai accesso a $reduce
da MongoDB 3.4, puoi effettivamente fare qualcosa del genere:
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" }
}
}
}
}}
])
Se hai MongoDB 3.6, puoi pulirlo un po' con $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" } }
]
}
}
}
}}
])
Ma è più o meno la stessa cosa, tranne per il fatto che manteniamo i additionalData
Tornando indietro un po' prima, puoi sempre $unwind
il "cities"
accumulare:
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" }
}
}
}
}}
])
Tutti restituiscono (quasi) la stessa cosa:
{
"_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
}
]
}
I primi due moduli sono ovviamente la cosa più ottimale da fare poiché funzionano semplicemente "all'interno" dello stesso documento in ogni momento.
Operatori come $reduce
consentire espressioni di "accumulo" sugli array, quindi possiamo usarlo qui per mantenere un array "ridotto" che testiamo per l'unico "_id"
valore utilizzando $indexOfArray
per vedere se c'è già un oggetto accumulato che corrisponde. Un risultato di -1
significa che non c'è.
Per costruire un "array ridotto" prendiamo il "initialValue"
di []
come array vuoto e quindi aggiungerlo tramite $concatArrays
. Tutto questo processo viene deciso tramite il "ternario" $cond
operatore che considera "if"
condizione e "then"
o "unisce" l'output di $filter
sul $$value
corrente per escludere l'indice corrente _id
entry, con ovviamente un altro "array" che rappresenta l'oggetto singolare.
Per quell'"oggetto" utilizziamo di nuovo $indexOfArray
per ottenere effettivamente l'indice corrispondente poiché sappiamo che l'elemento "è presente" e utilizzarlo per estrarre l'attuale "visited"
valore da quella voce tramite $arrayElemAt
e $add
ad esso per incrementare.
Nel "else"
caso aggiungiamo semplicemente un "array" come "oggetto" che ha solo un predefinito "visited"
valore di 1
. L'utilizzo di entrambi i casi consente di accumulare valori univoci all'interno dell'array da generare.
Nell'ultima versione, abbiamo solo $unwind
l'array e utilizzare $group
fasi per "contare" prima le voci interne univoche, quindi "ricostruire l'array" nella forma simile.
Utilizzo di $unwind
sembra molto più semplice, ma poiché ciò che fa effettivamente è prendere una copia del documento per ogni voce dell'array, ciò aggiunge effettivamente un notevole sovraccarico all'elaborazione. Nelle versioni moderne ci sono generalmente operatori di array, il che significa che non è necessario utilizzarli a meno che la tua intenzione non sia quella di "accumulare tra documenti". Quindi, se hai effettivamente bisogno di $group
su un valore di una chiave da "dentro" un array, allora è lì che devi effettivamente usarlo.
Per quanto riguarda le "variables"
quindi possiamo semplicemente usare $filter
di nuovo qui per ottenere il "Budget"
corrispondente iscrizione. Lo facciamo come input per $map
operatore che consente il "rimodellamento" del contenuto dell'array. Lo vogliamo principalmente in modo che tu possa prendere il contenuto dei "values"
(una volta che hai reso tutto numerico) e usa $avg
operatore, a cui viene fornito il formato di "notazione del percorso del campo" direttamente ai valori dell'array perché può effettivamente restituire un risultato da tale input.
Ciò rende generalmente il tour di praticamente TUTTI i principali "operatori di array" per la pipeline di aggregazione (esclusi gli operatori "set") all'interno di un'unica fase della pipeline.
Inoltre, non dimenticare mai che desideri quasi sempre $match
con i normali Operatori di query
come il "primo stadio" di qualsiasi pipeline di aggregazione per selezionare solo i documenti di cui hai bisogno. Idealmente usando un indice.
Alternative
I supplenti stanno lavorando sui documenti nel codice client. In genere non sarebbe raccomandato poiché tutti i metodi sopra mostrano che effettivamente "riducono" il contenuto restituito dal server, come generalmente accade per le "aggregazioni del server".
A causa della natura "basata sul documento" "potrebbe" essere possibile che set di risultati più grandi potrebbero richiedere molto più tempo utilizzando $unwind
e l'elaborazione del client potrebbe essere un'opzione, ma la considererei molto più probabile
Di seguito è riportato un elenco che mostra l'applicazione di una trasformazione al flusso del cursore quando i risultati vengono restituiti facendo la stessa cosa. Esistono tre versioni dimostrate della trasformazione, che mostrano "esattamente" la stessa logica di cui sopra, un'implementazione con lodash
metodi di accumulazione e una accumulazione "naturale" sulla Map
attuazione:
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()
}
})()