MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Aggregation Sammeln Sie innere Objekte

Als kurze Anmerkung:Sie müssen Ihren "Wert" ändern Feld innerhalb des "values" numerisch sein, da es derzeit eine Zeichenfolge ist. Aber weiter zur Antwort:

Wenn Sie Zugriff auf $reduce haben von MongoDB 3.4, dann können Sie tatsächlich so etwas tun:

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

Wenn Sie MongoDB 3.6 haben, können Sie das mit $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" } }
           ]
         }
       }
     }
  }}
])

Aber es ist mehr oder weniger dasselbe, außer dass wir die additionalData behalten

Gehen wir davor ein wenig zurück, dann können Sie sich jederzeit $entspannen die "Städte" zu akkumulieren:

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

Alle geben (fast) dasselbe zurück:

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

Die ersten beiden Formen sind natürlich am besten geeignet, da sie einfach immer "innerhalb" desselben Dokuments arbeiten.

Operatoren wie $reduce "Akkumulation"-Ausdrücke für Arrays zulassen, sodass wir es hier verwenden können, um ein "reduziertes" Array zu behalten, das wir auf die eindeutige "_id" testen Wert mit $indexOfArray um zu sehen, ob es bereits einen passenden gesammelten Artikel gibt. Ein Ergebnis von -1 bedeutet, dass es nicht da ist.

Um ein "reduziertes Array" zu konstruieren, nehmen wir den "initialValue" von [] als leeres Array und fügen Sie es dann über $concatArrays . All dieser Prozess wird über den „ternären“ $cond entschieden Operator, der das "if" berücksichtigt Bedingung und "dann" entweder "verbindet" die Ausgabe von $filter auf dem aktuellen $$value um den aktuellen Index _id auszuschließen Eintrag, mit natürlich einem weiteren "Array", das das einzelne Objekt darstellt.

Für dieses "Objekt" verwenden wir wieder den $indexOfArray um tatsächlich den übereinstimmenden Index zu erhalten, da wir wissen, dass das Element "vorhanden" ist, und diesen verwenden, um den aktuellen "visited" zu extrahieren Wert aus diesem Eintrag über $arrayElemAt und $add hinzu, um zu inkrementieren.

Im "else" In diesem Fall fügen wir einfach ein "Array" als "Objekt" hinzu, das nur einen Standard "visited" hat Wert von 1 . Die Verwendung dieser beiden Fälle akkumuliert effektiv eindeutige Werte innerhalb des auszugebenden Arrays.

In letzterer Version verwenden wir nur $unwind das Array und verwenden Sie nacheinander $group Phasen, um zuerst mit den eindeutigen inneren Einträgen zu "zählen" und dann das Array in die ähnliche Form "wieder aufzubauen".

Verwenden von $unwind sieht viel einfacher aus, aber da es tatsächlich eine Kopie des Dokuments für jeden Array-Eintrag erstellt, fügt dies der Verarbeitung erheblichen Overhead hinzu. In modernen Versionen gibt es im Allgemeinen Array-Operatoren, was bedeutet, dass Sie diese nicht verwenden müssen, es sei denn, Sie beabsichtigen, "dokumentenübergreifend zu akkumulieren". Wenn Sie also tatsächlich $group benötigen auf einen Wert eines Schlüssels "innerhalb" eines Arrays, dann müssen Sie ihn tatsächlich dort verwenden.

Wie für die "Variablen" dann können wir einfach den $filter verwenden nochmal hier um das passende "Budget" zu erhalten Eintrag. Wir tun dies als Eingabe für $map -Operator, der eine "Umformung" des Array-Inhalts ermöglicht. Das wollen wir hauptsächlich, damit Sie den Inhalt der "Werte" übernehmen können ( sobald Sie alles numerisch gemacht haben ) und verwenden Sie den $avg -Operator, der diese "Feldpfad-Notation" direkt an die Array-Werte liefert, da er tatsächlich ein Ergebnis aus einer solchen Eingabe zurückgeben kann.

Das macht im Allgemeinen die Tour durch so ziemlich ALLE wichtigen "Array-Operatoren" für die Aggregations-Pipeline (mit Ausnahme der "Set"-Operatoren) alle innerhalb einer einzigen Pipeline-Phase.

Vergessen Sie auch nie, dass Sie fast immer möchten $match mit regulären Abfrageoperatoren als "allererste Stufe" jeder Aggregationspipeline, um nur die benötigten Dokumente auszuwählen. Idealerweise mit einem Index.

Alternativen

Stellvertreter arbeiten die Dokumente im Client-Code durch. Dies wird im Allgemeinen nicht empfohlen, da alle oben genannten Methoden zeigen, dass sie den vom Server zurückgegebenen Inhalt tatsächlich „reduzieren“, wie es im Allgemeinen der Punkt von „Server-Aggregationen“ ist.

Aufgrund der "dokumentbasierten" Natur "kann" es möglich sein, dass größere Ergebnismengen mit $unwind erheblich mehr Zeit in Anspruch nehmen und Client-Verarbeitung wäre eine Option, aber ich würde es für viel wahrscheinlicher halten

Nachfolgend finden Sie eine Auflistung, die die Anwendung einer Transformation auf den Cursor-Stream zeigt, da Ergebnisse zurückgegeben werden, die dasselbe tun. Es gibt drei demonstrierte Versionen der Transformation, die "exakt" dieselbe Logik wie oben zeigen, eine Implementierung mit lodash Methoden zur Akkumulation und eine "natürliche" Akkumulation auf der Map Implementierung:

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

})()