计算键的值的出现次数

时间:2017-07-21 02:46:20

标签: javascript mongodb mapreduce mongodb-query aggregation-framework

我有很多具有许多属性的文档。在特定的$match通过后,我最终得到了一个小节。这里简化了:

[
    {"name": "foo", "code": "bbb"},
    {"name": "foo", "code": "aaa"},
    {"name": "foo", "code": "aaa"},
    {"name": "foo", "code": "aaa"},
    {"name": "bar", "code": "aaa"},
    {"name": "bar", "code": "aaa"},
    {"name": "bar", "code": "aaa"},
    {"name": "baz", "code": "aaa"},
    {"name": "baz", "code": "aaa"}
]

我想计算某些属性的出现,所以我最终得到了这个(简化):

{
    "name": {
        "foo": 4, 
        "bar": 3,
        "baz": 2
    },
    "code": {
        "bbb": 1,
        "aaa": 8
    }
}

(或者接下来我可以用Node.js“翻译”)

我已经做了$group阶段来计算其他属性(不同)。理想情况下,我会$addToSet并计算将相似值添加到集合中的次数。但我无法弄清楚如何。

或者我想$push最终得到这个(简化):

{
    "name": ["foo", "foo", "foo", "foo", "bar", "bar", "bar", "baz", "baz"],
    "code": ["bbb", "aaa", "aaa", "aaa", "aaa", "aaa", "aaa", "aaa", "aaa", ]
}

但我无法弄清楚如何将其转化为(接近)上述假设结果。

对于单个字段,我最接近的是使用上面的$push然后我可以使用$group

"$group": {
    "_id": {"_id": "$_id", "name": "$name"},
    "nameCount": {"$sum": 1}
}

现在我有_id.namenameCount。但是我丢失了之前计算的所有属性,大约20个。

有没有办法(接近)我想要的东西?

  

注意:使用MongoDB 3.2

1 个答案:

答案 0 :(得分:1)

对于MongoDB 3.2,如果要在返回的文档中将“数据”值作为“键”返回,则几乎只限于mapReduce。然而,有一种情况需要考虑你实际上“不需要”MongoDB为你做那部分。但要考虑这些方法:

Map Reduce

db.stuff.mapReduce(
  function() { 
    emit(null, {
     name: { [this.name]: 1 },
     code: { [this.code]: 1 }
    })
   },
  function(key,values) {
     let obj = { name: {}, code: {} };
     values.forEach(value => {
       ['name','code'].forEach(key => {
         Object.keys(value[key]).forEach(k => {
           if (!obj[key].hasOwnProperty(k))
             obj[key][k] = 0;
           obj[key][k] += value[key][k];
         })    
       })
     });
     return obj;    
  },
  { "out": { "inline": 1 } }
)

返回:

    {
        "_id" : null,
        "value" : {
            "name" : {
                "foo" : 4.0,
                "bar" : 3.0,
                "baz" : 2.0
            },
            "code" : {
                "bbb" : 1.0,
                "aaa" : 8.0
            }
        }
    }

聚合

对于MongoDB 3.4及更高版本,您可以使用$arrayToObject重塑为“键/值”对象。并且比仅使用$push制作两个大型数组更有效率,这几个数组几乎肯定会在现实世界中破坏BSON限制。

这种“或多或少”反映了mapReduce()操作:

db.stuff.aggregate([
  { "$project": {
    "_id": 0,
    "data": [
      { "k": "name", "v": { "k": "$name", "count": 1 } },
      { "k": "code", "v": { "k": "$code", "count": 1 } }
    ]
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": { "k": "$data.k",  "v": "$data.v.k" },
    "count": { "$sum": "$data.v.count" }
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": { "$push": { "k": "$_id.v", "v": "$count" } }
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }  
  }},
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": {
        "$map": {
          "input": "$data",
          "in": { 
            "k": "$$this.k",
            "v": { "$arrayToObject": "$$this.v" }
          }
        }    
      }
    }  
  }}
])

具有相似的输出(不通过应用$sort强制键排序):

{
    "code" : {
        "bbb" : 1.0,
        "aaa" : 8.0
    },
    "name" : {
        "baz" : 2.0,
        "foo" : 4.0,
        "bar" : 3.0
    }
}

所以它只是在我们实际使用新功能的最后阶段,到目前为止的输出非常相似,并且很容易在代码中重塑:

{
    "_id" : null,
    "data" : [ 
        {
            "k" : "code",
            "v" : [ 
                {
                    "k" : "bbb",
                    "v" : 1.0
                }, 
                {
                    "k" : "aaa",
                    "v" : 8.0
                }
            ]
        }, 
        {
            "k" : "name",
            "v" : [ 
                {
                    "k" : "baz",
                    "v" : 2.0
                }, 
                {
                    "k" : "foo",
                    "v" : 4.0
                }, 
                {
                    "k" : "bar",
                    "v" : 3.0
                }
            ]
        }
    ]
}

