MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Agrégation Accumuler des objets internes

En guise de remarque rapide, vous devez modifier votre "value" champ à l'intérieur du "values" être numérique, puisqu'il s'agit actuellement d'une chaîne. Mais passons à la réponse :

Si vous avez accès à $reduce à partir de MongoDB 3.4, vous pouvez alors faire quelque chose comme ceci :

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" }
         }
       }
     }
  }}
])

Si vous avez MongoDB 3.6, vous pouvez nettoyer un peu cela avec $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" } }
           ]
         }
       }
     }
  }}
])

Mais c'est plus ou moins la même chose sauf qu'on garde le additionalData

En remontant un peu avant cela, vous pouvez toujours $unwind les "cities" accumuler :

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" }
         }
       }
     }
  }}
])

Tous renvoient (presque) la même chose :

{
        "_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
                }
        ]
}

Les deux premières formes sont bien sûr la chose la plus optimale à faire puisqu'elles fonctionnent simplement "dans" le même document à tout moment.

Des opérateurs comme $reduce autoriser les expressions "d'accumulation" sur les tableaux, nous pouvons donc l'utiliser ici pour conserver un tableau "réduit" que nous testons pour l'unique "_id" valeur en utilisant $indexOfArray afin de voir s'il existe déjà un élément cumulé qui correspond. Un résultat de -1 signifie qu'il n'est pas là.

Pour construire un "tableau réduit", nous prenons la "initialValue" de [] comme un tableau vide, puis ajoutez-le via $concatArrays . Tout ce processus est décidé via le "ternaire" $cond opérateur qui considère le "if" condition et "then" soit "rejoint" la sortie du $filter sur la $$value actuelle pour exclure l'index courant _id entrée, avec bien sûr un autre "tableau" représentant l'objet singulier.

Pour cet "objet", nous utilisons à nouveau le $indexOfArray pour obtenir l'index correspondant puisque nous savons que l'élément "est là", et l'utiliser pour extraire le "visited" actuel valeur de cette entrée via $arrayElemAt et $add pour l'incrémenter.

Dans le "else" cas, nous ajoutons simplement un "tableau" en tant qu'"objet" qui a juste un "visited" par défaut valeur de 1 . L'utilisation de ces deux cas accumule efficacement des valeurs uniques dans le tableau à produire.

Dans cette dernière version, nous nous contentons de $unwind le tableau et utilisez successivement $group afin de "compter" d'abord sur les entrées internes uniques, puis de "reconstruire le tableau" sous une forme similaire.

Utilisation de $unwind semble beaucoup plus simple, mais puisque ce qu'il fait réellement est de prendre une copie du document pour chaque entrée de tableau, cela ajoute en fait une surcharge considérable au traitement. Dans les versions modernes, il existe généralement des opérateurs de tableau, ce qui signifie que vous n'avez pas besoin de l'utiliser à moins que votre intention ne soit "d'accumuler sur plusieurs documents". Donc, si vous avez réellement besoin de $group sur une valeur d'une clé de "l'intérieur" d'un tableau, alors c'est là que vous devez réellement l'utiliser.

Quant aux "variables" alors nous pouvons simplement utiliser le $filter à nouveau ici pour obtenir le "Budget" correspondant entrée. Nous faisons cela comme entrée du $map opérateur qui permet de "remodeler" le contenu du tableau. Nous voulons principalement que vous puissiez prendre le contenu des "values" (une fois que tout est numérique) et utilisez le $avg , qui est fourni que la "notation de chemin de champ" forme directement aux valeurs du tableau car il peut en fait renvoyer un résultat à partir d'une telle entrée.

Cela fait généralement le tour de pratiquement TOUS les principaux "opérateurs de tableau" pour le pipeline d'agrégation (à l'exclusion des opérateurs "set"), le tout en une seule étape de pipeline.

N'oubliez pas non plus que vous voulez presque toujours $match avec des Opérateurs de requête comme la "toute première étape" de tout pipeline d'agrégation afin de sélectionner uniquement les documents dont vous avez besoin. Idéalement en utilisant un index.

Alternatifs

Les suppléants travaillent sur les documents dans le code client. Cela ne serait généralement pas recommandé car toutes les méthodes ci-dessus montrent qu'elles "réduisent" en fait le contenu renvoyé par le serveur, comme c'est généralement le cas des "agrégations de serveurs".

Il "peut" être possible en raison de la nature "basée sur le document" que des ensembles de résultats plus volumineux puissent prendre beaucoup plus de temps en utilisant $unwind et le traitement des clients pourrait être une option, mais je considérerais cela beaucoup plus probable

Vous trouverez ci-dessous une liste qui illustre l'application d'une transformation au flux de curseur lorsque les résultats sont renvoyés en faisant la même chose. Il existe trois versions démontrées de la transformation, montrant "exactement" la même logique que ci-dessus, une implémentation avec lodash méthodes d'accumulation, et une accumulation "naturelle" sur la Map implémentation :

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

})()