使用 mongo 计算所有文档中的数组出现次数

Posted

技术标签:

【中文标题】使用 mongo 计算所有文档中的数组出现次数【英文标题】:count array occurrences across all documents with mongo 【发布时间】:2015-12-04 13:03:33 【问题描述】:

我试图从一组文档中提取数据,如下所示:

[
  
    name: 'john',
    sex: 'male',
    hobbies: ['football', 'tennis', 'swimming']
  ,
  
    name: 'betty'
    sex: 'female',
    hobbies: ['football', 'tennis']
  ,
  
    name: 'frank'
    sex: 'male',
    hobbies: ['football', 'tennis']
   
]

我正在尝试使用聚合框架来呈现数据,按性别划分,计算最常见的爱好。结果应该类似于。

 _id: 'male', 
  total: 2, 
  hobbies: 
    football: 2,
    tennis: 2,
    swimming: 1
   
,
 _id: 'female', 
  total: 1, 
    hobbies: 
      football: 1,
      tennis: 1
     

到目前为止,我可以获得每种性别的总数,但我不确定如何使用 unwind 来获得爱好数组的总数。

到目前为止我的代码:

collection.aggregate([
         
            $group:  
                _id: '$sex', 
                total:  $sum: 1 
            
        
    ])

【问题讨论】:

【参考方案1】:

就我个人而言,我不太喜欢将“数据”转换为结果中的键名。聚合框架原则倾向于同意,因为也不支持这种操作。

所以个人偏好是将“数据”作为“数据”来维护,并接受处理后的输出实际上更好,更符合一致的对象设计:

db.people.aggregate([
     "$group": 
        "_id": "$sex",
        "hobbies":  "$push": "$hobbies" ,
        "total":  "$sum": 1 
    ,
     "$unwind": "$hobbies" ,
     "$unwind": "$hobbies" ,
     "$group": 
        "_id": 
            "sex": "$_id",
            "hobby": "$hobbies"
        ,
        "total":  "$first": "$total" ,
        "hobbyCount":  "$sum": 1 
    ,
     "$group": 
        "_id": "$_id.sex",
        "total":  "$first": "$total" ,
        "hobbies": 
            "$push":  "name": "$_id.hobby", "count": "$hobbyCount" 
        
    
])

产生这样的结果:

[
    
            "_id" : "female",
            "total" : 1,
            "hobbies" : [
                
                    "name" : "tennis",
                    "count" : 1
                ,
                
                    "name" : "football",
                    "count" : 1
                
            ]
    ,
    
        "_id" : "male",
        "total" : 2,
        "hobbies" : [
            
                "name" : "swimming",
                "count" : 1
            ,
            
                "name" : "tennis",
                "count" : 2
            ,
            
                "name" : "football",
                "count" : 2
            
        ]
    
]

所以最初的$group 对每个“性别”进行计数,并将爱好堆叠成一个数组数组。然后对 $unwind 进行两次反规范化以获得单数项,$group 获得每个性别下每个爱好的总数,最后为每个性别重新组合一个数组。

它是相同的数据,具有易于处理的一致且有机的结构,MongoDB 和聚合框架非常高兴地产生了这种输出。

如果您确实必须将数据转换为键名(我仍然建议您不要这样做,因为这不是一个好的设计模式),那么从最终状态进行这样的转换对于客户端代码来说是相当微不足道的加工。作为一个适用于 shell 的基本 javascript 示例:

var out = db.people.aggregate([
     "$group": 
        "_id": "$sex",
        "hobbies":  "$push": "$hobbies" ,
        "total":  "$sum": 1 
    ,
     "$unwind": "$hobbies" ,
     "$unwind": "$hobbies" ,
     "$group": 
        "_id": 
            "sex": "$_id",
            "hobby": "$hobbies"
        ,
        "total":  "$first": "$total" ,
        "hobbyCount":  "$sum": 1 
    ,
     "$group": 
        "_id": "$_id.sex",
        "total":  "$first": "$total" ,
        "hobbies": 
            "$push":  "name": "$_id.hobby", "count": "$hobbyCount" 
        
    
]).toArray();

out.forEach(function(doc) 
    var obj = ;
    doc.hobbies.sort(function(a,b)  return a.count < b.count );
    doc.hobbies.forEach(function(hobby) 
        obj[hobby.name] = hobby.count;
    );
    doc.hobbies = obj;
    printjson(doc);
);

然后您基本上将每个游标结果处理为所需的输出形式,这实际上并不是服务器上真正需要的聚合函数:


    "_id" : "female",
    "total" : 1,
    "hobbies" : 
        "tennis" : 1,
        "football" : 1
    


    "_id" : "male",
    "total" : 2,
    "hobbies" : 
        "tennis" : 2,
        "football" : 2,
        "swimming" : 1
    

将这种操作实现到游标结果的流处理以根据需要进行转换也应该是相当简单的,因为它基本上只是相同的逻辑。

另一方面,您始终可以使用 mapReduce 在服务器上实现所有操作:

db.people.mapReduce(
    function() 
        emit(
            this.sex,
             
                "total": 1,
                "hobbies": this.hobbies.map(function(key) 
                    return  "name": key, "count": 1 ;
                )
            
        );
    ,
    function(key,values) 
        var obj  = ,
            reduced = 
                "total": 0,
                "hobbies": []
            ;

        values.forEach(function(value) 
            reduced.total += value.total;
            value.hobbies.forEach(function(hobby) 
                if ( !obj.hasOwnProperty(hobby.name) )
                    obj[hobby.name] = 0;
                obj[hobby.name] += hobby.count;
            );
        );

        reduced.hobbies = Object.keys(obj).map(function(key) 
            return  "name": key, "count": obj[key] ;
        ).sort(function(a,b) 
            return a.count < b.count;
        );

        return reduced;
    ,
     
        "out":  "inline": 1 ,
        "finalize": function(key,value) 
            var obj = ;
            value.hobbies.forEach(function(hobby) 
                obj[hobby.name] = hobby.count;
            );
            value.hobbies = obj;
            return value;
        
    
)

mapReduce 有它自己独特的输出风格,但在累积和操作中使用相同的原则,即使不像聚合框架那样高效:

   "results" : [
        
            "_id" : "female",
            "value" : 
                "total" : 1,
                "hobbies" : 
                    "football" : 1,
                    "tennis" : 1
                
            
        ,
        
            "_id" : "male",
            "value" : 
                "total" : 2,
                "hobbies" : 
                    "football" : 2,
                    "tennis" : 2,
                    "swimming" : 1
                
            
        
    ]

归根结底,我仍然说第一种处理形式是最有效的,并且在我看来,它提供了最自然和最一致的数据输出工作,甚至没有尝试将数据点转换为名称的钥匙。最好考虑遵循该模式,但如果您真的必须这样做,那么可以通过各种处理方法将结果操作为所需的形式。

【讨论】:

只是想对您的回答说“谢谢”...虽然我没有问这个问题:) 我帮了我很多!

以上是关于使用 mongo 计算所有文档中的数组出现次数的主要内容,如果未能解决你的问题,请参考以下文章

如何使用字典理解计算文档中每个单词的出现次数

怎样求数组中元素重复的次数

python计算数组中出现的所有元素并统计出现次数

用随机数计算数组中出现的次数,没有方法或 C# 中的列表

c_cpp 计算已排序数组中的出现次数

c_cpp 计算已排序数组中的出现次数(或频率)