MongoDB嵌套数组交集查询

8
感谢您的帮助。我有一个结构如下的 MongoDB 数据库:
```html

...

```
{
  '_id' : objectID(...),

  'userID' : id,

  'movies' : [{

       'movieID' : movieID,

       'rating' : rating
   }]
 }

我的问题是:

我想要查找一个特定的用户,该用户具有“userID”:3,例如获取所有电影,然后我想获取所有其他至少有15个或更多电影与相同“movieID”的用户,然后使用该组仅选择具有这些15部电影相似度并具有我选择的一个额外的“movieID”的用户。

我已经尝试过聚合,但失败了,如果我像从用户那里获取所有用户电影一样进行单个查询,那么循环每个用户电影并进行比较需要大量时间。

有任何建议吗?

谢谢

1个回答

11

使用聚合框架有几种方法可以做到这一点。

以下是一个简单的数据集示例:

{
    "_id" : ObjectId("538181738d6bd23253654690"),
    "movies": [
        { "_id": 1, "rating": 5 },
        { "_id": 2, "rating": 6 },
        { "_id": 3, "rating": 7 }
    ]
},
{
    "_id" : ObjectId("538181738d6bd23253654691"),
    "movies": [
        { "_id": 1, "rating": 5 },
        { "_id": 4, "rating": 6 },
        { "_id": 2, "rating": 7 }
    ]
},
{
    "_id" : ObjectId("538181738d6bd23253654692"),
    "movies": [
        { "_id": 2, "rating": 5 },
        { "_id": 5, "rating": 6 },
        { "_id": 6, "rating": 7 }
    ]
}

以第一个“用户”为例,现在您想查找另外两个用户是否有至少两个相同的电影。

对于MongoDB 2.6及以上版本,您可以使用$setIntersection运算符以及$size运算符:

db.users.aggregate([

    // Match the possible documents to reduce the working set
    { "$match": {
        "_id": { "$ne": ObjectId("538181738d6bd23253654690") },
        "movies._id": { "$in": [ 1, 2, 3 ] },
        "$and": [
            { "movies": { "$not": { "$size": 1 } } }
        ]
    }},

    // Project a copy of the document if you want to keep more than `_id`
    { "$project": {
        "_id": {
            "_id": "$_id",
            "movies": "$movies"
        },
        "movies": 1,
    }},

    // Unwind the array
    { "$unwind": "$movies" },

    // Build the array back with just `_id` values
    { "$group": {
        "_id": "$_id",
        "movies": { "$push": "$movies._id" }
    }},

    // Find the "set intersection" of the two arrays
    { "$project": {
        "movies": {
            "$size": {
                "$setIntersection": [
                   [ 1, 2, 3 ],
                   "$movies"
                ]
            }
        }
    }},

    // Filter the results to those that actually match
    { "$match": { "movies": { "$gte": 2 } } }

])

在MongoDB早期版本中仍然可以实现,只需要多执行几个步骤,而不使用这些操作符:

db.users.aggregate([

    // Match the possible documents to reduce the working set
    { "$match": {
        "_id": { "$ne": ObjectId("538181738d6bd23253654690") },
        "movies._id": { "$in": [ 1, 2, 3 ] },
        "$and": [
            { "movies": { "$not": { "$size": 1 } } }
        ]
    }},

    // Project a copy of the document along with the "set" to match
    { "$project": {
        "_id": {
            "_id": "$_id",
            "movies": "$movies"
        },
        "movies": 1,
        "set": { "$cond": [ 1, [ 1, 2, 3 ], 0 ] }
    }},

    // Unwind both those arrays
    { "$unwind": "$movies" },
    { "$unwind": "$set" },

    // Group back the count where both `_id` values are equal
    { "$group": {
        "_id": "$_id",
        "movies": {
           "$sum": {
               "$cond":[
                   { "$eq": [ "$movies._id", "$set" ] },
                   1,
                   0
               ]
           }
        } 
    }},

    // Filter the results to those that actually match
    { "$match": { "movies": { "$gte": 2 } } }
])

详细说明

可能需要一些时间来理解,因此我们可以逐个阶段查看并分解以查看它们在做什么。

$match:您不希望对集合中的每个文档进行操作,因此这是一个机会,可以排除那些未必是匹配项的项目,即使还有更多工作要做才能找到确切的匹配项。因此,明显的事情是排除相同的“用户”,然后只匹配与该“用户”找到的至少一个相同电影的文档。

下一件有意义的事情是考虑当你想要匹配n个条目时,只有具有大于n-1的“电影”数组的文档可能实际包含匹配项。这里使用$and看起来很奇怪,并不是特别必需的,但如果所需的匹配数为4,则语句的实际部分将如下所示:

        "$and": [
            { "movies": { "$not": { "$size": 1 } } },
            { "movies": { "$not": { "$size": 2 } } },
            { "movies": { "$not": { "$size": 3 } } }
        ]

因此,您基本上“排除”长度不足以具有n个匹配项的数组。需要注意的是,查询形式中的$size运算符与聚合框架中的$size不同。例如,无法使用不等运算符,如$gt,因为它的目的是专门匹配请求的“大小”。因此,这种查询形式用于指定所有可能比其小的大小。

$project:该语句有几个目的,其中一些取决于您拥有的MongoDB版本而有所不同。首先(可选),正在保留文档副本在_id值下,以便这些字段不会被其他步骤修改。这里的另一部分是将“movies”数组保留在文档顶部,作为下一阶段的副本。

在针对2.6版本之前的版本呈现的版本中,还存在一个表示“电影”的_id值的附加数组。这里使用$cond运算符的用途只是创建“字面”数组的一种方式。有趣的是,MongoDB 2.6引入了一个名为$literal的运算符,以完全执行此操作,而不需要我们在此处使用$cond的有趣方式。

$unwind:为了进一步处理电影数组,必须对其进行展开,因为无论如何,这都是隔离需要与“集”匹配的条目的现有_id值的唯一方法。因此,在2.6版本之前的版本中,您需要“展开”存在的两个数组。

$group:对于MongoDB 2.6及更高版本,您只需分组回包含删除“ratings”的电影_id值的数组。

对于2.6版本之前的版本,由于所有值都呈现在“并排”(且存在大量重复),因此需要比较两个值,以查看它们是否相同。如果为true,则告诉$cond运算符语句返回 1 的值,或者在条件为false时返回0的值。这直接通过$sum传递回来,以将数组中匹配元素的数量总计到所需的“set”。

$project: 在MongoDB 2.6及更高版本中的不同之处在于,由于您已经推回了“电影”_id值的数组,因此您随后使用$setIntersection直接比较这些数组。由于其结果是包含相同元素的数组,因此将其包装在$size运算符中,以确定在该匹配集中返回了多少个元素。 $match: 这里实现的最终阶段是仅匹配那些交错元素计数大于或等于所需数量的文档的清晰步骤。
那基本上就是这样做的方式。 在2.6之前有点笨重,并且由于通过复制由集合的所有可能值找到的每个数组成员而进行的扩展会需要更多的内存,但这仍然是一种有效的方法。
你需要做的就是将它与更大的n匹配值一起应用以满足您的条件,并且当然要确保您的原始用户匹配具有所需的n个可能性。 否则,只需根据“用户”的“电影”数组的长度生成n-1

非常好的解释。感谢您用心讲解! - joe_coolish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接