我有一个3集合架构,如下所示:
用户收藏有关于他们的朋友和每个艺术家的收听计数(权重)的信息
{
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
}
艺术家集合模式包含有关艺术家的名称、不同用户给他们的标记的信息。
{
artistID : 56,
name : "Ed Sheeran",
user_tag : [
{user_id : 2, tag_id : 6},
{user_id : 2, tag_id : 5},
{user_id : 3, tag_id : 7}
]
}
包含各种标记信息的标记集合。
{tag_id : 3, tag_value : "HipHop"}
我想使用以下规则为用户提供艺术家推荐:
规则1:找到用户的朋友而不是用户所听的艺术家,按朋友的收听次数之和排序。
规则2:选择用户使用的任何标签,找到该标签不在用户监听列表中的所有艺术家,并按唯一监听者的数量排序。
有谁能帮我写一个查询来执行上面的操作吗?
最佳答案
您需要在这里做一些事情来获得最终结果,但第一阶段相对简单。获取您提供的用户对象:
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
来显示列表是按所需的顺序返回的,但在稍后阶段不需要这样做。这至少是其中的一部分,因为最终的列表应该只是用户自己列表中没有的其他艺术家,并根据可能出现在多个好友上的任何艺术家的总和“权重”进行排序。
下一部分将需要来自“艺术家”集合的数据,以便考虑听众的数量。虽然Mongoose有一个
$sort
方法,但您确实不希望在这里看到这个,因为您正在寻找“独特的用户”计数。这意味着另一个聚合实现,以便为每个艺术家获取这些不同的计数。在上一个聚合操作的结果列表中,您将使用如下
.populate()
值:// 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
}
);
这里,这个技巧与
$_id
一起完成,对输入到$map
的值进行类似的转换,使它们成为一个唯一的列表。然后应用$setUnion
运算符来找出该列表有多大。另一种数学方法是,当对先前结果中已经记录的权重应用时,给这个数字一些含义。当然你需要以某种方式把所有这些结合起来,因为现在只有两组不同的结果。基本过程是一个“哈希表”,其中唯一的“艺术家”id值用作键,并将“权重”值组合在一起。
你可以用很多方法来实现这一点,但是由于人们希望对合并后的结果进行“排序”,所以我更喜欢“mongodbish”,因为它遵循了你已经习惯的基本方法。
实现这一点的一种简便方法是使用
$size
,它提供了一个“内存中”存储,它使用与mongodb集合的读写方法大致相同的类型。如果需要将实际集合用于大型结果,这也可以很好地扩展,因为所有原则都保持不变。
第一个聚合操作将新数据插入存储区
第二个聚合“更新”数据并增加“权重”字段
作为一个完整的函数列表,在
nedb
库的一些其他帮助下,它将如下所示: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
传递其结果。在这之前,尽管聚合结果被添加到带有常规
async.waterfall
语句的DataStore
中,但要注意将.insert()
字段重命名为_id
除了它自己生成的nedb
值之外,其他都不喜欢。每个结果都从聚合结果中插入_id
和artist_id
属性。然后将该列表传递给第二个聚合操作,该操作将返回每个指定的“艺术家”,并根据不同的用户大小计算“权重”。每个艺术家的
weight
上都有相同的.update()
语句的“updated”,并递增“weight”字段。一切顺利,最后的操作是通过组合的“权重”将结果
DataStore
和.find()
返回给函数的传入回调。所以你可以这样使用它:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
它将返回当前不在该用户列表中但在其好友列表中的所有艺术家,并按好友收听计数加上该艺术家的不同用户数的分数的组合权重排序。
这是处理来自两个不同集合的数据的方式,您需要将它们组合成具有各种聚合细节的单个结果。它是多个查询和一个工作空间,但也是MongoDB Philosopy的一部分,这样的操作比将它们扔到数据库中“连接”结果更好。
关于mongodb - MongoDB协助推荐,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/32857398/