Улучшение агрегирования монго для построения двудольного графа - PullRequest
1 голос
/ 23 июня 2019

У меня есть две коллекции, содержащие сотрудников и проекты. Каждый проект имеет массив тегов, и у каждого сотрудника есть массив предпочтений, где каждое предпочтение представляет собой объект с атрибутами тега и оценки (предпочтения сотрудника имеют оценку, основанную на порядке их выбора).

Пример сотрудников:

[{
    "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 для каждой коллекции, и сравниваю его с простым итеративным подходом, использующим вложенные циклы, и в результате этого итерационный подход на самом деле быстрее и легче (выполняется за меньшее время, используя меньше памяти). Я думал, что агрегация станет лучше, увеличив число документов, но, похоже, нет. У вас есть предложения по улучшению трубы? Есть идеи в целом? Заранее спасибо:)

1 Ответ

1 голос
/ 23 июня 2019

Перво-наперво: убедитесь, что ваша $lookup цель foreignField проиндексирована.Затем начните с описания вашей характерной для данных характеристики: часто ли у одного пользователя много предпочтений?Или для проекта иметь много тегов?Диспропорция в размерах employees и projects также повлияет на производительность.

Теперь перейдем к экспериментам.

  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'}
  }}
])
Избавление от одной $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'}
  }}
])
Избавление от обеих $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'
      ]}
    }}
  }}
])
Точно так же, как 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 |

Как видите,Все это зависит.Оцените все это на своих данных и выберите лучшее решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...