Mongodb子文件之间有很多对很多关系

时间:2014-06-05 14:36:08

标签: mongodb aggregation-framework

TL; DR :想象一下,第一个$ match阶段为您提供了几个文档,但是您希望在内部优化,就像$redact一样。但问题是你的子文档有关系,你想在它们之间做$where之类的检查。如何实现这一目标? 我无法放松,因为它会导致性能问题,(1.5 mb的文档有5倍1000的数组长度,单个展开会导致1000x~1mb的文档)。


我的架构如下:

{
    userName: "user44",
    userID: "44",
    posts : [
        ...
        {
            title : "post1",
            id : "123"
            ...
        },
        {
            title : "post2",
            id : "124"
            ...
        },
        ...
    ],
    comments: [
        ...
        {
            id: "1910",
            postId : "123",
            commentTitle : "comment1",
            comment : "some comment",
            user: "user13"
        },
        {
            id: "1911",
            postId : "124",
            title : "comment2",
            commentTitle : "some comment",
            user: "user22"
        },
        {
            id: "1912",
            postId : "124",
            title : "comment2",
            commentTitle : "some comment",
            user: "user22"
        },
        ...
    ], 
    commentUpvotes: [
        ...
        {
            id : 12,
            commentId : "1910",
            upvotedBy: "user91",
            upvoteDate: 1000,         
        },
        {
            id: 13,
            commentId : "1910",
            upvotedBy: "user92",
            upvoteDate: 2000
        },
        {
            id: 14,
            commentId : "1911",
            upvotedBy: "user92",
            upvoteDate: 2100
        },
        ...
    ]
} 

虽然这与我的数据库无关,但原始模式与上面完全相同。因此,上面的示例是一个用户集合,我存储了用户的posts; comments由其他用户commentUpvotes发布的帖子,用于存储有关谁投票的信息。 不要考虑其设计的逻辑和&内容;我做了它们,请不要建议任何其他架构。

问题:我正在寻找一种方法来查找在特定日期之后投票的帖子和评论,例如

 db.users.find("commentUpvotes.upvoteDate" : {$gte:0})

和结果:

{
    "_id" : ObjectId("539065d3cd0f2aac5f55778e"),
    "posts" : [
        {
            title : "post1",
            id : "123"
            ...
        },
        {
            title : "post2",
            id : "124"
            ...
        },
    ],
    "comments" : [
            {
            id: 1910,
            postId : "123",
            title : "comment1",
            comment : "some comment",
            user: "user13"
        },
        {
            id: 1911,
            postId : "124",
            title : "comment2",
            comment : "some comment",
            user: "user22"
        },
    ],
    "commentUpVotes" : [
            {
            id : 12,
            commentId : "1910",
            upvotedBy: "user91",
            upvoteDate: 1000,         
        },
        {
            id: 13,
            commentId : "1910",
            upvotedBy: "user92",
            upvoteDate: 2000
        },
        {
            id: 14,
            commentId : "1911",
            upvotedBy: "user92",
            upvoteDate: 2100
        }
    ]
}

注意:这是一个后置问题,可以找到前者here。我想在这一段中扩展一下。

2 个答案:

答案 0 :(得分:0)

我让我坐了一会儿,因为我在last question上对你做了评论,基本过程是做什么的。我还评论说$redact不是这种类型操作的动物,除了这里的答案之外还有两个原因要解释。我只想说你知道过滤后的值,而不只是过滤它们。

与以前一样,您仍然需要使用$unwind,但不是传统的用法可以在管道中处理要处理的文档数量,而是仅使用之后已过滤数组内容。这里唯一真正的区别是我们注意到“过滤后的数组”实际上将包含多于一个元素,因此您可以适当地处理它:

db.users.aggregate([
    { "$match": {
        "commentUpvotes.upvoteDate": { "$gte": 0 }
    }},
    { "$project": {
        "posts": 1,
        "comments": 1,
        "commentUpVotes": {
            "$setDifference": [
                { 
                    "$map": {
                        "input": "$commentUpvotes",
                        "as": "el",
                        "in": {
                            "$cond": [
                                { "$gte": [ "$$el.upvoteDate", 0 ] },
                                "$$el",
                                false
                            ]
                        }  
                    }
                },
                [false]
            ]
        }
    }},
    { "$project": {
         "posts": 1,
         "comments": 1,
         "kcommentUpVotes": "$commentUpVotes",
         "commentUpVotes": 1
    }},
    { "$unwind": "$commentUpVotes" },
    { "$project": {
        "posts": 1,
        "comments": {
            "$setDifference": [
                { 
                    "$map": {
                        "input": "$comments",
                        "as": "el",
                        "in": {
                            "$cond": [
                                { 
                                    "$eq": [ 
                                        { "$substr": [ "$$el.id", 0, 4 ] }, 
                                        "$commentUpVotes.commentId"
                                    ] 
                                },
                                "$$el",
                                false
                            ]
                        }  
                    }
                },
                [false]
            ]
        },
        "commentUpVotes": "$kcommentUpVotes"
    }},
    { "$unwind": "$comments" },
    { "$group": {
         "_id": "$_id",
         "posts": { "$first": "$posts" },
         "comments": { "$addToSet": "$comments" },
         "kcomments": { "$addToSet": "$comments" },
         "commentUpVotes": { "$first": "$commentUpVotes" }
    }},
    { "$unwind": "$comments" },
    { "$project": { 
        "posts": {
            "$setDifference": [
                { 
                    "$map": {
                        "input": "$posts",
                        "as": "el",
                        "in": {
                            "$cond": [
                                { 
                                    "$eq": [ 
                                        "$$el.id", 
                                        "$comments.postId"
                                    ] 
                                },
                                "$$el",
                                false
                            ]
                        }  
                    }
                },
                [false]
            ]
        },
        "comments": "$kcomments",
        "commentUpVotes": 1
    }},
    { "$unwind": "$posts" },
    { "$group": {
        "_id": "$_id",
        "posts": { "$addToSet": "$posts" },
        "comments": { "$first": "$comments" },
        "commentUpVotes": { "$first": "$commentUpVotes" }
    }}
])

