Mongodb聚合$ group,限制数组的长度

时间:2014-06-28 04:48:49

标签: mongodb mongoose mongodb-query aggregation-framework database

我想根据字段对所有文档进行分组,但要限制为每个值分组的文档数。

每条消息都有一个conversation_ID。我需要为每个conversation_ID获取10个或更少数量的消息。

我可以根据以下命令进行分组,但无法弄清楚如何限制 除了切片结果之外,分组文档的数量 Message.aggregate({'$group':{_id:'$conversation_ID',msgs:{'$push':{msgid:'$_id'}}}})

如何将每个conversation_ID的msgs数组长度限制为10?

4 个答案:

答案 0 :(得分:15)

现代

从MongoDB 3.6开始,有一本小说"通过使用$lookup来执行"自我加入"与下面演示的原始光标处理方式大致相同。

由于在此版本中您可以为$lookup指定"pipeline"参数作为" join"的来源,这实际上意味着您可以使用$match和{ {3}}收集并限制"数组的条目:

db.messages.aggregate([
  { "$group": { "_id": "$conversation_ID" } },
  { "$lookup": {
    "from": "messages",
    "let": { "conversation": "$_id" },
    "pipeline": [
      { "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
      { "$limit": 10 },
      { "$project": { "_id": 1 } }
    ],
    "as": "msgs"
  }}
])

您可以选择在$limit之后添加其他投影,以使数组项只是值而不是带有_id键的文档,但基本结果就是通过简单地执行上述操作。

仍有未完成的$lookup实际上要求限制推送"直接,但以这种方式使用SERVER-9277是临时的可行替代方案。

  

注意:在撰写原始答案后引入了$lookup,并提及了#34;杰出的JIRA问题"在原始内容中。虽然你可以用小的结果集获得相同的结果,但它确实涉及仍然"推动一切"进入数组,然后将最终的数组输出限制为所需的长度。

     

这是主要区别以及为什么$slice对于大结果通常不实用。但当然可以在它出现的情况下交替使用。

     

$slice上有关于其他用法的更多详情。


原始

如前所述,这并非不可能,但肯定是一个可怕的问题。

实际上,如果你主要担心的是你的结果数组会特别大,那么你最好的方法就是提交每个不同的" conversation_ID"作为单个查询然后合并您的结果。在非常MongoDB 2.6语法中,可能需要进行一些调整,具体取决于您的语言实现是什么:

var results = [];
db.messages.aggregate([
    { "$group": {
        "_id": "$conversation_ID"
    }}
]).forEach(function(doc) {
    db.messages.aggregate([
        { "$match": { "conversation_ID": doc._id } },
        { "$limit": 10 },
        { "$group": {
            "_id": "$conversation_ID",
            "msgs": { "$push": "$_id" }
        }}
    ]).forEach(function(res) {
        results.push( res );
    });
});

但这一切都取决于你是否想要避免这种情况。那么真正的答案是:


这里的第一个问题是没有任何功能来限制"推送的项目数量"成阵列。这当然是我们想要的,但功能目前还不存在。

第二个问题是即使将所有项目推送到数组中,也不能在聚合管道中使用mongodb group values by multiple fields或任何类似的运算符。所以现在没有办法获得前十名"前十名"通过简单操作生成的数组的结果。

但你实际上可以产生一系列有效的操作"切片"在你的分组边界上。它是相当复杂的,例如在这里我将减少数组元素"切片"到"六"只要。这里的主要原因是演示该过程并展示如何在不破坏数据的情况下执行此操作,这些数组不包含您想要的总数" slice"到。

给出一份文件样本:

{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }

你可以看到,根据你的条件进行分组时,你会得到一个包含十个元素的数组,另一个用"五个"。你想在这里做什么可以减少到顶部"六"没有"摧毁"只匹配"五"的数组元件。

以下查询:

db.messages.aggregate([
    { "$group": {
        "_id": "$conversation_ID",
        "first": { "$first": "$_id" },
        "msgs": { "$push": "$_id" },
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "seen": { "$eq": [ "$first", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "seen": { "$eq": [ "$second", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "seen": { "$eq": [ "$third", "$msgs" ] },
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "forth": 1,
        "seen": { "$eq": [ "$forth", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$forth" },
        "fifth": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "forth": 1,
        "fifth": 1,
        "seen": { "$eq": [ "$fifth", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$forth" },
        "fifth": { "$first": "$fifth" },
        "sixth": { "$first": "$msgs" },
    }},
    { "$project": {
         "first": 1,
         "second": 1,
         "third": 1,
         "forth": 1,
         "fifth": 1,
         "sixth": 1,
         "pos": { "$const": [ 1,2,3,4,5,6 ] }
    }},
    { "$unwind": "$pos" },
    { "$group": {
        "_id": "$_id",
        "msgs": {
            "$push": {
                "$cond": [
                    { "$eq": [ "$pos", 1 ] },
                    "$first",
                    { "$cond": [
                        { "$eq": [ "$pos", 2 ] },
                        "$second",
                        { "$cond": [
                            { "$eq": [ "$pos", 3 ] },
                            "$third",
                            { "$cond": [
                                { "$eq": [ "$pos", 4 ] },
                                "$forth",
                                { "$cond": [
                                    { "$eq": [ "$pos", 5 ] },
                                    "$fifth",
                                    { "$cond": [
                                        { "$eq": [ "$pos", 6 ] },
                                        "$sixth",
                                        false
                                    ]}
                                ]}
                            ]}
                        ]}
                    ]}
                ]
            }
        }
    }},
    { "$unwind": "$msgs" },
    { "$match": { "msgs": { "$ne": false } }},
    { "$group": {
        "_id": "$_id",
        "msgs": { "$push": "$msgs" }
    }}
])

您可以获得阵列中的最高结果,最多六个条目:

{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }

正如你在这里看到的,充满乐趣。

在您最初分组后,您基本上想要" pop"数组结果的堆栈$slice值。为了简化这个过程,我们实际上在初始操作中执行此操作。所以这个过程就变成了:

  • $first数组
  • 与已经看到的$unwind相等匹配
  • 的值进行比较
  • $eq结果为" float" false看不见的值到顶部(这仍然保留订单)
  • $sort又回来了," pop" $group看不见的值作为堆栈中的下一个成员。此外,这使用$first运算符替换"看到"使用false的数组堆栈中的值来帮助进行评估。

使用$cond的最终操作是为了确保将来的迭代不只是在" slice"的位置上反复添加数组的最后一个值。 count大于数组成员。

需要为您想要的任意数量的项目重复整个过程" slice"。因为我们已经找到了第一个"初始分组中的项目,表示所需切片结果的n-1次迭代。

最后的步骤实际上只是将所有内容转换回数组的可选示例,最终显示结果。所以真的只是有条不紊地推动项目或false回到他们的匹配位置,最后"过滤"输出所有false值,以便结束数组具有" six"和"五"成员分别。

所以没有一个标准的操作员可以满足这个要求,你不能只限制"限制"推送到5或10或阵列中的任何项目。但如果你真的必须这样做,那么这是你最好的方法。


你可以用mapReduce来解决这个问题,并将聚合框架放在一起。我将采取的方法(在合理的限制范围内)将有效地在服务器上具有内存中的哈希映射并将数组累积到该服务器上,同时使用JavaScript切片来限制"结果:

db.messages.mapReduce(
    function () {

        if ( !stash.hasOwnProperty(this.conversation_ID) ) {
            stash[this.conversation_ID] = [];
        }

        if ( stash[this.conversation_ID.length < maxLen ) {
            stash[this.conversation_ID].push( this._id );
            emit( this.conversation_ID, 1 );
        }

    },
    function(key,values) {
        return 1;   // really just want to keep the keys
    },
    { 
        "scope": { "stash": {}, "maxLen": 10 },
        "finalize": function(key,value) {
            return { "msgs": stash[key] };                
        },
        "out": { "inline": 1 }
    }
)

所以这基本上构建了内存中的&#34;&#34;对象匹配发出的&#34;键&#34;数组永远不会超过您想从结果中获取的最大大小。此外,这甚至不会打扰&#34;发出&#34;满足最大堆栈时的项目。

减少部分实际上除了基本上只是减少到&#34;键&#34;和一个单一的价值。因此,万一我们的reducer没有被调用,如果一个键只存在1个值就行了,finalize函数负责映射&#34; stash&#34;最终输出的关键。

这有效性因输出的大小而异,JavaScript评估肯定不会很快,但可能比在管道中处理大型数组更快。


投票$cond实际上有一个&#34;切片&#34;操作员甚至是限制&#34; on&#34; $ push&#34;和&#34; $ addToSet&#34;,这两者都很方便。个人希望至少可以对JIRA issues运算符进行一些修改以公开当前索引&#34;处理时的价值。这将有效地允许&#34;切片&#34;和其他行动。

你真的想要将其编码为&#34;生成&#34;所有必需的迭代。如果这里的答案得到足够的爱和/或其他时间待定,那么我可能会添加一些代码来演示如何执行此操作。这已经是一个相当长的回应。


生成管道的代码:

var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;

var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": []  } } };

for ( var x = 1; x <= maxLen; x++ ) {

    fproj["$project"][""+x] = 1;
    fproj["$project"]["pos"]["$const"].push( x );

    var rec = {
        "$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
    };
    if ( stack.length == 0 ) {
        rec["$cond"].push( false );
    } else {
        lval = stack.pop();
        rec["$cond"].push( lval );
    }

    stack.push( rec );

    if ( x == 1) {
        pipe.push({ "$group": {
           "_id": key,
           "1": { "$first": val },
           "msgs": { "$push": val }
        }});
    } else {
        pipe.push({ "$unwind": "$msgs" });
        var proj = {
            "$project": {
                "msgs": 1
            }
        };

        proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };

        var grp = {
            "$group": {
                "_id": "$_id",
                "msgs": {
                    "$push": {
                        "$cond": [ { "$not": "$seen" }, "$msgs", false ]
                    }
                }
            }
        };

        for ( n=x; n >= 1; n-- ) {
            if ( n != x ) 
                proj["$project"][""+n] = 1;
            grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
        }

        pipe.push( proj );
        pipe.push({ "$sort": { "seen": 1 } });
        pipe.push(grp);
    }
}

pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
    "$group": {
        "_id": "$_id",
        "msgs": { "$push": stack[0] }
    }
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
    "$group": {
        "_id": "$_id",
        "msgs": { "$push": "$msgs" }
    }
}); 

使用从maxLen$unwind的步骤,构建基本的迭代方法,直到$group。还嵌入了所需的最终投影的详细信息以及&#34;嵌套&#34;条件陈述。最后一个基本上是针对这个问题的方法:

$map

答案 1 :(得分:2)

Mongo 4.4开始,$group阶段有了一个新的聚合运算符$accumulator,允许通过javascript用户定义函数对文档进行分组时进行自定义累积。

因此,为了仅为每个对话选择n条消息(例如2):

// { "conversationId" : 3, "messageId" : 14 }
// { "conversationId" : 5, "messageId" : 34 }
// { "conversationId" : 3, "messageId" : 39 }
// { "conversationId" : 3, "messageId" : 47 }
db.collection.aggregate([
  { $group: {
    _id: "$conversationId",
    messages: {
      $accumulator: {
        accumulateArgs: ["$messageId"],
        init: function() { return [] },
        accumulate:
          function(messages, message) { return messages.concat(message).slice(0, 2); },
        merge:
          function(messages1, messages2) { return messages1.concat(messages2).slice(0, 2); },
        lang: "js"
      }
    }
  }}
])
// { "_id" : 5, "messages" : [ 34 ] }
// { "_id" : 3, "messages" : [ 14, 39 ] }

累加器:

  • 累积在messageIdaccumulateArgs)字段上
  • 被初始化为空数组(init
  • 在一个数组中累积messageId个项目,最多只能保留2个(accumulatemerge

答案 2 :(得分:0)

$ slice运算符不是聚合运算符,因此不能执行此操作(就像我在此答案中建议的那样,在编辑之前):

db.messages.aggregate([
   { $group : {_id:'$conversation_ID',msgs: { $push: { msgid:'$_id' }}}},
   { $project : { _id : 1, msgs : { $slice : 10 }}}]);

Neil的答案非常详细,但您可以采用略有不同的方法(如果它适合您的使用案例)。您可以汇总结果并将其输出到新集合:

db.messages.aggregate([
   { $group : {_id:'$conversation_ID',msgs: { $push: { msgid:'$_id' }}}},
   { $out : "msgs_agg" }
]);

$out运算符会将聚合的结果写入新集合。然后,您可以使用$ slice运算符使用常规查找查询项目结果:

db.msgs_agg.find({}, { msgs : { $slice : 10 }});

对于此测试文档:

> db.messages.find().pretty();
{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 1234 }
{ "_id" : 8, "conversation_ID" : 1234 }
{ "_id" : 9, "conversation_ID" : 1234 }

结果将是:

> db.msgs_agg.find({}, { msgs : { $slice : 10 }});
{ "_id" : 1234, "msgs" : [ { "msgid" : 7 }, { "msgid" : 8 }, { "msgid" : 9 } ] }
{ "_id" : 123, "msgs" : [ { "msgid" : 1 }, { "msgid" : 2 }, { "msgid" : 3 }, 
                          { "msgid" : 4 }, { "msgid" : 5 } ] }

修改

  

我认为这意味着要复制整个邮件集合。   那不是太难了吗?

嗯,显然这种方法不会扩大规模。但是,由于您正在考虑使用大型聚合管道或大型地图缩减作业,您可能不会将其用于实时&#34;请求。

这种方法有很多缺点:如果您使用聚合创建大型文档,浪费磁盘空间/内存并复制,增加磁盘IO,则会有16 MB BSON限制...

这种方法的优点:它易于实现,因此易于更改。如果你的收藏品很少更新,你可以使用这个&#34; out&#34;像缓存一样的集合。这样您就不必多次执行聚合操作,甚至可以支持&#34;实时&#34;客户请求&#34; out&#34;采集。要刷新数据,您可以定期进行聚合(例如,在每晚运行的后台作业中)。

就像在评论中所说的那样,这不是一个容易出问题的问题,而且还没有一个完美的解决方案(还有!)。我向您展示了您可以使用的另一种方法,由您决定基准并决定哪种方法最适合您的用例。

答案 3 :(得分:0)

我希望这会如您所愿:

db.messages.aggregate([
   { $group : {_id:'$conversation_ID',msgs: { $push: { msgid:'$_id' }}}},
   { $project : { _id : 1, msgs : { $slice : ["$msgid",0,10] }}}
]);