您需要在此处为最终结果做几件事,但第一阶段相对简单。获取您提供的用户对象:
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 运算符来找出该列表有多大。额外的数学运算是在应用于之前结果中已记录的权重时赋予该数字一些含义。
当然,您需要以某种方式将所有这些结合在一起,因为现在只有两组不同的结果。基本流程是一个“哈希表”,其中唯一的“艺术家”id 值作为键,“权重”值组合在一起。
您可以通过多种方式执行此操作,但由于希望对组合结果进行“排序”,因此我更喜欢“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 传递结果。
在此之前,虽然聚合结果使用常规 .insert() 语句添加到 DataStore,但注意将 _id 字段重命名为 nedb 除了它自己生成的 _id价值观。每个结果都插入有来自聚合结果的artist_id 和weight 属性。
然后将该列表传递给第二个聚合操作,该操作将返回每个指定的“艺术家”,并根据不同的用户大小计算出“权重”。每个艺术家的 DataStore 上都有相同的 .update() 语句的“更新”,并增加了“权重”字段。
一切顺利,最后的操作就是将.find()那些结果和.sort()它们通过组合的“权重”,并将结果简单地返回给传入的回调函数。
所以你会这样使用它:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
它将返回当前不在该用户列表中但在其朋友列表中的所有艺术家,并按朋友收听计数的组合权重加上该艺术家的不同用户数量的分数排序。
这是您处理来自两个不同集合的数据的方式,您需要将这些数据组合成一个包含各种聚合详细信息的结果。它是多个查询和一个工作空间,也是 MongoDB 理念的一部分,即此类操作以这种方式执行比将它们扔到数据库中以“加入”结果更好。