Тук трябва да направите няколко неща за крайния резултат, но първите етапи са относително прости. Вземете предоставения от вас потребителски обект:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Ако сега приемем, че вече сте извлекли тези данни, това се свежда до намиране на едни и същи структури за всеки „приятел“ и филтриране на съдържанието на масива на „Артисти“ в един отделен списък. Предполага се, че всяко „тегло“ също ще се разглежда общо тук.
Това е проста операция за събиране, която първо ще филтрира изпълнителите, които вече са в списъка за даден потребител:
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
}
);
„Предварителният филтър“ е единствената наистина трудна част тук. Можете просто да $unwind
масивът и $match
отново, за да филтрирате записите, които не искате. Въпреки че искаме да $unwind
резултатите по-късно, за да ги комбинирате, работи по-ефективно да ги премахнете от масива "първи", така че има по-малко за разширяване.
Така че тук $map
позволява проверка на всеки елемент от потребителския масив „Изпълнители“, а също и за сравнение с филтрирания „потребителски“ списък с изпълнители, за да върне само желаните подробности. $setDifference
се използва за действително „филтриране“ на всички резултати, които не са върнати като съдържание на масива, а по-скоро върнати като false
.
След това има само $unwind
за денормализиране на съдържанието в масива и $group
за да съберем общо за всеки изпълнител. За забавление използваме $sort
за да покаже, че списъкът е върнат в желания ред, но това няма да е необходимо на по-късен етап.
Това е поне част от пътя тук, тъй като полученият списък трябва да включва само други изпълнители, които все още не са в собствения списък на потребителя, и сортирани по сумираното „тегло“ от всички изпълнители, които евентуално биха могли да се появят на множество приятели.
Следващата част ще се нуждае от данни от колекцията "художници", за да вземе предвид броя на слушателите. Докато mongoose има .populate()
метод, наистина не искате това тук, тъй като търсите броя на „отделните потребители“. Това предполага друга реализация на агрегиране, за да се получат тези отделни бройки за всеки изпълнител.
Следвайки списъка с резултати от предишната операция за агрегиране, бихте използвали $_id
стойности като тази:
// 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
}
);
Тук трикът се прави в съвкупност с $map
за извършване на подобна трансформация на стойности, която се подава към $setUnion
за да ги направите уникален списък. След това $size
оператор се прилага, за да разбере колко голям е този списък. Допълнителната математика е да придаде на това число някакво значение, когато се приложи спрямо вече записаните тегла от предишните резултати.
Разбира се, трябва да обедините всичко това по някакъв начин, тъй като в момента има само два различни набора от резултати. Основният процес е „хеш таблица“, където уникалните стойности на идентификатора на „изпълнител“ се използват като ключ, а стойностите на „тегло“ се комбинират.
Можете да направите това по много начини, но тъй като има желание да се „сортират“ комбинираните резултати, моето предпочитание би било нещо „MongoDBish“, тъй като следва основните методи, с които вече трябва да сте свикнали.
Удобен начин за прилагане на това е използването на nedb
, което предоставя хранилище „в паметта“, което използва почти същия тип методи като използваните за четене и запис в MongoDB колекции.
Това също се мащабира добре, ако трябва да използвате действителна колекция за големи резултати, тъй като всички принципи остават същите.
-
Първата операция за агрегиране вмъква нови данни в хранилището
-
Второто агрегиране „актуализира“, че данните увеличават полето „тегло“
Като пълен списък с функции и с друга помощ на async
библиотека ще изглежда така:
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);
}
);
});
}
Така че след съпоставяне на първоначалния потребителски обект източник, стойностите се предават в първата агрегатна функция, която се изпълнява последователно и използва async.waterfall
за да предаде резултата му.
Преди това да се случи, въпреки че резултатите от агрегацията се добавят към DataStore
с обикновен .insert()
изрази, като внимавате да преименувате _id
полета като nedb
не харесва нищо друго освен собствено генерирания си _id
стойности. Всеки резултат се вмъква с artist_id
и weight
свойства от резултата от агрегацията.
След това този списък се предава на втората операция за агрегиране, която ще върне всеки определен „изпълнител“ с изчислено „тегло“ на базата на отделен потребителски размер. Има „актуализирани“ със същия .update()
изявление в DataStore
за всеки изпълнител и увеличаване на полето "тегло".
Всичко върви добре, последната операция е .find()
тези резултати и .sort()
ги по комбинираното „тегло“ и просто връща резултата към предаденото обратно извикване на функцията.
Така че бихте го използвали по следния начин:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
И ще върне всички изпълнители, които в момента не са в списъка на този потребител, но в техните списъци с приятели и подредени по комбинираните тегла на броя на слушанията на приятели плюс резултата от броя на отделните потребители на този изпълнител.
Това е начинът, по който се справяте с данни от две различни колекции, които трябва да комбинирате в един резултат с различни обобщени подробности. Това са множество заявки и работно пространство, но също така и част от философията на MongoDB, че такива операции се изпълняват по-добре по този начин, отколкото да се хвърлят в базата данни за „обединяване“ на резултатите.