sql >> Base de Datos >  >> NoSQL >> MongoDB

MongoDB para ayudar con las recomendaciones

Debe hacer un par de cosas aquí para obtener el resultado final, pero las primeras etapas son relativamente simples. Toma el objeto de usuario que proporcionas:

var user = {
    user_id : 1,
    Friends : [3,5,6],
    Artists : [
        {artist_id: 10 , weight : 345},
        {artist_id: 17 , weight : 378}
    ]
};

Ahora, suponiendo que ya haya recuperado esos datos, esto se reduce a encontrar las mismas estructuras para cada "amigo" y filtrar el contenido de la matriz de "Artistas" en una sola lista distinta. Presumiblemente, cada "peso" también se considerará en total aquí.

Esta es una operación de agregación simple que primero filtrará los artistas que ya están en la lista para el usuario dado:

var artists = user.Artists.map(function(artist) { return artist.artist_id });

User.aggregate(
    [ 
        // Find possible friends without all the same artists
        { "$match": {
            "user_id": { "$in": user.Friends },
            "Artists.artist_id": { "$nin": artists }
        }},
        // Pre-filter the artists already in the user list
        { "$project": 
            "Artists": {
                "$setDifference": [
                    { "$map": {
                        "input": "$Artists",
                        "as": "$el",
                        "in": {
                            "$cond": [
                                "$anyElementTrue": {
                                    "$map": {
                                        "input": artists,
                                        "as": "artist",
                                        "in": { "$eq": [ "$$artist", "$el.artist_id" ] }
                                    }
                                },
                                false,
                                "$$el"
                            ]
                        } 
                    }}
                    [false]
                ]
            } 
        }},
        // Unwind the reduced array
        { "$unwind": "$Artists" },
        // Group back by each artist and sum weights
        { "$group": {
            "_id": "$Artists.artist_id",
            "weight": { "$sum": "$Artists.weight" }
        }},
        // Sort the results by weight
        { "$sort": { "weight": -1 } }
    ],
    function(err,results) {
        // more to come here
    }
);

El "prefiltro" es la única parte realmente complicada aquí. Podría simplemente $unwind la matriz y $match de nuevo para filtrar las entradas que no desea. Aunque queremos $unwind los resultados más tarde para combinarlos, resulta más eficiente eliminarlos de la matriz "primero", por lo que hay menos para expandir.

Así que aquí el $map El operador permite la inspección de cada elemento de la matriz de "Artistas" del usuario y también la comparación con la lista filtrada de artistas del "usuario" para devolver los detalles deseados. El $setDifference se usa para "filtrar" cualquier resultado que no se haya devuelto como el contenido de la matriz, sino que se haya devuelto como false .

Después de eso, solo queda $unwind para desnormalizar el contenido en la matriz y el $group para reunir un total por artista. Por diversión estamos usando $sort para mostrar que la lista se devuelve en el orden deseado, pero eso no será necesario en una etapa posterior.

Eso es al menos una parte del camino aquí, ya que la lista resultante solo debe incluir otros artistas que no estén ya en la lista del usuario, y ordenados por el "peso" sumado de cualquier artista que posiblemente pueda aparecer en varios amigos.

La siguiente parte necesitará datos de la colección de "artistas" para tener en cuenta el número de oyentes. Mientras que la mangosta tiene un .populate() método, realmente no quiere esto aquí ya que está buscando los recuentos de "usuario distinto". Esto implica otra implementación de agregación para obtener esos recuentos distintos para cada artista.

Siguiendo con la lista de resultados de la operación de agregación anterior, usaría el $_id valores como este:

// First get just an array of artist id's
var artists = results.map(function(artist) {
    return artist._id;
});

Artist.aggregate(
    [
        // Match artists
        { "$match": {
            "artistID": { "$in": artists }
        }},
        // Project with weight for distinct users
        { "$project": {
            "_id": "$artistID",
            "weight": {
                "$multiply": [
                    { "$size": {
                        "$setUnion": [
                            { "$map": {
                                "input": "$user_tag",
                                "as": "tag",
                                "in": "$$tag.user_id"
                            }},
                            []
                        ]
                    }},
                    10
                ]
            }
        }}
    ],
    function(err,results) {
        // more later
    }
);

Aquí el truco se hace en conjunto con $map para hacer una transformación similar de valores que se envía a $setUnion para hacer de ellos una lista única. Entonces el $size se aplica el operador para averiguar qué tan grande es esa lista. La matemática adicional es para darle a ese número algún significado cuando se aplica contra los pesos ya registrados de los resultados anteriores.

Por supuesto, debe unir todo esto de alguna manera, ya que en este momento solo hay dos conjuntos distintos de resultados. El proceso básico es una "tabla hash", donde los valores únicos de identificación del "artista" se utilizan como clave y los valores de "peso" se combinan.

Puede hacer esto de varias maneras, pero dado que existe el deseo de "ordenar" los resultados combinados, mi preferencia sería algo "MongoDBish", ya que sigue los métodos básicos a los que ya debería estar acostumbrado.

Una forma práctica de implementar esto es usar nedb , que proporciona un almacén "en memoria" que utiliza gran parte del mismo tipo de métodos que se utilizan para leer y escribir en las colecciones de MongoDB.

