Je moet hier een aantal dingen doen voor je eindresultaat, maar de eerste fasen zijn relatief eenvoudig. Neem het door u verstrekte gebruikersobject:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Ervan uitgaande dat je die gegevens al hebt opgehaald, komt dit neer op het vinden van dezelfde structuren voor elke "vriend" en het filteren van de array-inhoud van "Artiesten" in een enkele afzonderlijke lijst. Vermoedelijk zal hier ook elk "gewicht" in totaal worden beschouwd.
Dit is een eenvoudige aggregatiebewerking die eerst de artiesten filtert die al in de lijst voor de gegeven gebruiker staan:
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
}
);
Het "voorfilter" is het enige echt lastige deel hier. Je kunt gewoon $unwind
de array en $match
nogmaals om de items die u niet wilt eruit te filteren. Ook al willen we $unwind
de resultaten later om ze te combineren, werkt het efficiënter om ze "eerst" uit de array te verwijderen, zodat er minder uit te breiden is.
Dus hier de $map
operator staat inspectie toe van elk element van de gebruiker "Artiesten"-array en ook voor vergelijking met de gefilterde "gebruiker" artiestenlijst om alleen de gewenste details terug te geven. De $setDifference
wordt gebruikt om daadwerkelijk alle resultaten te "filteren" die niet zijn geretourneerd als de array-inhoud, maar eerder zijn geretourneerd als false
.
Daarna is er alleen nog de $unwind
om de inhoud in de array en de $group
om een totaal per artiest bij elkaar te brengen. Voor de lol gebruiken we $sort
om aan te tonen dat de lijst in de gewenste volgorde wordt geretourneerd, maar dat is in een later stadium niet nodig.
Dat is in ieder geval een deel van de weg hierheen, aangezien de resulterende lijst alleen andere artiesten zou moeten zijn die nog niet in de eigen lijst van de gebruiker staan, en gesorteerd op het opgetelde "gewicht" van alle artiesten die mogelijk op meerdere vrienden zouden kunnen verschijnen.
Het volgende deel heeft gegevens uit de collectie "artiesten" nodig om rekening te houden met het aantal luisteraars. Terwijl mangoest een .populate()
. heeft methode, wilt u dit hier echt niet omdat u op zoek bent naar de "verschillende gebruikers"-tellingen. Dit impliceert een andere aggregatie-implementatie om die verschillende tellingen voor elke artiest te krijgen.
In navolging van de resultatenlijst van de vorige aggregatiebewerking, zou u de $_id
gebruiken waarden als deze:
// 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
}
);
Hier wordt de truc gecombineerd met $map
om een vergelijkbare transformatie van waarden uit te voeren die wordt ingevoerd in $setUnion
om er een unieke lijst van te maken. Dan de $size
operator wordt toegepast om erachter te komen hoe groot die lijst is. De extra wiskunde is om dat getal enige betekenis te geven wanneer het wordt toegepast op de reeds geregistreerde gewichten van de vorige resultaten.
Natuurlijk moet je dit allemaal op de een of andere manier bij elkaar brengen, want op dit moment zijn er slechts twee verschillende reeksen resultaten. Het basisproces is een "Hash-tabel", waarbij de unieke "artiest"-id-waarden als sleutel worden gebruikt en de "gewichts"-waarden worden gecombineerd.
U kunt dit op een aantal manieren doen, maar aangezien er een wens is om de gecombineerde resultaten te "sorteren", zou mijn voorkeur uit gaan naar iets "MongoDBish", aangezien het de basismethoden volgt waaraan u al gewend zou moeten zijn.
Een handige manier om dit te implementeren is het gebruik van nedb
, die een "in memory"-opslag biedt die veel van hetzelfde type methoden gebruikt als die worden gebruikt om MongoDB-verzamelingen te lezen en te schrijven.
Dit schaalt ook goed als je een echte verzameling moet gebruiken voor grote resultaten, omdat alle principes hetzelfde blijven.
-
Eerste aggregatiebewerking voegt nieuwe gegevens toe aan de winkel
-
Tweede aggregatie "update" die gegevens en vergroot het veld "gewicht"
Als een volledige functielijst, en met wat andere hulp van de async
bibliotheek zou het er als volgt uitzien:
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);
}
);
});
}
Dus na het matchen van het initiële brongebruikersobject worden de waarden doorgegeven aan de eerste aggregatiefunctie, die in serie wordt uitgevoerd en async.waterfall
gebruikt. om het resultaat door te geven.
Voordat dat gebeurt, worden de aggregatieresultaten toegevoegd aan de DataStore
met gewone .insert()
verklaringen, waarbij u ervoor zorgt dat u de _id
. hernoemt velden als nedb
houdt niet van iets anders dan zijn eigen zelf gegenereerde _id
waarden. Elk resultaat wordt ingevoegd met artist_id
en weight
eigenschappen uit het aggregatieresultaat.
Die lijst wordt vervolgens doorgegeven aan de tweede aggregatiebewerking die elke gespecificeerde "artiest" zal retourneren met een berekend "gewicht" op basis van de verschillende gebruikersgrootte. Er zijn de "bijgewerkt" met dezelfde .update()
statement op de DataStore
voor elke artiest en het verhogen van het veld "gewicht".
Alles gaat goed, de laatste handeling is om .find()
die resultaten en .sort()
ze door het gecombineerde "gewicht", en retourneer eenvoudig het resultaat naar de doorgegeven in callback naar de functie.
Dus je zou het als volgt gebruiken:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
En het zal alle artiesten retourneren die momenteel niet in de lijst van die gebruiker staan, maar in hun vriendenlijsten en geordend op het gecombineerde gewicht van de luistertelling van vrienden plus de score van het aantal verschillende gebruikers van die artiest.
Zo ga je om met data uit twee verschillende collecties die je moet combineren tot één resultaat met verschillende geaggregeerde details. Het zijn meerdere query's en een werkruimte, maar het maakt ook deel uit van de MongoDB-filosofie dat dergelijke bewerkingen beter op deze manier kunnen worden uitgevoerd dan ze naar de database te gooien om resultaten te "meedoen".