Você precisa fazer algumas coisas aqui para o resultado final, mas os primeiros estágios são relativamente simples. Pegue o objeto de usuário que você fornece:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Agora, supondo que você já tenha esses dados recuperados, isso se resume a encontrar as mesmas estruturas para cada "amigo" e filtrar o conteúdo da matriz de "Artistas" em uma única lista distinta. Presumivelmente, cada "peso" também será considerado no total aqui.
Esta é uma operação de agregação simples que primeiro filtrará os artistas que já estão na lista para um determinado usuário:
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
}
);
O "pré-filtro" é a única parte realmente complicada aqui. Você pode apenas
$unwind
a matriz e $match
novamente para filtrar as entradas que você não deseja. Mesmo que queiramos $unwind
os resultados posteriormente para combiná-los, é mais eficiente removê-los da matriz "primeiro", para que haja menos a expandir. Então aqui está o
$map
O operador permite a inspeção de cada elemento do array "Artists" do usuário e também para comparação com a lista de artistas "user" filtrada para retornar apenas os detalhes desejados. O $setDifference
é usado para realmente "filtrar" quaisquer resultados que não foram retornados como o conteúdo do array, mas sim retornados como false
. Depois disso, há apenas o
$unwind
para desnormalizar o conteúdo na matriz e o $group
para reunir um total por artista. Por diversão, estamos usando $sort
para mostrar que a lista é retornada na ordem desejada, mas isso não será necessário em um estágio posterior. Isso é pelo menos parte do caminho aqui, pois a lista resultante deve ser apenas outros artistas que ainda não estão na lista do próprio usuário e classificada pelo "peso" somado de qualquer artista que possa aparecer em vários amigos.
A próxima parte vai precisar de dados da coleção de "artistas" para levar em conta o número de ouvintes. Enquanto o mangusto tem um
.populate()
método, você realmente não quer isso aqui, pois está procurando as contagens de "usuários distintos". Isso implica em outra implementação de agregação para obter essas contagens distintas para cada artista. Seguindo a lista de resultados da operação de agregação anterior, você usaria o
$_id
valores assim:// 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
}
);
Aqui o truque é feito em conjunto com
$map
para fazer uma transformação semelhante de valores que é alimentado para $setUnion
para torná-los uma lista única. Em seguida, o $size
é aplicado para descobrir o tamanho dessa lista. A matemática adicional é dar a esse número algum significado quando aplicado contra os pesos já registrados dos resultados anteriores. Claro que você precisa reunir tudo isso de alguma forma, pois agora existem apenas dois conjuntos distintos de resultados. O processo básico é uma "Tabela de Hash", onde os valores de id exclusivos do "artista" são usados como chave e os valores de "peso" são combinados.
Você pode fazer isso de várias maneiras, mas como há um desejo de "classificar" os resultados combinados, minha preferência seria algo "MongoDBish", pois segue os métodos básicos aos quais você já deve estar acostumado.
Uma maneira prática de implementar isso é usar
nedb
, que fornece um armazenamento "na memória" que usa muito do mesmo tipo de métodos usados para ler e gravar em coleções do MongoDB. Isso também se adapta bem se você precisar usar uma coleção real para grandes resultados, pois todos os princípios permanecem os mesmos.
-
A primeira operação de agregação insere novos dados na loja
-
A segunda agregação "atualiza" esses dados e incrementa o campo "peso"
Como uma lista completa de funções e com alguma outra ajuda do
async
biblioteca ficaria assim: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);
}
);
});
}
Então, depois de corresponder ao objeto de usuário de origem inicial, os valores são passados para a primeira função de agregação, que está sendo executada em série e usando
async.waterfall
para passar o resultado. Antes que isso aconteça, os resultados da agregação são adicionados ao
DataStore
com .insert()
normal declarações, tomando o cuidado de renomear o _id
campos como nedb
não gosta de nada além de seu próprio _id
gerado por si mesmo valores. Cada resultado é inserido com artist_id
e weight
propriedades do resultado da agregação. Essa lista é então passada para a segunda operação de agregação que retornará cada "artista" especificado com um "peso" calculado com base no tamanho do usuário distinto. Existem os "atualizados" com o mesmo
.update()
declaração no DataStore
para cada artista e incrementando o campo "peso". Tudo indo bem, a operação final é
.find()
esses resultados e .sort()
eles pelo "peso" combinado e simplesmente retornar o resultado para o retorno de chamada passado para a função. Então você usaria assim:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
E retornará todos os artistas que não estão atualmente na lista desse usuário, mas em suas listas de amigos e ordenados pelos pesos combinados da contagem de ouvintes do amigo mais a pontuação do número de usuários distintos desse artista.
É assim que você lida com dados de duas coleções diferentes que você precisa combinar em um único resultado com vários detalhes agregados. São várias consultas e um espaço de trabalho, mas também faz parte da filosofia do MongoDB que essas operações sejam melhor executadas dessa maneira do que jogá-las no banco de dados para "juntar" resultados.