【问题标题】:Group by values and conditions按值和条件分组
【发布时间】:2015-07-25 13:27:58
【问题描述】:

对 mongo 非常陌生,在解决这个问题时遇到了一些麻烦。我的收藏看起来像这样

{ "_id" : "PN89dNYYkBBmab3uH", "card_id" : 1, "vote" : 1, "time" : 1437700845154 }
{ "_id" : "Ldz7N5syeW2SXtQzP", "card_id" : 1, "vote" : 1, "time" : 1437700846035 }
{ "_id" : "v3XWHHvFSHwYxxk6H", "card_id" : 1, "vote" : 2, "time" : 1437700849817 }
{ "_id" : "eehcDaCyTdz6Yd2a9", "card_id" : 2, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "efhcDaCyTdz6Yd2b9", "card_id" : 2, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "efhcDaCyTdz7Yd2b9", "card_id" : 3, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "w3XWgHvFSHwYxxk6H", "card_id" : 1, "vote" : 1, "time" : 1437700849817 }

我需要编写一个查询,该查询将基本上按投票对卡片进行分组(1 表示喜欢;2 表示不喜欢)。所以我需要得到每张卡的喜欢减去不喜欢的总数,并能够对它们进行排名。

结果会是这样的:

card_id 1:3 个喜欢 - 1 个不喜欢 = 2

card_id 3:1 喜欢 - 0 不喜欢 = 1

card_id 2:2 个喜欢 - 0 个不喜欢 = 0

知道如何计算所有卡片的 1 票减去 2 票,然后排序结果吗?

【问题讨论】:

  • @user2994560 在服务器上还是在客户端上? IE。您是否已经发布了所有数据并且只需要在模板中进行聚合,还是需要在将文档发送到客户端之前对其进行排序?

标签: mongodb meteor mongodb-query aggregation-framework


【解决方案1】:

要对 MongoDB 查询进行任何类型的“分组”,那么您希望能够使用 aggregation framework 或 mapReduce。聚合框架通常是首选,因为它使用本机编码运算符而不是 JavaScript 翻译,因此通常更快。

聚合语句只能在服务器 API 端运行,这很有意义,因为您不想在客户端执行此操作。但它可以在那里完成并将结果提供给客户。

感谢this answer 提供发布结果的方法:

Meteor.publish("cardLikesDislikes", function(args) {
    var sub = this;

    var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;

    var pipeline = [
        { "$group": {
            "_id": "$card_id",
            "likes": {
                "$sum": {
                    "$cond": [
                        { "$eq": [ "$vote", 1 ] },
                        1,
                        0
                    ]
                }
            },
            "dislikes": {
                "$sum": {
                    "$cond": [
                        { "$eq": [ "$vote", 2 ] },
                        1,
                        0
                    ]
                }
            },
            "total": {
                "$sum": {
                    "$cond": [
                        { "$eq": [ "$vote", 1 ] },
                        1,
                        -1
                    ]
                }
            }
        }},
        { "$sort": { "total": -1 } }
    ];

    db.collection("server_collection_name").aggregate(        
        pipeline,
        // Need to wrap the callback so it gets called in a Fiber.
        Meteor.bindEnvironment(
            function(err, result) {
                // Add each of the results to the subscription.
                _.each(result, function(e) {
                    // Generate a random disposable id for aggregated documents
                    sub.added("client_collection_name", Random.id(), {
                        card: e._id,                        
                        likes: e.likes,
                        dislikes: e.dislikes,
                        total: e.total
                    });
                });
                sub.ready();
            },
            function(error) {
                Meteor._debug( "Error doing aggregation: " + error);
            }
        )
    );

});

一般的聚合语句只是对“card_id”的单个键进行$group 操作。为了获得“喜欢”和“不喜欢”,您使用“条件表达式”,即$cond

这是一个“三元”运算符,它考虑对“投票”的值进行逻辑测试,如果它与期望类型匹配,则返回正数 1,否则返回 0

然后将这些值发送到累加器 $sum 以将它们加在一起,并通过“喜欢”或“不喜欢”产生每个“card_id”的总计数。

对于“总数”,最有效的方法是在分组的同时为“喜欢”赋予“正”值,为“不喜欢”赋予负值。有一个 $add 运算符,但在这种情况下,它的使用将需要另一个管道阶段。所以我们只是在一个阶段完成。

最后,$sort 按“降序”排列,因此最大的正票数排在首位。这是可选的,您可能只想使用动态排序客户端。但对于消除必须这样做的开销的默认设置来说,这是一个好的开始。

这就是进行条件聚合并处理结果。


