Som en snabb notering måste du ändra ditt "värde"
fältet i "värden"
att vara numerisk, eftersom det för närvarande är en sträng. Men till svaret:
Om du har tillgång till $reduce
från MongoDB 3.4, så kan du faktiskt göra något så här:
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" }
}
}
}
}}
])
Om du har MongoDB 3.6 kan du rensa upp det lite med $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" } }
]
}
}
}
}}
])
Men det är mer eller mindre samma sak förutom att vi behåller additionalData
Om du går tillbaka lite innan dess kan du alltid $unwind
"städer"
att ackumulera:
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" }
}
}
}
}}
])
Alla returnerar (nästan) samma sak:
{
"_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
}
]
}
De två första formerna är naturligtvis det mest optimala att göra eftersom de helt enkelt arbetar "inom" samma dokument hela tiden.
Operatörer som $reduce
tillåt "ackumulation"-uttryck på arrayer, så vi kan använda det här för att behålla en "reducerad" array som vi testar för det unika "_id"
värde med $indexOfArray
för att se om det redan finns en ackumulerad artikel som matchar. Ett resultat av -1
betyder att den inte finns där.
För att konstruera en "reducerad array" tar vi "initialValue"
av []
som en tom array och lägg sedan till den via $concatArrays
. Hela den processen avgörs via den "ternära" $cond
operator som beaktar "if"
condition och "då"
antingen "ansluter sig" till utdata från $filter
på det aktuella $$värdet
för att utesluta det aktuella indexet _id
post, med naturligtvis en annan "array" som representerar singularobjektet.
För det "objektet" använder vi återigen $indexOfArray
för att faktiskt få det matchade indexet eftersom vi vet att objektet "finns där", och använda det för att extrahera den nuvarande "besökt"
värde från den posten via $arrayElemAt
och $add
till den för att öka.
I "annat"
I detta fall lägger vi helt enkelt till en "array" som ett "objekt" som bara har en standard "besökt"
värdet 1
. Genom att använda båda dessa fall ackumuleras effektivt unika värden inom arrayen för utmatning.
I den senare versionen är vi bara $unwind
arrayen och använd successivt $group
steg för att först "räkna" på de unika inre posterna och sedan "rekonstruera arrayen" till liknande form.
Använda $unwind
ser mycket enklare ut, men eftersom vad det faktiskt gör är att ta en kopia av dokumentet för varje array-post, så lägger detta faktiskt till avsevärd overhead till bearbetningen. I moderna versioner finns det generellt arrayoperatorer som innebär att du inte behöver använda detta om inte din avsikt är att "ackumulera över dokument". Så om du faktiskt behöver $group
på ett värde på en nyckel från "inuti" en array, så är det där du faktiskt behöver använda den.
När det gäller "variabler"
då kan vi helt enkelt använda $filter
här igen för att få den matchande "Budget"
inträde. Vi gör detta som indata till $map
operatör som tillåter "omformning" av arrayinnehållet. Vi vill främst ha det så att du kan ta innehållet i "värdena"
(när du har gjort allt numeriskt) och använd $avg
operator, som tillhandahålls att "fältvägsnotation" bildar direkt till matrisvärdena eftersom den faktiskt kan returnera ett resultat från en sådan inmatning.
Det gör i allmänhet att i stort sett ALLA de viktigaste "array-operatörerna" för aggregeringspipelinen (exklusive "set"-operatörerna) alla inom ett enda pipelinesteg.
Glöm heller aldrig att du nästan alltid vill $match
med vanliga Frågeoperatörer
som det "allra första steget" i en aggregeringspipeline för att bara välja de dokument du behöver. Helst använder du ett index.
Alternativ
Suppleanter arbetar igenom dokumenten i klientkoden. Det skulle i allmänhet inte rekommenderas eftersom alla metoder ovan visar att de faktiskt "minskar" innehållet som returneras från servern, vilket i allmänhet är poängen med "serveraggregationer".
Det "kan" vara möjligt på grund av den "dokumentbaserade" naturen att större resultatuppsättningar kan ta betydligt längre tid med $unwind
och klientbearbetning kan vara ett alternativ, men jag anser att det är mycket mer troligt
Nedan finns en lista som visar att en transformering tillämpas på markörströmmen när resultat returneras och gör samma sak. Det finns tre demonstrerade versioner av transformationen, som visar "exakt" samma logik som ovan, en implementering med lodash
metoder för ackumulering och en "naturlig" ackumulering på Kartan
implementering:
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()
}
})()