MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Aggregazione accumula oggetti interni

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

})()