测试清单

这是我用新创建的流星项目测试的,没有插件,只有一个模板和 javascript 文件

控制台命令

meteor create cardtest
cd cardtest
meteor remove autopublish

使用问题中发布的文档在数据库中创建了“卡片”集合。然后编辑默认文件,内容如下:

cardtest.js

Cards = new Meteor.Collection("cardStore");

if (Meteor.isClient) {

  Meteor.subscribe("cards");

  Template.body.helpers({
    cards: function() {
      return Cards.find({});
    }
  });

}

if (Meteor.isServer) {

  Meteor.publish("cards",function(args) {
    var sub = this;

    var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;

    var pipeline = [
      { "$group": {
        "_id": "$card_id",
        "likes": { "$sum": { "$cond": [{ "$eq": [ "$vote", 1 ] },1,0] } },
        "dislikes": { "$sum": { "$cond": [{ "$eq": [ "$vote", 2 ] },1,0] } },
        "total": { "$sum": { "$cond": [{ "$eq": [ "$vote", 1 ] },1,-1] } }
      }},
      { "$sort": { "total": -1, "_id": 1 } }

    ];

    db.collection("cards").aggregate(
      pipeline,
      Meteor.bindEnvironment(
        function(err,result) {
          _.each(result,function(e) {
            e.card_id = e._id;
            delete e._id;

            sub.added("cardStore",Random.id(), e);
          });
          sub.ready();
        },
        function(error) {
          Meteor._debug( "error running: " + error);
        }
      )
    );

  });
}

cardtest.html

<head>
  <title>cardtest</title>
</head>

<body>
  <h1>Card aggregation</h1>

  <table border="1">
    <tr>
      <th>Card_id</th>
      <th>Likes</th>
      <th>Dislikes</th>
      <th>Total</th>
    </tr>
    {{#each cards}}
      {{> card }}
    {{/each}}
  </table>

</body>

<template name="card">
  <tr>
    <td>{{card_id}}</td>
    <td>{{likes}}</td>
    <td>{{dislikes}}</td>
    <td>{{total}}</td>
  </tr>
</template>

最终聚合集合内容:

[
   {
     "_id":"Z9cg2p2vQExmCRLoM",
     "likes":3,
     "dislikes":1,
     "total":2,
     "card_id":1
   },
   {
     "_id":"KQWCS8pHHYEbiwzBA",
      "likes":2,
      "dislikes":0,
      "total":2,
      "card_id":2
   },
   {
      "_id":"KbGnfh3Lqcmjow3WN",
      "likes":1,
      "dislikes":0,
      "total":1,
      "card_id":3
   }
]

【讨论】:

  • 谢谢。 “server_collection_name”和“client_collection_name”是否与现有集合的名称相同?这是在为数据库创建一个名为“cardLikesDislikes”的新集合吗?如何访问结果
  • @user2994560 无论您的实际“收藏”名称是什么,这只是一个“占位符”,因为从您的问题的上下文中我不知道它实际上是什么。它基本上意味着“使用真正的集合名称”,因为这是较低级别的驱动程序,而不是流星集合的模型访问。
  • 这似乎几乎可以工作,除非我在控制台中运行 Scores.find().fetch() ,它只返回一个具有一个结果的对象。 like this _id: "4TbK37Yr2MoEJeJJy" card: null likes: 5 total: 4 dislikes: 1...这是所有卡片的正确总数,但不是按 ID 对投票进行分组
  • 全部加起来
  • @user2994560 抱歉耽搁了。如果您遇到问题,那么它将与您的实施方式有关。我已经包含了一个我使用过的非常基本的测试,并且对我来说可以正常工作。
【解决方案2】:

使用meteorhacks:aggregate 包。

Cards = new Mongo.Collection('cards');

var groupedCards = Cards.aggregate({ $group: { _id: { card_id: '$card_id' }}});

groupedCards.forEach(function(card) {
  console.log(card);
});


{ "_id" : "v3XWHHvFSHwYxxk6H", "card_id" : 1, "vote" : 2, "time" : 1437700849817 }
{ "_id" : "eehcDaCyTdz6Yd2a9", "card_id" : 2, "vote" : 1, "time" : 1437700850666 }
{ "_id" : "efhcDaCyTdz7Yd2b9", "card_id" : 3, "vote" : 1, "time" : 1437700850666 }
猜你喜欢
  • 1970-01-01
  • 2018-12-10
  • 2015-07-22
  • 2020-05-11
  • 1970-01-01
  • 1970-01-01
  • 2019-06-18
  • 2017-06-04
相关资源
最近更新 更多