【问题标题】:MongoDB to assist with recommendationsMongoDB 协助提出建议
【发布时间】:2015-09-30 13:48:14
【问题描述】:

我有一个如下所示的 3 个集合架构:

  1. 用户收藏有关于他们的朋友和每位艺术家的收听次数(权重)的信息

    {
        user_id : 1,
        Friends : [3,5,6],
        Artists : [
            {artist_id: 10 , weight : 345},
            {artist_id: 17 , weight : 378}
        ]
    }
    
  2. 艺术家收藏架构包含有关艺术家姓名、各种用户给他们的标签的信息。

    {
        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}
        ]
    }
    
  3. 包含各种标签信息的标签集合。

    {tag_id : 3, tag_value : "HipHop"}
    

我想使用以下规则为用户提供艺术家推荐:

规则 1 :查找用户的朋友而不是用户收听的艺术家,按朋友收听次数的总和排序。

规则2:选择用户使用的任意标签,找到所有没有在用户收听列表中的带有该标签的艺术家,并按照唯一听众的数量对其进行排序。

谁能帮我写一个查询来执行上述操作。

【问题讨论】:

  • 欢迎来到 StackOverflow。您似乎在这里问了多个问题,格式是“一个问题”,希望是“一个明确的答案”。无论如何,这肯定是多个查询。您的第一个意图很明确,您希望从用户的朋友那里获取所有结果并对其进行排序。您的第二个意图不是很清楚,因为您并没有说您是否要做的只是从已经在用户自己的收听结果中的朋友收听结果中排除任何项目。如果你能澄清这一点,那么你实际上可能会问一个问题。
  • 您似乎来自关系背景,请查看此帖子:Transitioning from Relational Databases to MongoDB - Data Models 以及此文档:Database References,MongoDB 不支持连接,而是使用引用。
  • 我基本上是想找到不在用户列表中但在用户好友列表中的艺术家,以便将这些艺术家推荐给用户。第二条规则将要求完成推荐,以使这些艺术家具有用户给其他艺术家的标签。 @BlakesSeven

标签: mongodb mongodb-query aggregation-framework


【解决方案1】:

您需要在此处为​​最终结果做几件事,但第一阶段相对简单。获取您提供的用户对象:

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 集合所使用的大部分相同类型的方法。

如果您需要将实际集合用于大型结果,这也可以很好地扩展,因为所有原则都保持不变。

  1. 第一个聚合操作将新数据插入到存储中

  2. 第二次聚合“更新”数据并增加“权重”字段

作为一个完整的函数列表,并在 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_idweight 属性。

然后将该列表传递给第二个聚合操作,该操作将返回每个指定的“艺术家”,并根据不同的用户大小计算出“权重”。每个艺术家的 DataStore 上都有相同的 .update() 语句的“更新”,并增加了“权重”字段。

一切顺利,最后的操作就是将.find()那些结果和.sort()它们通过组合的“权重”,并将结果简单地返回给传入的回调函数。

所以你会这样使用它:

GetUserRecommendations(1,function(err,results) {
   // results is the sorted list
});

它将返回当前不在该用户列表中但在其朋友列表中的所有艺术家,并按朋友收听计数的组合权重加上该艺术家的不同用户数量的分数排序。

这是您处理来自两个不同集合的数据的方式,您需要将这些数据组合成一个包含各种聚合详细信息的结果。它是多个查询和一个工作空间,也是 MongoDB 理念的一部分,即此类操作以这种方式执行比将它们扔到数据库中以“加入”结果更好。

【讨论】:

    猜你喜欢
    • 2010-11-14
    • 2017-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-21
    • 1970-01-01
    • 2012-07-01
    • 2018-05-07
    相关资源
    最近更新 更多