MongoDB eindeutigen Wert aggregation über Karte reduzieren

Sehe ich jede Menge Fragen auf, SO dass über aggregation in MongoDB, aber ich habe nicht gefunden, eine komplette Lösung, um mir noch.

Hier ist ein Beispiel meiner Daten:

{
    "fruits" : {
        "apple" : "red",
        "orange" : "orange",
        "plum" : "purple"
    }
}
{
    "fruits" : {
        "apple" : "green",
        "plum" : "purple"
    }
}
{
    "fruits" : {
        "apple" : "red",
        "orange" : "yellow",
        "plum" : "purple"
    }
}

Nun, mein Ziel ist die Bestimmung der Popularität von jeder Farbe, für jede Frucht, so etwas wie diese wäre die Ausgabe Sammlung:

{
    "_id" : "apple"
    "values" : {
        "red" : 2,
        "green" : 1
    }
}
{
    "_id" : "orange"
    "values" : {
        "orange" : 1,
        "yellow" : 1
    }
}
{
    "_id" : "plum"
    "values" : {
        "purple" : 3
    }
}

Habe ich versucht verschiedene M/R Funktionen, und am Ende werden Sie entweder nicht funktionieren, oder Sie nehmen exponentiell lang. Im Kontext von Beispiel (Frucht), ich habe über 1000 verschiedene Früchte und 100.000 Farben über ungefähr 10.000.000 insgesamt Dokumente. Meine aktuellen arbeiten M/R ist diese:

map = function() {
    if (!this.fruits) return;
    for (var fruit in this.fruits) {
        emit(fruit, {
            val_array: [
                {value: this.fruits[fruit], count: 1}
            ]
        });
    }
};

reduce = function(key, values) {
    var collection = {
        val_array: []
    };
    var found = false;
    values.forEach(function(map_obj) {
        map_obj.val_array.forEach(function(value_obj) {
            found = false;
            //if exists in collection, inc, else add
            collection.val_array.forEach(function(coll_obj) {
                if (coll_obj.value == value_obj.value) {
                    //the collection already has this object, increment it
                    coll_obj.count += value_obj.count;
                    found = true;
                    return;
                }
            });
            if (!found) {
                //the collection doesn't have this obj yet, push it
                collection.val_array.push(value_obj);
            }
        });
    });
    return collection;
};

Nun, das funktioniert, und für 100 Datensätze, es dauert nur eine Sekunde oder so, aber die Zeit steigt nicht Linear, so 100M Datensätze würde eine sehr lange Zeit. Das problem ist, dass ich einen poor-mans-sub-aggregation in der senken-Funktion mit der collection array, sodass mir der Iteration über beide collection und die Werte von meinem map-Funktion. Jetzt brauche ich nur, um herauszufinden, wie dies zu tun, effizient (auch wenn es erfordert, mehrere Ermäßigungen). Alle Vorschläge sind willkommen!


BEARBEITEN aus Mangel an einen besseren Ort, um es zu veröffentlichen, hier meine Lösung.


Zuerst habe ich eine Datei namens mr.js:

map = function() {
    if (!this.fruits) return;
    var skip_fruits = {
        'Watermelon':1,
        'Grapefruit':1,
        'Tomato':1 //yes, a tomato is a fruit
    }
    for (var fruit in this.fruits) {
        if (skip_fruits[fruit]) continue;
        var obj = {};
        obj[this.fruits[fruit]] = 1;
        emit(fruit, obj);
    }
};

reduce = function(key, values) {
    var out_values = {};
    values.forEach(function(v) {
        for(var k in v) { //iterate values
            if (!out_values[k]) {
                out_values[k] = v[k]; //init missing counter
            } else {
                out_values[k] += v[k];
            }
        }
    });
    return out_values;
};

var in_coll = "fruit_repo";
var out_coll = "fruit_agg_so";
var total_docs = db[in_coll].count();
var page_size = 100000;
var pages = Math.floor(total_docs / page_size);
print('Starting incremental MR job with '+pages+' pages');
db[out_coll].drop();
for (var i=0; i<pages; i++) {
    var skip = page_size * i;
    print("Calculating page limits for "+skip+" - "+(skip+page_size-1)+"...");
    var start_date = db[in_coll].find({},{date:1}).sort({date:1}).skip(skip).limit(1)[0].date;
    var end_date = db[in_coll].find({},{date:1}).sort({date:1}).skip(skip+page_size-1).limit(1)[0].date;
    var mr_command = {
        mapreduce: in_coll,
        map: map,
        reduce: reduce,
        out: {reduce: out_coll},
        sort: {date: 1},
        query: {
            date: {
                $gte: start_date,
                $lt: end_date
            }
        },
        limit: (page_size - 1)
    };
    print("Running mapreduce for "+skip+" - "+(skip+page_size-1));
    db[in_coll].runCommand(mr_command);
}

Datei iteriert über meine gesamte Sammlung, schrittweise Karte/reduzieren 100k docs (sortiert nach date die einen index aufweisen MÜSSEN!) in einer Zeit, und verringern Sie Sie in einer einzigen Ausgabe Sammlung. Man benutzt es wie folgt: mongo db_name mr.js.

Dann, nach ein paar Stunden, ich habe eine Sammlung mit allen Informationen. Um herauszufinden, welche Früchte haben die meisten Farben, ich benutze diese von der mongo shell drucken Sie die top-25:

//Show number of number of possible values per key
var keys = [];
for (var c = db.fruit_agg_so.find(); c.hasNext();) {
    var obj = c.next();
    if (!obj.value) break;
    var len=0;for(var l in obj.value){len++;}
    keys.push({key: obj['_id'], value: len});
}
keys.sort(function(a, b){
    if (a.value == b.value) return 0;
    return (a.value > b.value)? -1: 1;
});
for (var i=0; i<20; i++) {
    print(keys[i].key+':'+keys[i].value);
}

Das wirklich Coole an diesem Ansatz ist, dass, da es inkrementell, an dem ich arbeiten kann mit dem output-Daten während der mapreduce ausgeführt wird.

InformationsquelleAutor SteveK | 2012-05-06
Schreibe einen Kommentar