Como uma observação rápida, você precisa alterar seu
"valor"
campo dentro dos "valores"
para ser numérico, já que atualmente é uma string. Mas vamos à resposta:Se você tiver acesso a
$reduce
do MongoDB 3.4, então você pode fazer algo assim: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 você tiver o MongoDB 3.6, poderá limpá-lo um pouco com
$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" } }
]
}
}
}
}}
])
Mas é mais ou menos a mesma coisa, exceto que mantemos os
additionalData
Voltando um pouco antes disso, você sempre pode
$unwind
as "cidades"
acumular: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" }
}
}
}
}}
])
Todos retornam (quase) a mesma coisa:
{
"_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
}
]
}
Os dois primeiros formulários são, obviamente, a coisa mais ideal a fazer, pois estão simplesmente trabalhando "dentro" do mesmo documento o tempo todo.
Operadores como
$reduce
permitir expressões de "acumulação" em matrizes, para que possamos usá-lo aqui para manter uma matriz "reduzida" que testamos para o "_id"
exclusivo valor usando $indexOfArray
para ver se já existe um item acumulado que corresponda. Um resultado de -1
significa que não está lá. Para construir um "array reduzido" pegamos o
"initialValue"
de []
como uma matriz vazia e adicione a ela via $concatArrays
. Todo esse processo é decidido por meio do "ternário" $cond
operador que considera o "if"
condição e "então"
ou "junta" a saída do $filter
no $$value
atual para excluir o índice atual _id
entrada, com claro outro "array" representando o objeto singular. Para esse "objeto", usamos novamente o
$indexOfArray
para realmente obter o índice correspondente, pois sabemos que o item "está lá", e usá-lo para extrair o "visited"
atual valor dessa entrada via $arrayElemAt
e $add
a ele para incrementar. No
"senão"
caso nós simplesmente adicionamos um "array" como um "objeto" que tem apenas um padrão "visited"
valor de 1
. O uso desses dois casos efetivamente acumula valores exclusivos na matriz para saída. Na última versão, nós apenas
$unwind
a matriz e use sucessivamente $group
estágios para primeiro "contar" com as entradas internas exclusivas e, em seguida, "reconstruir a matriz" na forma semelhante. Usando
$unwind
parece muito mais simples, mas como o que ele realmente faz é tirar uma cópia do documento para cada entrada da matriz, isso realmente adiciona uma sobrecarga considerável ao processamento. Nas versões modernas, geralmente existem operadores de matriz, o que significa que você não precisa usar isso, a menos que sua intenção seja "acumular entre documentos". Então, se você realmente precisa $group
em um valor de uma chave de "dentro" de um array, então é aí que você realmente precisa usá-lo. Quanto às
"variáveis"
então podemos simplesmente usar o $filter
novamente aqui para obter o "Orçamento"
correspondente entrada. Fazemos isso como entrada para o $map
operador que permite "remodelar" o conteúdo do array. Queremos principalmente que você possa pegar o conteúdo dos "values"
( depois de tornar tudo numérico ) e use o $avg
operador, que é fornecido com a forma de "notação de caminho de campo" diretamente para os valores da matriz, porque pode, de fato, retornar um resultado de tal entrada. Isso geralmente faz o tour de praticamente TODOS os principais "operadores de matriz" para o pipeline de agregação (excluindo os operadores "conjunto"), todos dentro de um único estágio de pipeline.
Também não se esqueça de que você sempre quer
$match
com operadores de consulta
regulares como o "primeiro estágio" de qualquer pipeline de agregação para selecionar apenas os documentos necessários. O ideal é usar um índice. Suplentes
Os substitutos estão trabalhando nos documentos no código do cliente. Geralmente, não seria recomendado, pois todos os métodos acima mostram que realmente "reduzem" o conteúdo retornado do servidor, como geralmente é o ponto de "agregações de servidor".
"Pode" ser possível devido à natureza "baseada em documentos" que conjuntos de resultados maiores podem levar muito mais tempo usando
$unwind
e o processamento do cliente pode ser uma opção, mas eu consideraria muito mais provável Abaixo está uma lista que demonstra a aplicação de uma transformação ao fluxo do cursor à medida que os resultados são retornados fazendo a mesma coisa. Existem três versões demonstradas da transformação, mostrando "exatamente" a mesma lógica acima, uma implementação com
lodash
métodos de acumulação, e uma acumulação "natural" no Mapa
implementação: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()
}
})()