Krótko mówiąc, musisz zmienić swoją „wartość”
pole wewnątrz "wartości"
być liczbowym, ponieważ obecnie jest to łańcuch. Ale do odpowiedzi:
Jeśli masz dostęp do $reduce
z MongoDB 3.4, możesz zrobić coś takiego:
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" }
}
}
}
}}
])
Jeśli masz MongoDB 3.6, możesz to trochę posprzątać za pomocą $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" } }
]
}
}
}
}}
])
Ale to mniej więcej to samo, z wyjątkiem tego, że zachowujemy additionalData
Wracając trochę wcześniej, zawsze możesz $unwind
"miasta"
gromadzić:
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" }
}
}
}
}}
])
Wszystkie zwracają (prawie) to samo:
{
"_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
}
]
}
Pierwsze dwa formularze są oczywiście najbardziej optymalnym rozwiązaniem, ponieważ po prostu działają „w” tym samym dokumencie przez cały czas.
Operatory takie jak $reduce
zezwalaj na wyrażenia "akumulacji" na tablicach, więc możemy go tutaj użyć, aby zachować "zredukowaną" tablicę, którą testujemy pod kątem unikalnego "_id"
wartość przy użyciu $indexOfArray
aby sprawdzić, czy istnieje już skumulowany element, który pasuje. Wynik -1
oznacza, że go tam nie ma.
Aby skonstruować "tablicę zredukowaną" bierzemy "initialValue"
z []
jako pustą tablicę, a następnie dodaj do niej za pomocą $concatArrays
. Cały ten proces jest określany przez „trójargumentowy” $cond
operator, który uwzględnia "if"
warunek i "wtedy"
albo „dołącza” do wyjścia $filter
na bieżącej wartości $$
aby wykluczyć bieżący indeks _id
wpis, z oczywiście kolejną "tablicą" reprezentującą pojedynczy obiekt.
Dla tego "obiektu" ponownie używamy $indexOfArray
aby faktycznie uzyskać dopasowany indeks, ponieważ wiemy, że element "tam jest" i użyć go do wyodrębnienia bieżącego "odwiedzonego"
wartość z tego wpisu za pośrednictwem $arrayElemAt
i $add
do niego w celu zwiększenia.
W "innym"
przypadku po prostu dodajemy "tablicę" jako "obiekt", który ma po prostu domyślny "odwiedzony"
wartość 1
. Korzystanie z obu tych przypadków skutecznie akumuluje unikalne wartości w tablicy do wyprowadzenia.
W drugiej wersji po prostu $unwind
tablicę i użyj kolejnych $group
etapy, aby najpierw "policzyć" unikalne wpisy wewnętrzne, a następnie "zrekonstruować tablicę" w podobnej formie.
Korzystanie z $unwind
wygląda o wiele prościej, ale ponieważ tak naprawdę robi kopię dokumentu dla każdego wpisu tablicy, to w rzeczywistości powoduje znaczne obciążenie przetwarzania. We współczesnych wersjach istnieją generalnie operatory tablicowe, co oznacza, że nie musisz ich używać, chyba że chcesz „kumulować w dokumentach”. Więc jeśli rzeczywiście potrzebujesz $group
na wartości klucza z „wewnątrz” tablicy, to właśnie tam naprawdę musisz go użyć.
Jeśli chodzi o "zmienne"
wtedy możemy po prostu użyć $filter
ponownie tutaj, aby uzyskać pasujący „Budżet”
wejście. Robimy to jako dane wejściowe do $map
operator, który umożliwia „przekształcenie” zawartości tablicy. Chcemy tego głównie, abyś mógł wziąć zawartość "wartości"
(kiedy wszystko zrobisz numerycznie) i użyj $avg
operator, który jest dostarczany, że "notacja ścieżki pola" tworzy bezpośrednio wartości tablicy, ponieważ w rzeczywistości może zwrócić wynik z takiego wejścia.
To generalnie sprawia, że przeglądanie prawie WSZYSTKICH głównych „operatorów tablicowych” dla potoku agregacji (z wyjątkiem operatorów „ustawionych”) w ramach jednego etapu potoku.
Nie zapominaj też, że prawie zawsze chcesz $match
ze zwykłymi operatorami zapytań
jako „pierwszy etap” dowolnego potoku agregacji, aby po prostu wybrać potrzebne dokumenty. Najlepiej przy użyciu indeksu.
Alternatywne
Alternatywni pracują nad dokumentami w kodzie klienta. Generalnie nie jest to zalecane, ponieważ wszystkie powyższe metody pokazują, że faktycznie "redukują" zawartość zwracaną z serwera, jak to zwykle ma miejsce w przypadku "agregacji serwera".
"Może" być możliwe ze względu na naturę "opartą na dokumencie", że większe zestawy wyników mogą zająć znacznie więcej czasu przy użyciu $unwind
i przetwarzanie klienta może być opcją, ale uważam to za znacznie bardziej prawdopodobne
Poniżej znajduje się lista, która pokazuje zastosowanie transformacji do strumienia kursora, gdy wyniki są zwracane, robiąc to samo. Istnieją trzy zademonstrowane wersje transformacji, pokazujące "dokładnie" tę samą logikę, co powyżej, implementacja z lodash
metody akumulacji i "naturalna" akumulacja na Mapie
realizacja:
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()
}
})()