计算键值的出现次数

Counting Occurrences of Values for Keys

我有很多具有很多属性的文件。在特定的 $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
    }
}

(或者我可以 'translate' 之后用 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个左右

有没有办法做(接近)我想要的事情?

Note: Using MongoDB 3.2

对于 MongoDB 3.2,如果您想 return "data" 值作为 "keys" 在 returned 文档中,您几乎只能使用 mapReduce .但是,在某些情况下,您实际上 "do not need" MongoDB 会为您完成该部分。但要考虑这些方法:

地图缩小

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 } }
)

Returns:

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

汇总

对于 MongoDB 3.4 及更高版本,您可以使用 $arrayToObject 重塑为 "key/value" 对象。并且比简单地使用 $push 来制作两个大数组更有效,这在现实世界中几乎肯定会打破 BSON 限制。

此 "more or less" 反映了 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 })
    ,{}
  )
)

这只是因为聚合框架没有在早期版本的输出中使用 "named keys" 的功能,所以您通常不需要它们。由于我们实际使用新功能的唯一地方是在 "final" 阶段,但我们可以通过简单地在客户端代码中重塑最终输出来轻松地做到这一点。

当然,结果是一样的:

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

因此,吸取教训 "where" 您实际上 需要 应用此类转换。它位于 "end",因为我们在任何 "aggregation" 阶段都不需要它,因此您只需重塑可以从聚合框架本身最佳提供的结果。


坏方法

如前所述,您到目前为止的尝试对于小数据可能没问题,但在大多数现实世界的情况下 "pushing" 将集合中的所有项目不缩减到一个文档中将打破 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
    }
}