MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Agregação Acumular Objetos Internos


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()
  }

})()