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

MongoDB Calculer les valeurs de deux tableaux, trier et limiter

Le traitement actuel est mapReduce

Si vous avez besoin d'exécuter ceci sur le serveur et de trier les meilleurs résultats et de ne conserver que les 100 premiers, vous pouvez utiliser mapReduce pour cela comme suit :

db.test.mapReduce(
    function() {
        var input = [0.1,0.3,0.4];
        var value = Array.sum(this.vals.map(function(el,idx) {
            return Math.abs( el - input[idx] )
        }));

        emit(null,{ "output": [{ "_id": this._id, "value": value }]});
    },
    function(key,values) {
        var output = [];

        values.forEach(function(value) {
            value.output.forEach(function(item) {
                output.push(item);
            });
        });

        output.sort(function(a,b) {
            return a.value < b.value;
        });

        return { "output": output.slice(0,100) };
    },
    { "out": { "inline": 1 } }
)

Ainsi, la fonction mapper effectue le calcul et produit tout sous la même clé afin que tous les résultats soient envoyés au réducteur. La sortie finale va être contenue dans un tableau dans un seul document de sortie, il est donc à la fois important que tous les résultats soient émis avec la même valeur de clé et que la sortie de chaque émission soit elle-même un tableau afin que mapReduce puisse fonctionner correctement.

Le tri et la réduction sont effectués dans le réducteur lui-même, à mesure que chaque document émis est inspecté, les éléments sont placés dans un seul tableau temporaire, triés et les meilleurs résultats sont renvoyés.

C'est important, et c'est justement la raison pour laquelle l'émetteur produit cela sous forme de tableau, même s'il s'agit d'un seul élément au début. MapReduce fonctionne en traitant les résultats en "morceaux", donc même si tous les documents émis ont la même clé, ils ne sont pas tous traités en même temps. Au lieu de cela, le réducteur remet ses résultats dans la file d'attente des résultats émis à réduire jusqu'à ce qu'il ne reste plus qu'un seul document pour cette clé particulière.

Je limite la sortie "tranche" ici à 10 pour la brièveté de la liste, et j'inclus les statistiques pour faire un point, car les 100 cycles de réduction appelés sur cet échantillon de 10000 peuvent être vus :

{
    "results" : [
        {
            "_id" : null,
            "value" : {
                "output" : [
                    {
                        "_id" : ObjectId("56558d93138303848b496cd4"),
                        "value" : 2.2
                    },
                    {
                        "_id" : ObjectId("56558d96138303848b49906e"),
                        "value" : 2.2
                    },
                    {
                        "_id" : ObjectId("56558d93138303848b496d9a"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d93138303848b496ef2"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497861"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497b58"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497ba5"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d94138303848b497c43"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d95138303848b49842b"),
                        "value" : 2.1
                    },
                    {
                        "_id" : ObjectId("56558d96138303848b498db4"),
                        "value" : 2.1
                    }
                ]
            }
        }
    ],
    "timeMillis" : 1758,
    "counts" : {
            "input" : 10000,
            "emit" : 10000,
            "reduce" : 100,
            "output" : 1
    },
    "ok" : 1
}

Il s'agit donc d'une sortie de document unique, dans le format mapReduce spécifique, où la "valeur" contient un élément qui est un tableau du résultat trié et limité.

Le traitement futur est global

Au moment de la rédaction, la dernière version stable actuelle de MongoDB est la 3.0, et il lui manque la fonctionnalité pour rendre votre opération possible. Mais la prochaine version 3.2 introduit de nouveaux opérateurs qui rendent cela possible :

db.test.aggregate([
    { "$unwind": { "path": "$vals", "includeArrayIndex": "index" }},
    { "$group": {
        "_id": "$_id",
        "result": {
            "$sum": {
                "$abs": {
                    "$subtract": [ 
                        "$vals", 
                        { "$arrayElemAt": [ { "$literal": [0.1,0.3,0.4] }, "$index" ] } 
                    ]
                }
            }
        }
    }},
    { "$sort": { "result": -1 } },
    { "$limit": 100 }
])

En limitant également aux 10 mêmes résultats par souci de brièveté, vous obtenez une sortie comme celle-ci :

