Pro konečný výsledek zde musíte udělat několik věcí, ale první fáze jsou relativně jednoduché. Vezměte objekt uživatele, který zadáte:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Nyní za předpokladu, že již máte tato data načtena, pak jde o nalezení stejných struktur pro každého „přítele“ a odfiltrování obsahu pole „Umělci“ do jediného odlišného seznamu. Pravděpodobně zde bude uvažována také každá „váha“ jako celek.
Toto je jednoduchá agregační operace, která nejprve odfiltruje umělce, kteří jsou již v seznamu pro daného uživatele:
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
}
);
„Předfiltr“ je zde jediná opravdu záludná část. Stačí $unwind
pole a $match
znovu, abyste odfiltrovali položky, které nechcete. I když se chceme $unwind
výsledky později, aby je bylo možné zkombinovat, je efektivnější je z pole „nejprve“ odstranit, takže je méně co rozšiřovat.
Zde je tedy $map
Operátor umožňuje kontrolu každého prvku uživatelského pole "Umělci" a také pro porovnání s filtrovaným seznamem "uživatelů" umělců, aby pouze vrátil požadované detaily. $setDifference
se používá ke skutečnému "filtrování" jakýchkoli výsledků, které nebyly vráceny jako obsah pole, ale spíše jako false
.
Poté už zbývá jen $unwind
k denormalizaci obsahu v poli a $group
dát dohromady celkový počet na umělce. Pro zábavu používáme $sort
ukázat, že seznam je vrácen v požadovaném pořadí, ale v pozdější fázi to nebude nutné.
To je alespoň část cesty, protože výsledný seznam by měl obsahovat pouze ostatní umělce, kteří ještě nejsou ve vlastním seznamu uživatele, a seřazený podle součtu „váhy“ od všech umělců, kteří by se případně mohli objevit u více přátel.
Další část bude potřebovat data ze sbírky "umělci", aby bylo možné vzít v úvahu počet posluchačů. Zatímco mongoose má .populate()
metodu, tohle tu opravdu nechcete, protože hledáte počty "odlišných uživatelů". To znamená další implementaci agregace za účelem získání těchto odlišných počtů pro každého interpreta.
V návaznosti na seznam výsledků předchozí operace agregace byste použili $_id
hodnoty jako toto:
// 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
}
);
Zde se trik provádí souhrnně pomocí $map
provést podobnou transformaci hodnot, která je přiváděna do $setUnion
vytvořit z nich jedinečný seznam. Poté $size
Pomocí operátoru zjistíte, jak velký je tento seznam. Dodatečná matematika má dát tomuto číslu nějaký význam, když je aplikován na již zaznamenané váhy z předchozích výsledků.
Samozřejmě to všechno musíte nějak dát dohromady, protože právě teď existují pouze dvě odlišné sady výsledků. Základním procesem je "Hash Table", kde se jako klíč používají jedinečné hodnoty "artist" id a hodnoty "weight" jsou kombinovány.
Můžete to udělat mnoha způsoby, ale protože existuje touha "uspořádat" kombinované výsledky, pak bych preferoval něco "MongoDBish", protože se řídí základními metodami, na které byste již měli být zvyklí.
Šikovný způsob, jak to implementovat, je použití nedb
, který poskytuje úložiště „v paměti“, které používá většinu stejných metod, jaké se používají ke čtení a zápisu do kolekcí MongoDB.
To se také dobře škáluje, pokud potřebujete použít skutečnou kolekci pro velké výsledky, protože všechny principy zůstávají stejné.
-
První operace agregace vloží nová data do úložiště
-
Druhá agregace „aktualizuje“ data a zvyšuje pole „váha“
Jako úplný výpis funkcí a s nějakou další pomocí async
knihovna by to vypadalo takto:
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);
}
);
});
}
Takže po shodě s původním zdrojovým uživatelským objektem jsou hodnoty předány do první agregační funkce, která se spouští v sérii a používá async.waterfall
předat výsledek.
Než k tomu dojde, ale výsledky agregace jsou přidány do DataStore
s běžným .insert()
příkazy, přičemž dbejte na přejmenování _id
pole jako nedb
nemá rád nic jiného než své vlastní _id
hodnoty. Každý výsledek je vložen s artist_id
a weight
vlastnosti z výsledku agregace.
Tento seznam je poté předán druhé agregační operaci, která vrátí každého zadaného „umělce“ s vypočítanou „váhou“ na základě odlišné velikosti uživatele. Existují "aktualizované" se stejným .update()
v DataStore
pro každého umělce a zvýšením pole "váha".
Vše jde dobře, poslední operace je .find()
tyto výsledky a .sort()
jejich kombinovanou "váhou" a jednoduše vrátíte výsledek na předaný zpětným voláním funkce.
Takže byste to použili takto:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
A vrátí všechny umělce, kteří nejsou momentálně v seznamu daného uživatele, ale v jejich seznamech přátel a seřazení podle kombinované váhy počtu přátel, kteří poslouchají, plus skóre z počtu různých uživatelů tohoto umělce.
Takto nakládáte s daty ze dvou různých kolekcí, které je třeba spojit do jednoho výsledku s různými agregovanými detaily. Je to mnohonásobné dotazy a pracovní prostor, ale také součást filozofie MongoDB, že takové operace se lépe provádějí tímto způsobem, než je házet do databáze za účelem „spojení“ výsledků.