我有一个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:选择用户使用的任何标签,找到所有具有此标签且不在用户收听列表中的艺术家,并按唯一收听者的数量对其进行排序。
任何人都可以帮我写一个查询来执行上述操作。
答案 0 :(得分:2)
你需要在这里做一些事情才能得到最终结果,但第一阶段相对简单。获取您提供的用户对象:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
现在假设您已经检索到了这些数据,那么这归结为为每个"朋友找到相同的结构"并筛选出" Artists"的数组内容。到一个单独的清单。大概每个"重量"这里也将全部考虑。
这是一个简单的聚合操作,它将首先过滤出已列入给定用户的艺术家:
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
结果以便将它们组合起来,但是从数组中删除它们会更有效率#34;首先",因此扩展的结果更少。
所以这里$map
运算符允许检查用户的每个元素" Artists"数组,也用于与过滤的"用户"进行比较艺术家列表只返回想要的细节。 $setDifference
用于实际"过滤"任何未作为数组内容返回的结果,而是返回为false
。
之后,只有$unwind
取消规范化数组中的内容,$group
将每个艺术家的总数归为一组。为了好玩,我们使用$sort
来显示列表按所需顺序返回,但在以后阶段不需要。
这至少是这里的一部分,因为结果列表应该只是其他未在用户自己的列表中的艺术家,并按照总和"权重"排序。来自任何可能出现在多个朋友身上的艺术家。
接下来的部分将需要来自"艺术家"的数据。集合以便将听众的数量考虑在内。虽然猫鼬采用.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值用作键和" weight"值组合在一起。
你可以通过多种方式实现这一目标,但因为我们希望能够排序"合并后的结果然后我的偏好将是" 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哲学的一部分,这种操作比这更好地执行,而不是将它们扔到数据库中以加入"结果