sql >> Databasteknik >  >> NoSQL >> MongoDB

MongoDB för att hjälpa till med rekommendationer

Du måste göra ett par saker här för ditt slutresultat, men de första stegen är relativt enkla. Ta användarobjektet du tillhandahåller:

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

Om du nu antar att du redan har hämtat den datan, handlar det om att hitta samma strukturer för varje "vän" och filtrera bort arrayinnehållet för "Artister" till en enda distinkt lista. Förmodligen kommer varje "vikt" också att beaktas totalt här.

Detta är en enkel sammanställningsoperation som först kommer att filtrera bort artisterna som redan finns i listan för den givna användaren:

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

"Förfiltret" är den enda riktigt knepiga delen här. Du kan bara $unwind arrayen och $match igen för att filtrera bort de poster du inte vill ha. Även om vi vill $unwind resultaten senare för att kombinera dem fungerar det mer effektivt att ta bort dem från arrayen "först", så det finns mindre att expandera.

Så här är $map operatören tillåter inspektion av varje element i användarens "Artists"-array och även för jämförelse med den filtrerade "user"-artistlistan för att bara returnera de önskade detaljerna. $setDifference används för att faktiskt "filtrera" alla resultat som inte returnerades som arrayinnehåll, utan snarare returnerades som false .

Efter det finns bara $unwind för att avnormalisera innehållet i arrayen och $group att få ihop en summa per artist. För skojs skull använder vi $sort för att visa att listan returneras i önskad ordning, men det kommer inte att behövas i ett senare skede.

Det är åtminstone en del av vägen här eftersom den resulterande listan endast bör vara andra artister som inte redan finns i användarens egen lista, och sorterad efter den summerade "vikten" från artister som eventuellt kan visas på flera vänner.

Nästa del kommer att behöva data från "artister"-samlingen för att ta hänsyn till antalet lyssnare. Medan mongoose har en .populate() metoden, vill du verkligen inte ha det här eftersom du letar efter "distinkt användare". Detta innebär ytterligare en aggregeringsimplementering för att få dessa distinkta siffror för varje artist.

Efter resultatlistan för den tidigare aggregeringsoperationen skulle du använda $_id värden som detta:

// 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
    }
);

Här görs tricket aggregerat med $map att göra en liknande omvandling av värden som matas till $setUnion för att göra dem till en unik lista. Sedan $size operatören används för att ta reda på hur stor den listan är. Den ytterligare matematiken är att ge den siffran en viss betydelse när den används mot de redan registrerade vikterna från de tidigare resultaten.

Naturligtvis måste du få ihop allt detta på något sätt, eftersom det just nu bara finns två distinkta uppsättningar resultat. Grundprocessen är en "Hash Table", där de unika "artist"-id-värdena används som en nyckel och "weight"-värdena kombineras.

Du kan göra detta på ett antal sätt, men eftersom det finns en önskan att "sortera" de kombinerade resultaten så skulle min preferens vara något "MongoDBish" eftersom det följer de grundläggande metoderna du redan borde vara van vid.

Ett praktiskt sätt att implementera detta är att använda nedb , som tillhandahåller en "i minne"-lagring som använder mycket av samma typ av metoder som används för att läsa och skriva till MongoDB-samlingar.

Detta kan även skalas bra om du behövde använda en faktisk samling för stora resultat, eftersom alla principer förblir desamma.

  1. Första aggregeringsoperationen infogar ny data i butiken

  2. Den andra aggregeringen "uppdaterar" denna data och ökar fältet "vikt"

Som en komplett funktionslista, och med lite annan hjälp av async biblioteket skulle det se ut så här:

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

    });

}

Så efter att ha matchat det ursprungliga källanvändarobjektet skickas värdena till den första aggregatfunktionen, som körs i serie och använder async.waterfall för att klara resultatet.

Innan det händer läggs aggregeringsresultaten till i DataStore med vanlig .insert() satser, var noga med att byta namn på _id fält som nedb gillar inte något annat än det egna självgenererade _id värden. Varje resultat infogas med artist_id och weight egenskaper från aggregeringsresultatet.

Den listan skickas sedan till den andra aggregeringsoperationen som kommer att returnera varje specificerad "artist" med en beräknad "vikt" baserat på den distinkta användarstorleken. Det finns "uppdaterade" med samma .update() uttalande i DataStore för varje artist och öka fältet "vikt".

Allt går bra, den sista operationen är att .find() dessa resultat och .sort() dem med den kombinerade "vikten", och returnera helt enkelt resultatet till den godkända återuppringningen till funktionen.

Så du skulle använda det så här:

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

Och det kommer att returnera alla artister som inte för närvarande finns i den användarens lista utan i deras vänlistor och sorterade efter den kombinerade vikten av antalet vänner som lyssnar plus poängen från antalet distinkta användare av den artisten.

Så här hanterar du data från två olika samlingar som du behöver kombinera till ett enda resultat med olika aggregerade detaljer. Det är flera frågor och ett arbetsutrymme, men också en del av MongoDB-filosofin att sådana operationer utförs bättre på detta sätt än att kasta dem i databasen för att "ansluta" resultat.




  1. Rekursionsfråga?

  2. mongodb Kan inte tillämpa modifieraren $pull/$pullAll på icke-array, hur man tar bort arrayelement

  3. Data null efter att ha sparat entitet med Moongose ​​och GraphQL

  4. Diff() mellan två samlingar i MongoDB