因此,有一点可以准确理解每个阶段(或重复过程)正在做什么以及为什么$unwind这里的操作很重要。

如果你考虑第一个$project,返回的结果总是一个数组。这就是使用$map进行“过滤”的方式,并且非常有意义,因为您期望有几个(在此示例中为全部)匹配的可能性。

重要的部分发生在您尝试将这些值与文档中的另一个数组匹配之前,就像您查看$map的解剖结构一样,该点是将元素与奇异值进行比较。这就是为什么你需要$unwind来获得那些“奇异”值来进行比较的原因。

除了保留“已过滤”数组的副本以使事情更清晰之外,让我们在匹配“comments”数组后跳到该部分。由于“commentUpvotes”数组是“解开”的,因此每个文档都有一个副本,现在它有自己的数组过滤版本。注意每个结果数组只能包含一个元素。

由于这些确实是数组,为了在文档之间组合它们,您需要展开这些“单个元素”数组,然后将它们组合在一起。请记住,虽然“commentsUpvotes”有“三个”匹配,但只有“两个”评论可以匹配,但“三个”匹配“两个”匹配相同的id。这是使用$addToSet进行分组变得很重要的地方,因为您不想复制匹配的帖子。

一旦所有匹配的元素都在数组中,它就会再次到$unwind并重复。

因此,一般前提与前一个例子和问题保持一致。事实上,这里的方法可以被认为是上一个清单的“2.0版本”,因为它可以满足所有情况下的单数和“多”匹配。

这里提到的一个“警告”是这些项目确实相关的基本原则,并且在任何阵列中都没有“孤立”细节。显而易见的原因是,经过测试以匹配从一个数组到另一个不匹配的任何数组将导致空数组。可能还有其他匹配项,但如果其中一个测试结果为空,那么您将不得不处理生成的空数组。

通过简单地测试结果的$size并以其他方式输入单false的值并在稍后阶段对其进行过滤,最终注释的概念很容易。但是为了练习的目的,我正在考虑你的“关系”确实完好无损,并将任何额外的处理留给你自己的实现。

最终结果当然是通过简单地将未过滤的数组相互展开并尝试将相等性与这些记录进行匹配来获得所需的结果,而无需通过相同级别的“井喷”。

答案 1 :(得分:0)

我找到了一种方法,可以在不使用$ unwind和$redact + $$ROOT的情况下使其正常工作。如您所知,$ redact将文档从父级扫描到子级,因此要在我需要使用$$ ROOT的子文档之间进行比较。

由于它仅在文档内部处理,我相信这是最有效的方法。如果有人提出更好的方法,我仍然会很高兴。 $ redact上没有太多资源,我相信下面的代码仍然可以改进:

   
// first query match
{
    "$match": {
        "commentUpvotes.upvoteDate": {
            "$gte": 0
        }
    }
},
// exclude commentUpvotes
{
    $redact: {
        $cond: {
            if: {
                $or: [
                    {
                        $gte: [
                            "$upvoteDate",
                            0
                        ]
                    },
                    {
                        $not: "$upvoteDate"
                    }
                ]
            },
            then: "$$DESCEND",
            else: "$$PRUNE"
        }
    }
},
// exclude comments
{
    $redact: {
        $cond: {
            if: {
                $or: [
                    {
                        $not: "$postId"
                    },
                    {
                        $anyElementTrue: { $map: {
                                input: "$$ROOT.commentUpvotes",
                                as: "el",
                                in: { $cond: { if: { $eq: [  "$$el.commentId",  "$id" ] },
                                        then: true, else: false
                                    }
                                }
                            }
                        }
                    }
                ]
            },
            then: "$$DESCEND",
            else: "$$PRUNE"
        }
    }
},
// exclude posts
{
    $redact: {
        $cond: {
            if: {
                $or: [
                    {
                        $not: "$title"
                    },
                    {
                        $anyElementTrue: {
                            $map: {
                                input: "$$ROOT.comments",
                                as: "el",
                                in: {
                                    $cond: {
                                        if: {
                                            $eq: [
                                                "$$el.postId",
                                                "$id"
                                            ]
                                        },
                                        then: true,
                                        else: false
                                    }
                                }
                            }
                        }
                    }
                ]
            },
            then: "$$DESCEND",
            else: "$$PRUNE"
        }
    }
}