所以事实上我们可以做到这一点:

db.stuff.aggregate([
  { "$project": {
    "_id": 0,
    "data": [
      { "k": "name", "v": { "k": "$name", "count": 1 } },
      { "k": "code", "v": { "k": "$code", "count": 1 } }
    ]
  }},
  { "$unwind": "$data" },
  { "$group": {
    "_id": { "k": "$data.k",  "v": "$data.v.k" },
    "count": { "$sum": "$data.v.count" }
  }},
  { "$group": {
    "_id": "$_id.k",
    "v": { "$push": { "k": "$_id.v", "v": "$count" } }
  }},
  { "$group": {
    "_id": null,
    "data": { "$push": { "k": "$_id", "v": "$v" } }  
  }},
  /*
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": {
        "$map": {
          "input": "$data",
          "in": { 
            "k": "$$this.k",
            "v": { "$arrayToObject": "$$this.v" }
          }
        }    
      }
    }  
  }}
  */
]).map( doc =>
  doc.data.map( d => ({
     k: d.k,
     v: d.v.reduce((acc,curr) => 
      Object.assign(acc,{ [curr.k]: curr.v })
      ,{}
     )
  })).reduce((acc,curr) => 
    Object.assign(acc,{ [curr.k]: curr.v })
    ,{}
  )
)

这只是因为聚合框架没有在早期版本的输出中使用“命名键”的功能,你通常不需要它们。由于我们实际使用新功能的唯一地方是“最终”阶段,但我们可以通过简单地重塑客户端代码中的最终输出来轻松完成相同的工作。

当然,结果也是如此:

[
    {
        "code" : {
            "bbb" : 1.0,
            "aaa" : 8.0
        },
        "name" : {
            "baz" : 2.0,
            "foo" : 4.0,
            "bar" : 3.0
        }
    }
]

因此,了解您实际需要的确切“在哪里”应用此类转换是有帮助的。这是在“结束”,因为我们在任何“聚合”阶段都不需要它,因此您只需重塑从聚合框架本身可以最佳提供的结果。

糟糕的方式

如上所述,到目前为止您的尝试对于小数据可能没什么问题,但在大多数现实情况下,将一个集合中的所有项目“推”到一个文档而不减少将会打破16MB BSON限制。

它实际上会留在哪里,那么你可以使用$reduce这个怪物:

db.stuff.aggregate([
  { "$group": {
    "_id": null,
    "name": { "$push": "$name" },
    "code": { "$push": "$code" }
  }},
  { "$replaceRoot": {
    "newRoot": { 
      "$arrayToObject": {
        "$map": {
          "input": [
            { "k": "name", "v": "$name" },
            { "k": "code", "v": "$code" }
          ],
          "as": "m",
          "in": {
            "k": "$$m.k",
            "v": {
              "$arrayToObject": {
                "$reduce": {
                  "input": "$$m.v",
                  "initialValue": [],
                  "in": {
                    "$cond": {
                      "if": { 
                        "$in": [
                          "$$this",
                          { "$map": {
                            "input": "$$value",
                            "as": "v",
                            "in": "$$v.k"
                          }}
                        ]
                      },
                      "then": {
                        "$concatArrays": [
                          { "$filter": {
                            "input": "$$value",
                            "as": "v",
                            "cond": { "$ne": [ "$$v.k", "$$this" ] }
                          }},
                          [{
                            "k": "$$this",
                            "v": {
                              "$sum": [
                                { "$arrayElemAt": [
                                  "$$value.v",
                                  { "$indexOfArray": [ "$$value.k", "$$this" ] }
                                ]},
                                1
                              ]
                            }    
                          }]
                        ]    
                      },
                      "else": {
                        "$concatArrays": [
                          "$$value",
                          [{ "k": "$$this", "v": 1 }]
                        ]    
                      }
                    } 
                  }
                }
              }
            }
          }
        }
      }
    }
  }}
])

产生:

{
    "name" : {
        "foo" : 4.0,
        "bar" : 3.0,
        "baz" : 2.0
    },
    "code" : {
        "bbb" : 1.0,
        "aaa" : 8.0
    }
}

或者客户端代码中的确实相同的缩减过程:

db.stuff.aggregate([
  { "$group": {
    "_id": null,
    "name": { "$push": "$name" },
    "code": { "$push": "$code" }
  }},
]).map( doc => 
  ["name","code"].reduce((acc,curr) =>
    Object.assign(
      acc,
      { [curr]: doc[curr].reduce((acc,curr) =>
          Object.assign(acc,
            (acc.hasOwnProperty(curr))
              ? { [curr]: acc[curr] += 1 }
              : { [curr]: 1 }
          ),{}
        )
      }
    ),
    {}
  )
)

这又有同样的结果:

{
    "name" : {
        "foo" : 4.0,
        "bar" : 3.0,
        "baz" : 2.0
    },
    "code" : {
        "bbb" : 1.0,
        "aaa" : 8.0
    }
}