【问题标题】:Improving mongo aggregation to build bipartite graph改进 mongo 聚合以构建二分图
【发布时间】:2019-06-23 07:01:53
【问题描述】:

我有两个包含员工和项目的集合。每个项目都有一个标签数组,每个员工都有一个偏好数组,其中每个偏好都是一个具有标签和分数属性的对象(员工偏好有一个基于他们选择它们的顺序的分数)。

员工示例:

[{
    "name":"employee1",
    "preferences":[
        {
            "tag": "tag1",
            "score": 3
        },
        {
            "tag": "tag2",
            "score": 2
        }
    ]
},
{
    "name":"employee2",
    "preferences":[
        {
            "tag": "tag2",
            "score": 3
        },
        {
            "tag": "tag3",
            "score": 2
        }
    ]
}]

项目示例:

[{
    "name":"project1",
    "tags":["tag1", "tag2"]
},
{
    "name":"project2",
    "tags":["tag2", "tag3"]
}]

期望的输出:

[{
    "project": "project1",
    "employee": "employee1",
    "score": 5
},
{
    "project": "project2",
    "employee": "employee1",
    "score": 2
},
{
    "project": "project1",
    "employee": "employee2",
    "score": 2
},
{
    "project": "project2",
    "employee": "employee2",
    "score": 5
}]

我写了这个聚合管道,它给了我想要的输出(不完全相同,只是添加一个投影阶段):

db.employees.aggregate([
  {
    '$unwind' : '$preferences'
  },
  {
    '$lookup' : {
      'from' : 'projects',
      'localField' : 'preferences.tag',
      'foreignField': 'tags',
      'as' : 'match'
    }
  },
  {
    '$group' :
      {
        '_id': {'project' : '$match.name', 'employee' : '$name' },
        'score' : { '$sum' : '$preferences.score' }
      }
  },
  {
    '$unwind': '$_id.project'
  },
  {
    '$group' :
      {
        '_id': {'project' : '$_id.project', 'employee' : '$_id.employee' },
        'score' : { '$sum' : '$score' }
      }
  }
])

现在这是一个可行的解决方案,但我不确定它是否是最好的。我正在对可变数量的文档进行一些测试,每个集合从 100 到 5000,我将它与使用嵌套 for 循环的简单迭代方法进行比较,结果是迭代方法实际上更快更轻(在更短的时间内执行,使用更少的内存)。 我认为聚合会更好地增加文档数量,但似乎并非如此。你有什么改进管道的建议吗?一般有什么想法吗?提前谢谢你:)

