MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

MongoDB para ajudar com recomendações


Você precisa fazer algumas coisas aqui para o resultado final, mas os primeiros estágios são relativamente simples. Pegue o objeto de usuário que você fornece:
var user = {
    user_id : 1,
    Friends : [3,5,6],
    Artists : [
        {artist_id: 10 , weight : 345},
        {artist_id: 17 , weight : 378}
    ]
};

Agora, supondo que você já tenha esses dados recuperados, isso se resume a encontrar as mesmas estruturas para cada "amigo" e filtrar o conteúdo da matriz de "Artistas" em uma única lista distinta. Presumivelmente, cada "peso" também será considerado no total aqui.

Esta é uma operação de agregação simples que primeiro filtrará os artistas que já estão na lista para um determinado usuário:
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
    }
);

O "pré-filtro" é a única parte realmente complicada aqui. Você pode apenas $unwind a matriz e $match novamente para filtrar as entradas que você não deseja. Mesmo que queiramos $unwind os resultados posteriormente para combiná-los, é mais eficiente removê-los da matriz "primeiro", para que haja menos a expandir.

Então aqui está o $map O operador permite a inspeção de cada elemento do array "Artists" do usuário e também para comparação com a lista de artistas "user" filtrada para retornar apenas os detalhes desejados. O $setDifference é usado para realmente "filtrar" quaisquer resultados que não foram retornados como o conteúdo do array, mas sim retornados como false .

Depois disso, há apenas o $unwind para desnormalizar o conteúdo na matriz e o $group para reunir um total por artista. Por diversão, estamos usando $sort para mostrar que a lista é retornada na ordem desejada, mas isso não será necessário em um estágio posterior.

Isso é pelo menos parte do caminho aqui, pois a lista resultante deve ser apenas outros artistas que ainda não estão na lista do próprio usuário e classificada pelo "peso" somado de qualquer artista que possa aparecer em vários amigos.

A próxima parte vai precisar de dados da coleção de "artistas" para levar em conta o número de ouvintes. Enquanto o mangusto tem um .populate() método, você realmente não quer isso aqui, pois está procurando as contagens de "usuários distintos". Isso implica em outra implementação de agregação para obter essas contagens distintas para cada artista.

Seguindo a lista de resultados da operação de agregação anterior, você usaria o $_id valores assim:
// 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
    }
);

Aqui o truque é feito em conjunto com $map para fazer uma transformação semelhante de valores que é alimentado para $setUnion para torná-los uma lista única. Em seguida, o $size é aplicado para descobrir o tamanho dessa lista. A matemática adicional é dar a esse número algum significado quando aplicado contra os pesos já registrados dos resultados anteriores.

Claro que você precisa reunir tudo isso de alguma forma, pois agora existem apenas dois conjuntos distintos de resultados. O processo básico é uma "Tabela de Hash", onde os valores de id exclusivos do "artista" são usados ​​como chave e os valores de "peso" são combinados.

Você pode fazer isso de várias maneiras, mas como há um desejo de "classificar" os resultados combinados, minha preferência seria algo "MongoDBish", pois segue os métodos básicos aos quais você já deve estar acostumado.

Uma maneira prática de implementar isso é usar nedb , que fornece um armazenamento "na memória" que usa muito do mesmo tipo de métodos usados ​​para ler e gravar em coleções do MongoDB.

Isso também se adapta bem se você precisar usar uma coleção real para grandes resultados, pois todos os princípios permanecem os mesmos.

  1. A primeira operação de agregação insere novos dados na loja

  2. A segunda agregação "atualiza" esses dados e incrementa o campo "peso"

Como uma lista completa de funções e com alguma outra ajuda do async biblioteca ficaria assim:
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);
            }
        );

    });

}

Então, depois de corresponder ao objeto de usuário de origem inicial, os valores são passados ​​para a primeira função de agregação, que está sendo executada em série e usando async.waterfall para passar o resultado.

Antes que isso aconteça, os resultados da agregação são adicionados ao DataStore com .insert() normal declarações, tomando o cuidado de renomear o _id campos como nedb não gosta de nada além de seu próprio _id gerado por si mesmo valores. Cada resultado é inserido com artist_id e weight propriedades do resultado da agregação.

Essa lista é então passada para a segunda operação de agregação que retornará cada "artista" especificado com um "peso" calculado com base no tamanho do usuário distinto. Existem os "atualizados" com o mesmo .update() declaração no DataStore para cada artista e incrementando o campo "peso".

Tudo indo bem, a operação final é .find() esses resultados e .sort() eles pelo "peso" combinado e simplesmente retornar o resultado para o retorno de chamada passado para a função.

Então você usaria assim:
GetUserRecommendations(1,function(err,results) {
   // results is the sorted list
});

E retornará todos os artistas que não estão atualmente na lista desse usuário, mas em suas listas de amigos e ordenados pelos pesos combinados da contagem de ouvintes do amigo mais a pontuação do número de usuários distintos desse artista.

É assim que você lida com dados de duas coleções diferentes que você precisa combinar em um único resultado com vários detalhes agregados. São várias consultas e um espaço de trabalho, mas também faz parte da filosofia do MongoDB que essas operações sejam melhor executadas dessa maneira do que jogá-las no banco de dados para "juntar" resultados.