sql >> Database teknologi >  >> NoSQL >> MongoDB

MongoDB til at hjælpe med anbefalinger

Du skal gøre et par ting her for dit slutresultat, men de første trin er relativt enkle. Tag det brugerobjekt, du angiver:

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

Hvis du nu antager, at du allerede har disse data hentet, så handler det om at finde de samme strukturer for hver "ven" og filtrere array-indholdet fra "Kunstnere" ud i en enkelt særskilt liste. Formentlig vil hver "vægt" også blive taget i betragtning samlet her.

Dette er en simpel aggregeringsoperation, der først vil bortfiltrere de kunstnere, der allerede er på listen for den givne bruger:

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

"Forfilteret" er den eneste virkelig vanskelige del her. Du kan bare $unwind arrayet og $match igen for at bortfiltrere de poster, du ikke ønsker. Selvom vi gerne vil $unwind resultaterne senere for at kombinere dem, virker det mere effektivt at fjerne dem fra arrayet "først", så der er mindre at udvide.

Så her er $map operatøren tillader inspektion af hvert element i brugerens "Artists"-array og også til sammenligning med den filtrerede "bruger"-kunstnerliste for blot at returnere de ønskede detaljer. $setDifference bruges til faktisk at "filtrere" alle resultater, der ikke blev returneret som matrixindholdet, men snarere returneret som false .

Derefter er der bare $unwind at denormalisere indholdet i arrayet og $group at samle en total pr. kunstner. For sjov bruger vi $sort for at vise, at listen returneres i ønsket rækkefølge, men det vil ikke være nødvendigt på et senere tidspunkt.

Det er i det mindste en del af vejen her, da den resulterende liste kun bør være andre kunstnere, der ikke allerede er på brugerens egen liste, og sorteret efter den summerede "vægt" fra alle kunstnere, der muligvis kan optræde på flere venner.

Den næste del skal bruge data fra "kunstner"-samlingen for at tage højde for antallet af lyttere. Mens mongoose har en .populate() metode, vil du virkelig ikke have dette her, da du leder efter "særskilte bruger"-tæller. Dette indebærer en anden aggregeringsimplementering for at få disse særskilte tal for hver kunstner.

I forlængelse af resultatlisten fra den tidligere aggregeringsoperation ville du bruge $_id værdier som denne:

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

Her udføres tricket samlet med $map at udføre en lignende transformation af værdier, som føres til $setUnion at gøre dem til en unik liste. Derefter $size operatør anvendes for at finde ud af, hvor stor den liste er. Den ekstra matematik er at give det tal en vis betydning, når det anvendes mod de allerede registrerede vægte fra de tidligere resultater.

Selvfølgelig skal du samle alt dette på en eller anden måde, da der lige nu kun er to forskellige sæt resultater. Den grundlæggende proces er en "Hash Table", hvor de unikke "artist" id-værdier bruges som en nøgle, og "weight"-værdierne kombineres.

Du kan gøre dette på en række måder, men da der er et ønske om at "sortere" de kombinerede resultater, så ville min præference være noget "MongoDBish", da det følger de grundlæggende metoder, du allerede burde være vant til.

En praktisk måde at implementere dette på er at bruge nedb , som giver et "in memory"-lager, der bruger meget af den samme type metoder, som bruges til at læse og skrive til MongoDB-samlinger.

Dette kan også skaleres godt, hvis du skulle bruge en egentlig samling til store resultater, da alle principperne forbliver de samme.

  1. Første aggregeringsoperation indsætter nye data i butikken

  2. Anden aggregering "opdaterer" disse data og øger "vægt"-feltet

Som en komplet funktionsliste og med anden hjælp fra async bibliotek ville det se sådan ud:

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 at have matchet det oprindelige kildebrugerobjekt overføres værdierne til den første aggregerede funktion, som udføres i serie og bruger async.waterfall for at bestå dets resultat.

Før det sker, føjes aggregeringsresultaterne til DataStore med almindelig .insert() sætninger, idet du sørger for at omdøbe _id felter som nedb kan ikke lide andet end dets eget selvgenererede _id værdier. Hvert resultat indsættes med artist_id og weight egenskaber fra aggregeringsresultatet.

Denne liste videregives derefter til den anden aggregeringsoperation, som vil returnere hver specificeret "kunstner" med en beregnet "vægt" baseret på den distinkte brugerstørrelse. Der er "opdaterede" med den samme .update() sætning i DataStore for hver kunstner og øge feltet "vægt".

Alt går godt, den sidste operation er at .find() disse resultater og .sort() dem med den kombinerede "vægt", og returner blot resultatet til det beståede tilbagekald til funktionen.

Så du ville bruge det sådan her:

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

Og det vil returnere alle de kunstnere, der ikke i øjeblikket er på denne brugers liste, men på deres vennelister og sorteret efter den kombinerede vægt af vennelytningen plus score fra antallet af distinkte brugere af den pågældende kunstner.

Sådan håndterer du data fra to forskellige samlinger, som du skal kombinere til et enkelt resultat med forskellige aggregerede detaljer. Det er flere forespørgsler og et arbejdsområde, men også en del af MongoDB-filosofien, at sådanne operationer udføres bedre på denne måde end at smide dem i databasen for at "join" resultater.




  1. Opretter forbindelse til RedisToGo gennem Node.JS

  2. Cosmos DB Mongo API Hvordan man administrerer anmodningshastigheden er stor

  3. Sådan konverteres streng til objectId i LocalField for $lookup Mongodb

  4. Mongoose finder dokumenter, hvis array indeholder en værdi