聚合或映射减少以创建规范化的“每个供应商的唯一付费用户”

时间:2014-06-19 12:39:56

标签: javascript mongodb mapreduce aggregation-framework

我正在尝试使用Map Reduce或Mongodb中的聚合框架为每个供应商的独立付费用户创建一份报告。唯一的问题是需要对总数进行标准化,因此每个用户在他/她购买的所有供应商中总共贡献了1。例如

{
   "account": "abc",
   "vendor": "amazon",
},
{
   "account": "abc",
   "vendor": "overstock",
},
{
   "account": "ccc",
   "vendor": "overstock",
}

会产生

{
   "vendor": "amazon",
   "total" : 0.5
},
{ 
   "vendor": "overstock",
   "total": 1.5
}

在这里,我们看到用户'abc'进行了两次购买,并且对两家供应商的贡献相同。我们还看到,总结供应商总数将等于我们独特的付费用户。

我通过四个步骤执行此聚合的天真方法。

1. For each user, store number of purchases by vendor in a map.
2. For each user, sum up total purchases and divide each vendor purchases by total.
3. Perform an additive merge of each users normalized purchase map into a final vendor map. 

此方法适用于较小的数据集,但速度较慢,并且在较大的集合上耗尽内存。

使用聚合框架,我已经找到了如何计算总用户数,但是采用了规范化方法。

agg = this.db.aggregate(
[
    {
        $group :
        {
            _id :
            {
                vendor : '$vendor',
                user : '$account'
            },
            total :
            {
                $sum : 1
            }
        }
    }
]);

var transformed = {};
for( var index in agg.result)
{
    var entry = agg.result[index];

    var vendor= entry._id.vendor;
    if(!transformed[vendor])
    {
        transformed[vendor] = 0;
    }
    transformed[vendor] += 1;
}

如何重新构建此查询以规范用户总数?

2 个答案:

答案 0 :(得分:1)

有两种方法可以分别应用于.aggregate().mapReduce()方法,它们的效率当然会相对于数据的总体大小而有所不同。

首先使用聚合,你需要像你所做的那样得到每个“供应商”的总数,但是你需要每个用户的总数来计算你的百分比。所以里程数可能因分组操作考虑我们将要创建和$unwind数组的效率而有所不同:

db.collection.aggregate([
    { "$group": {
        "_id": { "account": "$account", "vendor": "$vendor" },
        "count": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.account",
        "purch": { "$push": { "vendor": "$_id.vendor", "count": "$count" } },
        "total": { "$sum": "$count" },
    }},
    { "$unwind": "$purch" },
    { "$project": {
        "vendor": "$purch.vendor",
        "total": { 
            "$divide": [ "$purch.count", "$total" ]
        }
    }},
    { "$group": {
        "_id": "$vendor",
        "total": { "$sum": "$total" }
    }}
])

mapReduce方法必须分两步运行,首先减少用户对供应商的响应,然后再降低供应商的响应:

db.collection.mapReduce(
    function () {
        emit(
            this.account,
            {
                "data": [{
                    "vendor": this.vendor,
                    "count": 1,
                }],
                "total": 1,
                "seen": false
            }
        );
    },
    function (key,values) {

        var reduced = { data: [], total: 0, seen: true };

        values.forEach(function(value) {
            value.data.forEach(function(data) {
                var index = -1;
                for (var i = 0; i <=reduced.data.length-1; i++) {

                    if ( reduced.data[i].vendor == data.vendor ) {
                        index = i;
                        break;
                    }
                }

                if ( index == -1 ) {
                    reduced.data.push(data);
                } else {
                    if (!value.seen)
                        reduced.data[index].count += data.count;
                }
            });
        });

        reduced.data.map(function(x) {
            reduced.total += x.count;
        });

        return reduced;
    },
    { 
        "out": { "replace": "output" },
        "finalize": function (key,value) {

            var result = {
                data: []
            };

            result.data = value.data.map(function(x) {
                var res = { };
                res["vendor"] = x.vendor;
                res["total"] = x.count / value.total;
                return res;
            });

            return result;
        }
    }
)

关于输出的第二部分:

db.output.mapReduce(
    function () {
        this.value.data.forEach(function(data){
            emit( data.vendor, data.total );
        });
    },
    function(key,values) {
        return Array.sum( values );
    },
    { "out": { "inline": 1 } }
)

所以这取决于您的数据大小。 mapReduce方法速度较慢,需要输出到集合,然后再次运行聚合。

另一方面,聚合框架方法通常应该运行得更快,但是根据每个用户可以获得的供应商数组的大小,它可以减慢速度。

答案 1 :(得分:0)

这是对Neil Lunn的回答。经过一番思考之后,我开始认识到,如果在map reduce中,聚合必须是一个多步骤的过程。我喜欢你的答案,因为它使用map reduce来写入一个需要更大数据集的集合。我还将尝试使用.aggregrate()方法来提高性能。有趣的是,Mongo 2.6中的新聚合框架也具有这种“out”功能。

我最终得到的解决方案如下(适用于我们的数据集)。

1. use aggregation framework to calculate purchases per account.
2. convert this result into a map for fast access
3. perform map reduce on collection making user of the 'scope' field to pass in the account total map we built in step 2. 

代码看起来与此类似。

var agg = this.db.aggregate(
[
    {
        $group :
        {
            _id :
            {
                user : '$account'
            },
            total :
            {
                $sum : 1
            }
        }
    }
]);

var accountMap = {};
for( var index in agg.result)
{
    var entry = agg.result[index];
    addToMap(accountMap, entry._id.user, entry.total);
}

delete agg; // free up memory?

var mapFunction = function()
{
    var key = this.vendor;

    // create normalized total for the vendor based on the users purchases. 
    var value = 1 / accountMap[this.account];

    emit(key, value);
};

var reduceFunction = function(key, values)
{
    return(Array.sum(values));
};

var res = this.db.mapReduce(mapFunction, reduceFunction,
{
    out :
    {
        inline : 1
    },
    scope :
    {
        'accountMap' : accountMap
    }
});

delete accountMap;

var transformed = {};

for( var index in res.results)
{
    transformed[entry._id] = entry.value;
}