【问题讨论】:

    标签: mongodb aggregation-framework


    【解决方案1】:

    首先要做的是:确保您的$lookup 目标foreignField 已编入索引。然后,从描述您的数据特定特征开始:一个用户有很多偏好是否很常见?或者一个项目有很多标签? employeesprojects 的大小不均衡也会影响性能。

    现在,让我们开始实验吧。

    1. 基线(您的解决方案)。记得索引projects.tags
    db.employees.aggregate([
      {$unwind: '$preferences'},
      {$lookup: {
        from: 'projects',
        localField: 'preferences.tag',
        foreignField: 'tags',
        as: 'match'
      }},
      {$group: {
        _id: {project: '$match.name', employee: '$name'},
        score: {$sum: '$preferences.score'}
      }},
      {$unwind: '$_id.project'},
      {$group: {
        _id: {project: '$_id.project', employee: '$_id.employee'},
        score: {$sum: '$score'}
      }}
    ])
    
    1. 摆脱一个$group 阶段。记得索引projects.tags
    db.employees.aggregate([
      {$unwind: '$preferences'},
      {$lookup: {
        from: 'projects',
        localField: 'preferences.tag',
        foreignField: 'tags',
        as: 'match'
      }},
      {$unwind: '$match'},
      {$group: {
        _id: {project: '$match.name', employee: '$name'},
        score: {$sum: '$preferences.score'}
      }}
    ])
    
    1. 摆脱 both $group 阶段。这employees.preferences.tag 唯一的情况下才有效(它会多次计算重复项)。记得索引projects.tags
    db.employees.aggregate([
      {$lookup: {
        from: 'projects',
        localField: 'preferences.tag',
        foreignField: 'tags',
        as: 'match'}},
      {$unwind: '$match'},
      {$project: {
        _id: 0,
        employee: '$name',
        project: '$match.name',
        score: {$reduce: {
          input: '$preferences',
          initialValue: 0,
          in: {$cond: [
            {$in: ['$$this.tag', '$match.tags']},
            {$add: ['$$this.score', '$$value']},
            '$$value'
          ]}
        }}
      }}
    ])
    
    1. 与 3 类似,但方向相反。这employees.preferences.tag 唯一的情况下才有效(它会多次计算重复项)。记得索引employees.preferences.tag
    db.projects.aggregate([
      {$lookup: {
        from: 'employees',
        localField: 'tags',
        foreignField: 'preferences.tag',
        as: 'match'
      }},
      {$unwind: '$match'},
      {$project: {
        _id: 0,
        employee: '$match.name',
        project: '$name',
        score: {$reduce: {
          input: '$match.preferences',
          initialValue: 0,
          in: {$cond: [
            {$in: ['$$this.tag', '$tags']},
            {$add: ['$$this.score', '$$value']},
            '$$value'
          ]}
        }}
      }}
    ])
    

    结果。在 MongoDB 版本 4.0.10 上测试。我已经准备了一个包含n 员工和项目的数据库,每个都有 1-7 个偏好/标签。

    n | 10     | 100    | 1000   |
    --|--------|--------|--------|
    1 | 0.004s | 0.070s | 4.061s |
    2 | 0.004s | 0.069s | 4.022s |
    3 | 0.002s | 0.051s | 3.983s |
    4 | 0.002s | 0.060s | 4.225s |
    

    而且,如果我们改变规模,员工人数将是项目的 10 倍 (n)...

    n | 10     | 100    | 500    |
    --|--------|--------|--------|
    1 | 0.038s | 0.674s | 19.42s |
    2 | 0.036s | 0.672s | 17.91s |
    3 | 0.017s | 0.482s | 10.42s |
    4 | 0.018s | 0.497s | 12.13s |
    

    而且,如果我们改变规模,项目数量将是员工数量的 10 倍 (n)...

    n | 10     | 100    | 500    |
    --|--------|--------|--------|
    1 | 0.014s | 0.466s | 16.22s |
    2 | 0.015s | 0.481s | 16.08s |
    3 | 0.012s | 0.476s | 10.30s |
    4 | 0.032s | 0.697s | 13.09s |
    

    如您所见,这一切都取决于。根据您的数据对所有这些进行基准测试,然后选择最佳解决方案。

    【讨论】:

    • 首先感谢您的回答,真的很有帮助。在我的场景中,我假设员工和项目的数量相同,每个员工选择的标签数量大致相同,并且每个项目都有相同的标签。我还有一个问题:您是如何索引employee.preferences.tag 的?只使用:db.projects.createIndex({'tags' : 1})?抱歉,这个问题听起来可能很愚蠢:每次我在集合上进行新插入时,我应该调用这个函数吗?
    • 只需执行db.employees.createIndex({'preferences.tag': 1}) (createIndex docs) - 它将创建一个multikey index。不,您不必每次都运行它。如果您不熟悉索引,请在我链接的这两页中阅读更多内容。
    • 我现在正在测试您的解决方案,似乎具有 reduce 功能的解决方案与其他解决方案相比实际上是闪电般的(即使在 10,000 名员工和 10,000 名员工中执行不到 0.01 秒)项目)。这让我觉得我以某种方式误解了您对 employees.preferences.tag 唯一性的警告。你能更好地解释一下你的意思吗?
    • 如果这些值不是唯一的,它可能返回无效结果,因为它会计算每个偏好时间每个项目。如果有人拥有[{tag: 1, score: 1}, {tag: 1, score: 1}],它将与[{tag: 1, score: 2}] 相同。
    猜你喜欢
    • 2017-10-16
    • 1970-01-01
    • 1970-01-01
    • 2019-01-14
    • 2016-03-20
    • 2022-01-27
    • 1970-01-01
    • 2021-12-21
    • 2018-07-15
    相关资源
    最近更新 更多