Esto también escala bien si necesita usar una colección real para obtener grandes resultados, ya que todos los principios siguen siendo los mismos.

  1. La primera operación de agregación inserta nuevos datos en la tienda

  2. La segunda agregación "actualiza" esos datos e incrementa el campo "peso"

Como una lista completa de funciones, y con alguna otra ayuda de async biblioteca se vería así:

function GetUserRecommendations(userId,callback) {

    var async = require('async')
        DataStore = require('nedb');

    User.findOne({ "user_id": user_id},function(err,user) {
        if (err) callback(err);

        var artists = user.Artists.map(function(artist) {
            return artist.artist_id;
        });

        async.waterfall(
            [
                function(callback) {
                    var pipeline =  [ 
                        // Find possible friends without all the same artists
                        { "$match": {
                            "user_id": { "$in": user.Friends },
                            "Artists.artist_id": { "$nin": artists }
                        }},
                        // Pre-filter the artists already in the user list
                        { "$project": 
                            "Artists": {
                                "$setDifference": [
                                    { "$map": {
                                        "input": "$Artists",
                                        "as": "$el",
                                        "in": {
                                            "$cond": [
                                                "$anyElementTrue": {
                                                    "$map": {
                                                        "input": artists,
                                                        "as": "artist",
                                                        "in": { "$eq": [ "$$artist", "$el.artist_id" ] }
                                                    }
                                                },
                                                false,
                                                "$$el"
                                            ]
                                        } 
                                    }}
                                    [false]
                                ]
                            } 
                        }},
                        // Unwind the reduced array
                        { "$unwind": "$Artists" },
                        // Group back by each artist and sum weights
                        { "$group": {
                            "_id": "$Artists.artist_id",
                            "weight": { "$sum": "$Artists.weight" }
                        }},
                        // Sort the results by weight
                        { "$sort": { "weight": -1 } }
                    ];

                    User.aggregate(pipeline, function(err,results) {
                        if (err) callback(err);

                        async.each(
                            results,
                            function(result,callback) {
                                result.artist_id = result._id;
                                delete result._id;
                                DataStore.insert(result,callback);
                            },
                            function(err)
                                callback(err,results);
                            }
                        );

                    });
                },
                function(results,callback) {

                    var artists = results.map(function(artist) {
                        return artist.artist_id;  // note that we renamed this
                    });

                    var pipeline = [
                        // Match artists
                        { "$match": {
                            "artistID": { "$in": artists }
                        }},
                        // Project with weight for distinct users
                        { "$project": {
                            "_id": "$artistID",
                            "weight": {
                                "$multiply": [
                                    { "$size": {
                                        "$setUnion": [
                                            { "$map": {
                                                "input": "$user_tag",
                                                "as": "tag",
                                                "in": "$$tag.user_id"
                                            }},
                                            []
                                        ]
                                    }},
                                    10
                                ]
                            }
                        }}
                    ];

                    Artist.aggregate(pipeline,function(err,results) {
                        if (err) callback(err);
                        async.each(
                            results,
                            function(result,callback) {
                                result.artist_id = result._id;
                                delete result._id;
                                DataStore.update(
                                    { "artist_id": result.artist_id },
                                    { "$inc": { "weight": result.weight } },
                                    callback
                                );
                            },
                            function(err) {
                                callback(err);
                            }
                        );
                    });
                }
            ],
            function(err) {
                if (err) callback(err);     // callback with any errors
                // else fetch the combined results and sort to callback
                DataStore.find({}).sort({ "weight": -1 }).exec(callback);
            }
        );

    });

}

Entonces, después de hacer coincidir el objeto de usuario de origen inicial, los valores se pasan a la primera función agregada, que se ejecuta en serie y usa async.waterfall para pasar su resultado.

Antes de que eso suceda, los resultados de la agregación se agregan al DataStore con .insert() normal declaraciones, teniendo cuidado de cambiar el nombre del _id campos como nedb no le gusta nada más que su propio _id generado por sí mismo valores. Cada resultado se inserta con artist_id y weight propiedades del resultado de la agregación.

Luego, esa lista se pasa a la segunda operación de agregación que devolverá cada "artista" especificado con un "peso" calculado en función del tamaño de usuario distinto. Están los "actualizados" con el mismo .update() declaración en el DataStore para cada artista e incrementando el campo "peso".

Todo va bien, la operación final es .find() esos resultados y .sort() por el "peso" combinado, y simplemente devolver el resultado a la devolución de llamada pasada a la función.

Así que lo usarías así:

GetUserRecommendations(1,function(err,results) {
   // results is the sorted list
});

Y devolverá todos los artistas que no están actualmente en la lista de ese usuario pero sí en sus listas de amigos y ordenados por los pesos combinados del conteo de escucha de amigos más el puntaje del número de usuarios distintos de ese artista.

Así es como maneja los datos de dos colecciones diferentes que necesita combinar en un solo resultado con varios detalles agregados. Son consultas múltiples y un espacio de trabajo, pero también parte de la filosofía de MongoDB de que tales operaciones se realizan mejor de esta manera que arrojarlas a la base de datos para "unir" los resultados.