如何提取不在数组类型字段中的值的总计数

时间:2015-07-14 11:03:28

标签: mongodb mapreduce mongodb-query aggregation-framework

我将完整的消息线程(带消息)存储为单个文档。数组字段participants保存参与者用户ID。每条消息都有数组字段read_by,其中包含读取该消息的用户ID。

示例数据:

db.threads_test.insert( { "subject" : "subject 1", "participants" : ["u1", "u2"], "messages" : [
{"message_id" : "m1", "message" : "msg 1", "read_by" : ["u1"]},
{"message_id" : "m2", "message" : "msg 2", "read_by" : ["u2"]}
]});

db.threads_test.insert( { "subject" : "subject 2", "participants" : ["u1", "u2"], "messages" : [
{"message_id" : "m3", "message" : "msg 3", "read_by" : ["u1"]},
{"message_id" : "m4", "message" : "msg 4", "read_by" : ["u1"]}
]});

db.threads_test.insert( { "subject" : "subject 3", "participants" : ["u1", "u3"], "messages" : [
{"message_id" : "m5", "message" : "msg 5", "read_by" : ["u1", "u3"]}
]});

我需要知道有多少未读线程以及用户拥有多少条未读消息。

解释逻辑是:

  • 未读线程是用户在participants数组中且位于的位置 用户不在read_by
  • 的至少一条消息
  • 未读消息是属于具有用户的线程的消息 participants read_by并且u1: threds=1, messages=1 u2: threads=2, messages=3 u3: threads=0, messages=0
  • 中没有该用户

每位用户的预期未读数量:

@media screen

我一直在检查聚合框架但找不到解决方案。

Mongo版本是2.4.9

2 个答案:

答案 0 :(得分:0)

我认为你这样做是错误的结果(我认为我可能是错的)。

创建多个集合可能更好。为什么不让表格放threadsmessagesuser_read

Collection threads:
{thread_id: "...", subject: "...", participants: ["u1", "u2"], ...}
{thread_id: "...", subject: "...", participants: ["u1", "u3"], ...}
...

Collection messages:
{thread_id: "...", user_id: "...", message: "..."}
{thread_id: "...", user_id: "...", message: "..."}
...


Collection user_read:
{user_id: "u1", type: "thread", id: "..."}
{user_id: "u1", type: "message", id: "..."} # Care here the type is a message
...

现在您知道第一个集合中有X个线程(让我们称之为nbrThreads)。并且您可以计算" u1"读取的线程数(使用type=='thread')。轻松地在user_read(让我们称之为userThreadsRead)。因此:

unreadThread = nbrThreads-userThreadsRead

通过在type=='message'表上计算第二个集合和(使用user_read)消息的相同逻辑。调用那些变量nbrMessages和userMessagesRead

unreadMessage = nbrMessages-userMessagesRead

另外,如果你不能改变结构,我建议你重新组织你的结构,以便能够做类似的事情。通过获取数组的长度,我应该可以对你的结构做同样的事情。

祝你好运!

答案 1 :(得分:0)

这对聚合框架来说并不容易,主要是由于有很多数组,所以有很多方法可以轻易搞错。幸运的是,当你仔细观察它时,这里有一个合理的逻辑模式,因为它只是归结为一个比较点:

db.threads_test.aggregate([
    // Unwind all arrays
    { "$unwind": "$messages" },
    { "$unwind": "$messages.read_by" },
    { "$unwind": "$participants" },

    // Group on distinct "message_id" comparing "particpant" and "read_by"
    { "$group": {
        "_id": {
            "_id": "$_id",
            "participant": "$participants",
            "message_id": "$messages.message_id"
        },
        "unread": { 
            "$min": {
                "$cond": [
                    { "$ne": [ "$participants", "$messages.read_by" ] },
                    1,
                    0
                ]
            }
        }
    }},

    // Get a sum of unread per thread
    { "$group": {
        "_id": {
            "_id": "$_id._id",
            "participant": "$_id.participant",
        },
        "unread": { "$sum": "$unread" }
    }},

    // Sum per participant counting unread threads
    { "$group": {
        "_id": "$_id.participant",
        "threads": { 
            "$sum": { 
                "$cond": [
                    { "$ne": [ "$unread", 0 ] },
                    1,
                    0
                ] 
            }       
        },
        "unread": { "$sum": "$unread" }
    }}
])

