Как использовать агрегацию MongoDB для операций общего назначения (объединение, пересечение, разность)

Я столкнулся с некоторыми целенаправленными реализациями заданных операций, но ничего для общего случая. Каков общий случай выполнения операций установки (в частности, пересечение, объединение, симметричная разность). Это проще понять с помощью javascript в $where или map reduce, но я хочу знать, как это сделать в агрегации, чтобы получить собственную производительность.

Лучшим способом проиллюстрировать этот вопрос является пример. Скажем, у меня есть запись с двумя массивами/наборами:

db.colors.insert({
    _id: 1,
    left : ['red', 'green'],
    right : ['green', 'blue']
});

Я хочу найти объединение, пересечение и различие в "левых" и "правильных" массивах. Еще лучше, наглядно я хочу найти:

Союз → ['красный', 'зеленый', 'синий']

union

Пересечение → ['green']

enter image description here

Симметричная разница → ['red', 'blue']

enter image description here

Ответ 1

Версия 2.6+ Только:

Начиная с версии 2.6 MongoDB, это стало намного проще. Вы можете сделать следующее для решения этой проблемы:

Union

db.colors.aggregate([
    {'$project': {  
                    union:{$setUnion:["$left","$right"]}
                 }
    }
]);

Пересечение

db.colors.aggregate([
    {'$project': {  
                  int:{$setIntersection:["$left","$right"]}
                 }
    }
]);

Относительное дополнение

db.colors.aggregate([
    {'$project': {  
                    diff:{$setDifference:["$left","$right"]}
                 }
    }
]);

Симметричная разница

db.colors.aggregate([
    {'$project': {  
                    diff:{$setUnion:[{$setDifference:["$left","$right"]}, {$setDifference:["$right","$left"]}]}
                 }
    }
]);

Примечание: существует билет, требующий симметричной разницы, добавляемый как основная функция, а не необходимость объединения двух заданных различий.

Ответ 2

Простейшим из этих трех, использующих агрегацию, является пересечение **. Общий случай для этого можно сделать, используя агрегацию следующим образом:

Пересечения:

db.colors.aggregate([
    {'$unwind' : "$left"},
    {'$unwind' : "$right"},
    {'$project': {  
                    value:"$left", 
                    same:{$cond:[{$eq:["$left","$right"]}, 1, 0]}
                 }
    },
    {'$group'  : { 
                    _id: {id:'$_id', val:'$value'}, 
                    doesMatch:{$max:"$same"}
                 }
    },
    {'$match'   :{doesMatch:1}},
]);

Остальные два становятся немного более сложными. Насколько я знаю, нет единого способа объединить два отдельных поля в одном документе. Было бы неплохо иметь $add, $comb или $addToSet в фазе проекта $project, но этого не существует. Поэтому лучшее, что мы можем сделать, это сказать, что что-то пересекло или нет. Мы можем начать обе скопления со следующим:

db.colors.aggregate([
    {'$unwind' : "$left"},
    {'$unwind' : "$right"},
    {'$project': {  
                    left:"$left",
                    right:'$right',
                    same:{$cond:[{$eq:["$left","$right"]}, 1, 0]}
                 }
    },
    {'$group'  : {
                    _id:{id:'$_id', left:'$left'},
                    right:{'$addToSet':'$right'},
                    sum: {'$sum':'$same'},
                 }
    },
    {'$project': {  
                    left:{val:"$_id.left",inter:"$sum"},
                    right:'$right',
                 }
    },
    {'$unwind' : "$right"},
    {'$project': {  
                    left:"$left",
                    right:'$right',
                    same:{$cond:[{$eq:["$left.val","$right"]}, 1, 0]}
                 }
    },
    {'$group'  : {
                    _id:{id:'$_id.id', right:'$right'},
                    left:{'$addToSet':'$left'},
                    sum: {'$sum':'$same'},
                 }
    },
    {'$project': {  
                    right:{val:"$_id.right",inter:"$sum"},
                    left:'$left',
                 }
    },
    {'$unwind' : "$left"},
    {'$group'  : {
                    _id:'$_id.id',
                    left:{'$addToSet':'$left'},
                    right: {'$addToSet':'$right'},
                 }
    },
]);

Это агрегирование на примере, предоставленном в вопросе, даст такой результат:

{
        "_id" : 1,
        "left" : [
                {
                        "val" : "green",
                        "inter" : 1
                },
                {
                        "val" : "red",
                        "inter" : 0
                }
        ],
        "right" : [
                {
                        "val" : "blue",
                        "inter" : 0
                },
                {
                        "val" : "green",
                        "inter" : 1
                }
        ]
}

Отсюда мы можем получить пересечение, добавив следующее к агрегации:

{'$project': {  
                    left:"$left"
                 }
    },
    {'$unwind' : "$left"},
    {'$match'  : {'left.inter': 1}},
    {'$group'  : {
                    _id:'$_id',
                    left:{'$addToSet':'$left'},
                 }
    },

Мы можем найти разницу, а также относительное дополнение, добавив следующее к концу базовой агрегации:

enter image description here

{'$unwind' : "$left"},
    {'$match'  : {'left.inter': 0}},
    {'$unwind' : "$right"},
    {'$match'  : {'right.inter': 0}},
    {'$group'  : {
                    _id:'$_id',
                    left:{'$addToSet':'$left'},
                    right:{'$addToSet':'$right'},
                 }
    },

К сожалению, похоже, что нет хорошего способа объединить разнородные элементы из разных полей. Чтобы получить союз, лучше всего сделать это от клиента. Или, если вы хотите фильтровать, делайте это по каждому набору отдельно.