【问题标题】:Counting Occurrences of Values for Keys计算键值的出现次数
【发布时间】:2017-07-21 02:46:19
【问题描述】:

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

【问题讨论】:

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


    【解决方案1】:

    对于 MongoDB 3.2,如果您想在返回的文档中将“数据”值作为“键”返回,则几乎只能使用 mapReduce。但是,有时需要考虑您实际上“不需要” 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 } }
    )
    

    返回:

        {
            "_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
        }
    }
    

    【讨论】:

    • 您的回答非常宝贵,我会仔细研究。请不要删除它的任何部分。但是,由于我的人为限制,我忘了提到初始文档集是使用$match 获得的,并且需要计算生成的集合。我猜 Map Reduce 是这样的。我也受到使用 MongoDB 3.2 的 SLA 限制的限制,否则我会对 $arrayToObject$replaceRoot 大加赞赏。
    • 你说得对,我的尝试很糟糕。第一个 $match 阶段根据用户输入即时发生,并返回 1 到 50000 个文档之间的任何集合。我正在做类似于您最底层的“坏”示例的事情。它适用于测试数据,但我知道这对于最终产品来说是不可持续的。
    • 我喜欢你如何注释掉 $replaceRoot 以进行说明。我将更多地研究您答案的这一部分,看看它是否有效。因为最初的$group 已经包含了一些$sum 计数,但是如果我需要从$project 开始,我就不能这样做。我必须想办法一次性完成这两件事。最后使用$first 重复$group 中的所有属性。
    • @Redsandro 好吧,较大的主体本质上说您实际上根本不需要新功能,因为结果的“转换”部分总是可以在实际聚合部分完成“之后”完成.还有mapReduce does have a "query" option,与$match基本相同。但从更广泛的角度来看,这个答案解决了“你问的问题”,因此对“问题范围之外”的其他数据的任何考虑都不是这个答案的关注点。
    • @Redsandro 当然,当您发现有用的答案时,正确的做法是"accept the answer and vote for it's usefulness as well"
    猜你喜欢
    • 2012-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多