{ "_id" : ObjectId("56558d96138303848b49906e"), "result" : 2.2 }
{ "_id" : ObjectId("56558d93138303848b496cd4"), "result" : 2.2 }
{ "_id" : ObjectId("56558d96138303848b498e31"), "result" : 2.1 }
{ "_id" : ObjectId("56558d94138303848b497c43"), "result" : 2.1 }
{ "_id" : ObjectId("56558d94138303848b497861"), "result" : 2.1 }
{ "_id" : ObjectId("56558d96138303848b499037"), "result" : 2.1 }
{ "_id" : ObjectId("56558d96138303848b498db4"), "result" : 2.1 }
{ "_id" : ObjectId("56558d93138303848b496ef2"), "result" : 2.1 }
{ "_id" : ObjectId("56558d93138303848b496d9a"), "result" : 2.1 }
{ "_id" : ObjectId("56558d96138303848b499182"), "result" : 2.1 }

Ceci est rendu possible en grande partie grâce à $unwind étant modifié pour projeter un champ dans les résultats qui contient l'index du tableau, et également en raison de $arrayElemAt qui est un nouvel opérateur qui peut extraire un élément de tableau en tant que valeur singulière à partir d'un index fourni.

Cela permet la "recherche" des valeurs par position d'index à partir de votre tableau d'entrée afin d'appliquer les calculs à chaque élément. Le tableau d'entrée est facilité par le $literal opérateur donc $arrayElemAt ne se plaint pas et le reconnaît comme un tableau (semble être un petit bogue à l'heure actuelle, car les autres fonctions de tableau n'ont pas le problème avec l'entrée directe) et obtient la valeur d'index correspondante appropriée en utilisant le champ "index" produit par $unwind à titre de comparaison.

Le calcul est effectué par $subtract et bien sûr un autre nouvel opérateur dans $abs pour répondre à votre fonctionnalité. De plus, comme il était nécessaire de dérouler le tableau en premier lieu, tout cela se fait dans un $group étape accumulant tous les membres du tableau par document et appliquant l'ajout d'entrées via le $sum accumulateur.

Enfin, tous les documents de résultat sont traités avec $sort puis le $limit est appliqué pour renvoyer uniquement les meilleurs résultats.

Résumé

Même avec la nouvelle fonctionnalité sur le point d'être disponible pour le cadre d'agrégation pour MongoDB, il est discutable de savoir quelle approche est réellement la plus efficace pour les résultats. Cela est en grande partie dû au fait qu'il est toujours nécessaire de $unwind le contenu du tableau, qui produit effectivement une copie de chaque document par membre du tableau dans le pipeline à traiter, et qui entraîne généralement une surcharge.

Ainsi, bien que mapReduce soit le seul moyen actuel de le faire jusqu'à une nouvelle version, il peut en fait surpasser l'instruction d'agrégation en fonction de la quantité de données à traiter, et malgré le fait que le cadre d'agrégation fonctionne sur des opérateurs codés natifs plutôt que JavaScript traduit opérations.

Comme pour toutes choses, des tests sont toujours recommandés pour voir quel cas convient le mieux à vos besoins et lequel offre les meilleures performances pour le traitement attendu.

Échantillon

Bien sûr, le résultat attendu pour l'exemple de document fourni dans la question est 0.9 par les mathématiques appliquées. Mais juste pour mes besoins de test, voici une courte liste utilisée pour générer des exemples de données que je voulais au moins vérifier que le code mapReduce fonctionnait comme il se doit :

var bulk = db.test.initializeUnorderedBulkOp();

var x = 10000;

while ( x-- ) {
    var vals = [0,0,0];

    vals = vals.map(function(val) {
        return Math.round((Math.random()*10),1)/10;
    });

    bulk.insert({ "vals": vals });

    if ( x % 1000 == 0) {
        bulk.execute();
        bulk = db.test.initializeUnorderedBulkOp();
    }
}

Les tableaux sont des valeurs décimales uniques totalement aléatoires, il n'y a donc pas beaucoup de distribution dans les résultats répertoriés que j'ai donnés comme exemple de sortie.