【问题标题】:Counting Distinct Values Per Key from Inside an Array从数组内部计算每个键的不同值
【发布时间】:2017-11-21 02:16:55
【问题描述】:

我有一组反馈评级(沟通、及时性和交付),所有这些都可以包含 1-5 个值。我需要计算有多少人在每个评分中评分为 5 星,然后是 4 星,然后是 3 星,最后是 1 星。

这是我的用户架构

var UserSchema = new mongoose.Schema({
    username: String,
    fullname: String,
    email: {
        type: String,
        lowercase: true,
        unique: true
    },
  password: String,
  feedback: [{
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Feedback'    
  }]
});

反馈架构

var FeedbackSchema = new mongoose.Schema({
    postname: String,
    userWhoSentFeedback: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User'
    },
    message: String,
    feedbacktype: String,
    thumbsup: Boolean,
    rating: {
        delivery: Number,
        timeliness: Number,
        communication: Number
    }
});

到目前为止,我正在使用 $match、$unwind、$lookup 和 $count 尝试获取值但失败了。这是我正在运行的代码...

router.get('/testagg', (req, res)=>{
    User.aggregate([
        {"$match": 
            { "username": "user1"}
        },
        {
            "$lookup": {
            "from": "feedbacks",
            "localField": "feedback",
            "foreignField": "_id",
            "as": "outputResult"
            }
        }, {"$unwind": "$outputResult"},
        {"$match": {"outputResult.rating.communication": {$eq: 1}}},{"$count": 'Communication_1'},
        {
            "$project": {
                outputResult: 1,
                Communication_1: 1
            }
        }
    ], (err, user)=>{
        console.log(user)
        res.json(user);
    })
})

使用这段代码,这是我得到的结果

[
  {
    "Communication_1": 1
  }
]

所以我尝试通过实现多个 $match 来获取 Communication 的所有评级数字(这不起作用)

router.get('/testagg', (req, res)=>{
    User.aggregate([
        {"$match": 
            { "username": "user1"}
        },
        {
            "$lookup": {
            "from": "feedbacks",
            "localField": "feedback",
            "foreignField": "_id",
            "as": "outputResult"
            }
        }, {"$unwind": "$outputResult"},
        {"$match": {"outputResult.rating.communication": {$eq: 1}}},{"$count": 'Communication_1'},
        {"$match": {"outputResult.rating.communication": {$eq: 2}}},{"$count": 'Communication_2'},
        {"$match": {"outputResult.rating.communication": {$eq: 3}}},{"$count": 'Communication_3'},
        {"$match": {"outputResult.rating.communication": {$eq: 4}}},{"$count": 'Communication_4'},
        {"$match": {"outputResult.rating.communication": {$eq: 5}}},{"$count": 'Communication_5'},
        {
            "$project": {
                outputResult: 1,
                Communication_1: 1,
                Communication_2: 1,
                Communication_3: 1,
                Communication_4: 1,
                Communication_5: 1
            }
        }
    ], (err, user)=>{
        console.log(user)
        res.json(user);
    })
})

但我得到了一个空洞的回应。所以我觉得我做错了。

任何帮助将不胜感激!谢谢!

**更新

我也试过这段代码。只是为了得到1和4的通信值而已。

router.get('/testagg', (req, res)=>{
    User.aggregate([
        {"$match": 
            { "username": "user1"}
        },
        {
            "$lookup": {
            "from": "feedbacks",
            "localField": "feedback",
            "foreignField": "_id",
            "as": "outputResult"
            }
        }, {"$unwind": "$outputResult"}, {
            "$project": {
                "outputResult.rating": 1,
                comm1: { $cond: [{$eq: ['$outputResult.rating.communication', 1]}, 1, 0]},
                comm4: { $cond: [{$eq: ['$outputResult.rating.communication', 4]}, 1, 0]}
            }
        },
         { $group: {
            _id: '$outputResult.rating',
            total: { $sum: 1 },
            comm1: { $sum: '$comm1'},
            comm4: { $sum: '$comm4'}
            }
        }
    ], (err, user)=>{
        console.log(user)
        res.json(user);
    })
})

