$ project MongoDB Aggregation中的不同项目

时间:2014-01-08 08:00:51

标签: mongodb aggregation-framework

我在mongodb中有一个非常大的项目集合,其架构是我无法改变的。简化版本如下所示:

{event: { address: {ip: "1.1.1.1", port: 80}}}
{event: { address: {ip: "1.1.1.2", port: 80}}}
{event: { address: [{ip: "1.1.1.1", port: 80}, {ip: "1.1.1.1", port: 443}]}}
{event: { address: [{ip: "1.1.1.1", port: 8080}, {ip: "1.1.1.2", port: 443}]}}

每个事件可能有一个或多个地址。每个地址都有“ip”和“port”。因此,在具有多个地址的事件中可能会重复“ip”。

我想要做的就是计算每个IP地址的事件数量并找到最高IP地址。对于上面的示例,首选结果是:

[ { "ip" : "1.1.1.1", "count" : 3 },
  { "ip" : "1.1.1.2", "count" : 2 } ]

我想到的一个问题是:

db.collection.aggregate({$project: {ip: "$event.address.ip"}}, {$group: {_id: "$ip", count: {$sum: 1}}}, {$sort: {count: -1}}, {$limit: 5})

但结果是:

{
 "result" : [
  { "_id" : ["1.1.1.1", "1.1.1.2"], "count" : 1 },
  { "_id" : ["1.1.1.1", "1.1.1.1"], "count" : 1 },
  { "_id" : "1.1.1.2", "count" : 1 },
  { "_id" : "1.1.1.1", "count" : 1 } ],
 "ok" : 1
}

我无法使用$ unwind因为每个事件的每个IP地址只应计数一次,但某些事件具有相同的IP重复次数。另外,$ unwind一般不起作用,因为“地址”并不总是一个数组。有些事件只有一个不是数组的地址,$ unwind会为它们抛出异常。

我在$ group中尝试了不同的聚合运算符,例如$ addToSet,但都无济于事。

该集合非常庞大,我无法首先提取应用程序中的所有IP地址,然后为每个地址计算事件。

可以使用map / reduce完成。你会建议什么?

2 个答案:

答案 0 :(得分:5)

虽然可以使用MapReduce完成,但聚合框架会更快。你需要为你的计划添加两个步骤 - 1)你需要“规范化”格式,以便地址始终是一个数组,2)然后你需要$展开该数组,按_id,ip分组以摆脱重复和然后通过ip分组以获得所需的计数。

规范化数组和非数组很棘手,但可以使用$unwind之前和之后的两个投影来完成。

var p1 = { "$project" : {
        "array" : {
            "$cond" : [
                {
                    "$eq" : [
                        "$address.0",
                        [ ]
                    ]
                },
                "$address",
                [
                    null
                ]
            ]
        },
        "notarray" : {
            "$cond" : [
                {
                    "$ne" : [
                        "$address.0",
                        [ ]
                    ]
                },
                "$address",
                [
                    null
                ]
            ]
        },
        "isArray" : {
            "$eq" : [
                "$address.0.ip",
                [ ]
            ]
        }
    }
};
var u = { "$unwind" : "$array" };
var p2 = { "$project" : {
        "address" : {
            "$cond" : [
                "$isArray",
                "$array",
                "$notarray"
            ]
        }
    }
};

相比之下,两个$group阶段很简单:

var g1 = { "$group" : { "_id" : { "_id" : "$_id", "ip" : "$address.ip" } } };
var g2 = { "$group" : { "_id" : "$_id.ip", "count" : { "$sum" : 1 } } };

以下是我的示例数据:

> db.coll.find()
{ "_id" : ObjectId("52cd0badba17f3b7ed212575"), "address" : { "ip" : "1.1.1.1" } }
{ "_id" : ObjectId("52cd0bc4ba17f3b7ed212576"), "address" : [  {  "ip" : "1.1.1.1" },  {  "ip" : "1.1.1.1" } ] }
{ "_id" : ObjectId("52cd0bc9ba17f3b7ed212577"), "address" : [  {  "ip" : "1.1.1.1" },  {  "ip" : "1.1.1.2" } ] }

这是聚合及其输出:

> db.coll.aggregate(p1, u, p2, g1, g2)
{ "_id" : "1.1.1.1", "count" : 3 }
{ "_id" : "1.1.1.2", "count" : 1 }

答案 1 :(得分:0)

啊,你有两个问题,一个是架构设计得不好,两个问题没有标准化,因此相同的字段至少没有相同的结构。你在岩石和坚硬的地方之间。

如果所有的地址字段都是数组,那么这很容易,但是你不能当前有条件地$unwind,如果你尝试$unwind除了一个数组之外的其他任何东西,那么你很可能会放弃地址然后你会收到错误:

  

如果为$ unwind指定的目标字段不是数组,则db.collection.aggregate()会生成错误。

http://docs.mongodb.org/manual/reference/operator/aggregation/unwind/

所以是的,你被困在这里。

这可以通过MR完成,但分组会很痛苦。我个人会做的是运行增量MR,它以标准化格式写出这个模式,以便可以在其上使用聚合框架。