这会给你结果:

{ "_id" : "u2", "threads" : 2, "unread" : 3 }
{ "_id" : "u3", "threads" : 0, "unread" : 0 }
{ "_id" : "u1", "threads" : 1, "unread" : 1 }

第一个$group阶段至关重要。在每个阵列上处理的Ater $unwind将会有很多重复的复制。幸运的是每个级别的"线程"和"消息"有他们自己独特的" id"值。与独特的"参与者"他们自己这是至关重要的一点。

当您查看"解开"中的数据时形式,你应该能够看到所有重复的"关键测试"这里是比较"参与者"和" read_by"值以查看它们是否相同。就像"处理循环" (除了所有组合都没有列出),那么你只需要返回"一次"对于给定的消息,其中"参与者"和" ready_by"是"等于"。

这解释了"分组"组合。使用"键"由"线程","参与者"和" message_id"您只需要与" read_by"进行比较后的 $min 数值结果。所以,如果至少"一个" " read_by"已匹配,则计数为1,否则为0

接下来的几个阶段只是为您的总计进行仔细分组。首先得到总数"未读"计算每个线程,然后将未读消息的线程计数到最终的"参与者"分组密钥。

所以虽然它不是"总是"找到解决方案的途径,在开始时执行所有$unwind操作是一种可视化数据的好方法,这样您就可以理解解决方案。

替代方法

正如您所说,您可以使用MongoDB 2.4,并且根据您的集合的大小,然后像这样处理$unwind会导致很多开销。后来的版本对此有一些规定,但它可能是一个问题。

我之前提到"处理循环",这正是您可以使用mapReduce做的事情。

虽然这里通常首选聚合框架,但如果大小是限制,则可能需要考虑这个:

db.threads_test.mapReduce(
    function () {
      var doc = this;
      doc.participants.forEach(function(participant) {
        doc.messages.forEach(function(message) {
          var obj = {
            threads: [],
            unread: 0
          };

          if ( message.read_by.indexOf(participant) == -1 ) {
            obj.threads.push(doc._id.valueOf());
            obj.unread = 1;
          }

          emit(participant,obj);
        })
      })
    },
    function (key,values) {

      var result = { "threads": [], "unread": 0 };

      values.forEach(function(value) {
        value.threads.forEach(function(thread) {
          if ( result.threads.indexOf(thread) == -1 )
            result.threads.push(thread);
        })
        result.unread += value.unread;
      });

      return result;

    },
    { 
       "finalize": function(key,value) {
           value.threads = value.threads.length;
           return value;
       },
       "out": { "inline": 1 }
    }
)

真的在这里。通过每个消息的线程上的每个参与者,将它们与" read_by"列表,看看他们是否在那里。我们发出"线程ID"当邮件未读时,结果如果"未读"。与参与者一起为线程上的每条消息发出此消息。所以循环循环"。

结果是"减少"从"线程"中提取不同的值并且参与者总计未读消息。

因为"线程"是一个独特的列表" id"值,我们只想在减少后最终得到该列表的长度。这就是"最终确定"在这里做并将列表转换为它的长度的数值。

相同的结果,但不是很漂亮,因为这是mapreduce的限制:

    "results" : [
            {
                    "_id" : "u1",
                    "value" : {
                            "threads" : 1,
                            "unread" : 1
                    }
            },
            {
                    "_id" : "u2",
                    "value" : {
                            "threads" : 2,
                            "unread" : 3
                    }
            },
            {
                    "_id" : "u3",
                    "value" : {
                            "threads" : 0,
                            "unread" : 0
                    }
            }
    ],

无论哪一个最适合你,问题解决方案现在应该对于程序性大脑和聚集大脑都是清楚的