这就是我得到的结果

[
  {
    "_id": {
      "communication": 1,
      "timeliness": 1,
      "delivery": 1
    },
    "total": 1,
    "comm1": 1,
    "comm4": 0
  },
  {
    "_id": {
      "communication": 5,
      "timeliness": 5,
      "delivery": 5
    },
    "total": 1,
    "comm1": 0,
    "comm4": 0
  },
  {
    "_id": {
      "communication": 4,
      "timeliness": 4,
      "delivery": 5
    },
    "total": 1,
    "comm1": 0,
    "comm4": 1
  }
]

算了,但这不是我想要的,我想要的是对每个评分进行总计数

这是我想要的输出

{
"comm1" : 1,
"comm2" : 0,
"comm3" : 0,
"comm4" : 1,
"comm5" : 1
}

【问题讨论】:

    标签: javascript node.js mongodb mongoose aggregation-framework


    【解决方案1】:

    这里更大的问题可能是您实际上是在跨文档进行聚合吗?还是您实际上只是希望“单个用户”使用此功能?因为这对于您“应该”如何处理这个问题确实有所不同。但是,让我们在您可以做些什么来获得结果的背景下解决这个问题。

    老实说,我认为您对此的看法是错误的。如果您要为多个键(“通信”、“交付”、“及时性”)获得多个评级,那么尝试为每个键设置一个“命名键”既麻烦又笨拙。事实上,恕我直言,这样的输出是彻头彻尾的“混乱”。

    在我看来,您最好对“列表”使用自然结构,这是一个“数组”,您可以在以后的代码中轻松地对其进行迭代和访问。

    为此,我建议您改为寻找包含每个“键”的结果,然后包含它自己的数组,用于您实际追求的“分数”及其“不同计数”。这是一种更清洁且机器可读的方法,因为替代方法在以后的代码中意味着“迭代键”,这真的很“混乱”。

    在您实际“聚合”文档的位置聚合

    考虑到您实际上是在跨文档“聚合”,因此要将其传递给聚合框架,那么您应该执行以下操作:

    User.aggregate(
      [
        { "$match": { "username": "user1" }
        { "$lookup": {
          "from": "feedbacks",
          "localField": "feedback",
          "foreignField": "_id",
          "as": "feedback"
        }},
        { "$unwind": "$feedback" },
        { "$project": {
          "username": 1,
          "feedback": [
            { "k": "delivery", "v": "$feedback.rating.delivery" }, 
            { "k": "timeliness", "v": "$feedback.rating.timeliness" },
            { "k": "communication", "v": "$feedback.rating.communication" }
          ]
        }},
        { "$unwind": "$feedback" },
        { "$group": {
           "_id": {
             "username": "$username",
             "type": "$feedback.k",
             "score": "$feedback.v",
           },
           "count": { "$sum": 1 }
        }},
        { "$group": {
          "_id": { 
            "username": "$_id.username",
            "type": "$_id.type"
          },
          "scores": { 
            "$push": { 
              "score": "$_id.score",
              "count": "$count"
            }
          }
        }},
        { "$group": {
          "_id": "$_id.username",
          "feedback": {
            "$push": {
              "type": "$_id.type",
              "scores": "$scores"
            }
          }
        }}
      ],
      function(err,users) {
    
      }
    )
    

    这里的基本过程是在$lookup“加入”之后,你$unwind得到结果数组,因为无论如何你都想从那个“跨文档”中聚合细节。我们要做的下一件事实际上是使您前面提到的“键”成员也成为一个数组。这是因为为了进一步处理这个问题,我们还将 $unwind 该内容为每个“反馈”成员和“每个密钥”生成有效的新文档。

    接下来的阶段都是用$group完成的,依次是:

    1. 根据提供的“用户名”的形成方式,对每个分数的“不同”反馈键进行分组和计数。

    2. 按“键”分组以按“用户名”分隔文档,将“分数”及其不同的计数添加到数组中。

    3. 仅对“用户名”进行分组,还可以为每个“键”创建一个包含“分数”数组的数组条目。


    当您实际上没有“聚合”时处理光标

    对此的替代方法是考虑到您只要求“单个用户名”,并且无论如何这些数据都是在“单个文档”中获得的。因此,您真正想要“在服务器上”做的唯一事情就是 $lookup 来执行“加入”。之后,使用您的客户端代码处理“反馈”数组会变得更加简单,从而产生完全相同的不同结果。

    只需使用$lookup 进行连接,然后处理结果:

    User.aggregate(
      [
        { "$match": { "username": "user1" }
        { "$lookup": {
          "from": "feedbacks",
          "localField": "feedback",
          "foreignField": "_id",
          "as": "feedback"
        }},
      ],
      function(err,users) {
        users = users.map(doc => {
          doc.feedback = [].concat.apply([],doc.feedback.map(
            r => Object.keys(r.rating).map(k => ({ k: k, v: r.rating[k] }) )
          )).reduce((a,b) => {
            if ( a.findIndex(e => JSON.stringify({ k: e.k , v: e.v }) == JSON.stringify(b) ) != -1 ) {
              a[a.findIndex(e => JSON.stringify({ k: e.k , v: e.v }) == JSON.stringify(b) )].count += 1;
            } else {
             a = a.concat([{ k: b.k, v: b.v, count: 1 }]);
            }
            return a;
          },[]).reduce((a,b) => {
            if ( a.findIndex(e => e.type == b.k) != -1 ) {
              a[a.findIndex(e => e.type == b.k)].scores.push({ score: b.v, count: b.count })
            } else {
              a = a.concat([{ type: b.k, scores: [{ score: b.v, count: b.count }] }]);
            }
            return a;
          },[]);
          return doc;
       });
    
       res.json(users)       
      }
    )
    

    事实上,如果它是一个“单一”用户,那么你可能不会在最后使用res.json(users[0]),因为.aggregate() 的结果始终是一个数组,无论返回多少结果。

    这实际上只是在 "feedback" 数组上使用 .map().reduce() JavaScript 函数,以重塑并返回“不同的计数”。应用了相同类型的方法,但如果这确实是单个文档响应,甚至是不需要“跨文档聚合”的小型响应,那么它是更清洁且可能“更快”的处理方式。

    理论上,我们可以编写一个“非常复杂”的聚合管道版本,它对单个文档执行完全相同的步骤,如此处的代码所示。然而,它“非常复杂”并且依赖于现代方法,因为真正没有显着“减少”的是从服务器传输的数据。

    因此,如果您需要这种“跨文档”,请使用第一个清单中的完整聚合管道。但是,如果您一次只需要一个“单个用户”,或者在一小部分选择中每个用户的“不同结果”,那么客户端处理代码就是要走的路。

    两者都产生相同的输出,正如我所提到的,这比您要走的方向友好得多,并且根据需要在进一步的代码中更容易处理:

    {
            "_id" : "user 1",
            "feedback" : [
                    {
                            "type" : "communication",
                            "scores" : [
                                    {
                                            "score" : 2,
                                            "count" : 2
                                    },
                                    {
                                            "score" : 4,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 5,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 3,
                                            "count" : 1
                                    }
                            ]
                    },
                    {
                            "type" : "delivery",
                            "scores" : [
                                    {
                                            "score" : 3,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 4,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 2,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 5,
                                            "count" : 2
                                    }
                            ]
                    },
                    {
                            "type" : "timeliness",
                            "scores" : [
                                    {
                                            "score" : 1,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 5,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 4,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 3,
                                            "count" : 1
                                    },
                                    {
                                            "score" : 2,
                                            "count" : 1
                                    }
                            ]
                    }
            ]
    }
    

    【讨论】:

    • 太棒了!我回家后会试试这个,然后给你一个反馈。是的,我只是想为单个用户获取它
    • @John 那就在家试试吧。如果您只是匹配单个文档,那么您“应该”使用_id。如果您知道通过除主键之外的任何其他方式来查找单个文档所需的额外内部指令,您会发出喘息声。
    猜你喜欢
    • 2018-01-17
    • 1970-01-01
    • 2021-05-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-15
    • 1970-01-01
    相关资源
    最近更新 更多