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.
-
Første aggregeringsoperation indsætter nye data i butikken
-
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.