我在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完成。你会建议什么?
答案 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,它以标准化格式写出这个模式,以便可以在其上使用聚合框架。