Mongo聚合管道 - 在另一个集合中进行多个查找

3
我在处理与另一个集合的复杂匹配时遇到了一些困难,不太理解聚合管道的工作原理。目标是获取一个特定用户在分析集合中没有“video_impression”条目的视频列表。
我的数据大致如下:
db={
  "videos": [
    {
      "_id": "1",
      "name": "1's Video",
      "status": "complete",
      "privacy": "public"
    },
    {
      "_id": "2",
      "name": "2's Video",
      "status": "complete",
      "privacy": "public"
    },
    {
      "_id": "3",
      "name": "3's Video",
      "status": "complete",
      "privacy": "public"
    },
    {
      "_id": "4",
      "name": "4's Video",
      "status": "complete",
      "privacy": "private"
    },
    {
      "_id": "5",
      "name": "5's Video",
      "status": "flagged",
      "privacy": "public"
    }
  ],
  "analytics": [
    {
      "_id": "1",
      "user": "1",
      "event": "video_impression",
      "data": {
        "video": "1"
      }
    },
    {
      "_id": "2",
      "user": "2",
      "event": "video_impression",
      "data": {
        "video": "2"
      }
    }
  ]
}

我已经成功地让匹配器工作了,但它是以全局的方式工作的,也就是说它不考虑用户ID,所以它返回的文档与任何人都不匹配。
db.videos.aggregate([
  {
    $match: {
      "status": "complete",
      "privacy": "public"
    }
  },
  {
    $lookup: {
      from: "analytics",
      localField: "_id",
      foreignField: "data.video",
      as: "matched_docs"
    }
  },
  {
    $match: {
      "matched_docs": {
        $eq: []
      }
    }
  }
])

我尝试在管道中添加另一个$lookup阶段来查找user字段,但似乎也没有起作用,因为数据始终为空。这里是我遇到的问题的Mongo Playground链接,可能会更好地解释。

1
你能举个例子,说明在搜索完成后你希望你的文档看起来是什么样子吗?我只是有点难以理解你具体需要什么。你说“目标是获取一个特定用户在分析集合中没有视频印象条目的视频列表”,但是你的所有analytics文档都有一个"event": "video_impression" - undefined
1
对于你在示例文档中的出色表现,展示了你的努力和Mongo Playground链接,特别是对于一个新手来说,非常棒!+1 - undefined
2
@jQueeny 对不起!我应该提供更完整的数据示例,其中还有其他事件,并且集合非常庞大。我的目标是得到一个视频ID数组,其中指定用户没有任何video_impression事件记录,也就是说,指定用户没有对任何结果视频产生印象。 - undefined
1个回答

3

1. 首先,这个聚合最好从analytics集合中运行,而不是videos集合。如果有的话,甚至最好使用users集合。

2. 根据jQueeny的评论,'analytics'集合的示例有点不完整。我假设每个事件只存在一次,所以如果一个用户观看了两个视频,在analytics中会有两个条目,而不仅仅是一个带有视频数组的条目。顺便说一句,我建议你将其更改为每个事件类型在data中是一个对象ID数组,根据你计划如何后续使用它,可以根据用户分别记录,甚至将它们全部合并成一个记录。

anaylytics集合:

[
  { "_id": "1", "user": "1", "event": "video_impression", "data": { "video": "1" } },
  { "_id": "2", "user": "2", "event": "video_impression", "data": { "video": "2" } },
  { "_id": "3", "user": "2", "event": "video_impression", "data": { "video": "3" } },
  { "_id": "4", "user": "2", "event": "liked_video", "data": { "video": "2" } }
]

3. 这里的策略是使用$lookup管道语法来获取所有视频的ID,使用一个非关联子查询。它的优点是只运行一次,然后使用缓存

MongoDB只需要在缓存查询之前运行一次$lookup子查询,因为源集合和外部集合之间没有关系。 $lookup子查询不基于源集合中的任何值。这种行为提高了后续执行此查询的性能。

然而,如果视频集合太大,每个阶段的文档大小超过100 MB,这个管道将失败,您将需要使用关联子查询。

4. 这里使用的方法是:
a) 使用“analytics”集合,筛选出仅为“video_impressions”事件,按“user_id”分组,然后创建一个观看/有印象的视频的集合(唯一数组)。
b) 使用查找操作将“public+complete”视频的所有视频ID放入一个数组中。
c) 对所有视频和有印象的视频进行差异比较。
5. 顺便说一下,如果您只想针对一个用户进行此操作,比如网页/前端页面,那么在第一个匹配阶段的“event”中添加“user: ”。
db.analytics.aggregate([
  {
    // select only the video_impression events
    $match: { event: "video_impression" }
  },
  {
    // first uniquify your users but you should
    // probably run this from the users collection
    $group: {
      _id: "$user",
      impressioned_vids: { "$addToSet": "$data.video" },
      // remove this if you want the user as _id
      user: { "$first": "$user" }
    }
  },
  { $project: { _id: 0 } },
  {
    // uncorrelated subquery which should only run once
    // and then is cached
    $lookup: {
      from: "videos",
      pipeline: [
        {
          $match: {
            status: "complete",
            privacy: "public"
          }
        },
        {
          $group: {
            _id: null,
            video_ids: { $push: "$_id" }
          }
        }
      ],
      as: "all_vids"
    }
  },
  {
    // put it conveniently into a single list
    $set: { all_vids: { $first: "$all_vids.video_ids" } }
  },
  {
    // these are the public-complete videos which that user has not seen
    $set: {
      unimpressed: {
        $setDifference: [ "$all_vids", "$impressioned_vids" ]
      }
    }
  },
  {
    // get rid of the other fields, uncomment to debug
    $project: {
      user: 1,
      unimpressed: 1
    }
  }
])

通过我的修改后的分析集合和你的原始视频集合,这是结果:
[
  {
    "unimpressed": ["1"],
    "user": "2"
  },
  {
    "unimpressed": ["2", "3"],
    "user": "1"
  }
]

Mongo Playground


选项2

如果all_vids的列表对于无关联的$lookup来说太大了,那么就需要使用相关的$lookup,它会针对每个文档执行一次。主要的变化在于$lookup阶段,我会检查视频是否不在印象/观看视频的列表中(或者在这种情况下,交集为空)。这需要将“已看视频”数组分配给let中的一个变量,然后在查找管道中使用它。

  {
    // correlated subquery which executes per record
    $lookup: {
      from: "videos",
      let: { seen_vid_ids: "$impressioned_vids" },
      pipeline: [
        {
          $match: {
            status: "complete",
            privacy: "public",
            $expr: {
              $not: {
                $in: ["$_id", "$$seen_vid_ids"],
              },
            },
          }
        }
      ],
      as: "unseen_vids"
    }
  },
  {
    // these are the public-complete videos which that user has not seen
    $set: {
      unimpressed: "$unseen_vids._id"
    }
  },

完整聚合的Mongo Playground


1
非常感谢您提供如此详细的答案,这让我找到了正确的方向,我成功地通过第二个选项解决了问题。 - undefined

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