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

Mongodb聚合$group,限制数组长度,mongodb,mongoose,mongodb-query,aggregation-framework,database,Mongodb,Mongoose,Mongodb Query,Aggregation Framework,Database,我想根据一个字段对所有文档进行分组,但要限制为每个值分组的文档数量 每条消息都有一个对话ID。我需要为每个对话ID获取10条或更少的消息 我可以根据以下命令进行分组,但不知道如何限制 除切片结果之外的分组文档数 Message.aggregate({'$group':{{u id:'$conversation\u id',msgs:{'$push':{msgid:'$$u id'}}) 如何将每个对话ID的msgs数组长度限制为10?$slice运算符不是聚合运算符,因此您不能执行此操作(如我在

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

每条消息都有一个对话ID。我需要为每个对话ID获取10条或更少的消息

我可以根据以下命令进行分组,但不知道如何限制 除切片结果之外的分组文档数
Message.aggregate({'$group':{{u id:'$conversation\u id',msgs:{'$push':{msgid:'$$u id'}})


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

$slice运算符不是聚合运算符,因此您不能执行此操作(如我在本回答中建议的,在编辑之前):

Neil的回答非常详细,但是您可以使用稍微不同的方法(如果它适合您的用例)。您可以聚合结果并将其输出到新集合:

db.messages.aggregate([
   { $group : {_id:'$conversation_ID',msgs: { $push: { msgid:'$_id' }}}},
   { $out : "msgs_agg" }
]);
运算符将聚合结果写入新集合。然后,您可以使用常规的“查找查询”项目将结果与$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 } ] }
编辑


我想这意味着复制整个消息集合。 这不是太过分了吗

很明显,这种方法无法扩展到大型收藏。但是,由于您正在考虑使用大型聚合管道或大型map reduce作业,您可能不会将其用于“实时”请求

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

这种方法的优点是:易于实现,因此易于更改。如果您的集合很少更新,您可以像缓存一样使用此“out”集合。这样,您就不必多次执行聚合操作,甚至可以在“out”集合上支持“实时”客户端请求。要刷新数据,可以定期进行聚合(例如,在夜间运行的后台作业中)

正如评论中所说,这不是一个容易的问题,也没有一个完美的解决方案。我向您展示了另一种可以使用的方法,这取决于您对基准测试和决定什么最适合您的用例

现代的 MongoDB 3.6提供了一种“新颖”的方法,即使用执行“自连接”,方式与下面演示的原始游标处理大致相同

因为在此版本中,您可以指定一个
“pipeline”
参数作为“join”的源,这本质上意味着您可以使用和来收集和“限制”数组的条目:

db.messages.aggregate([
{“$group”:{“\u id”:“$conversation\u id”},
{“$lookup”:{
“发件人”:“邮件”,
“let”:{“conversation”:“$\u id”},
“管道”:[
{“$match”:{“$expr”:{“$eq”:[“$conversation_ID”,“$$conversation”]}},
{“$limit”:10},
{“$project”:{“\u id”:1}
],
“as”:“msgs”
}}
])
您可以选择在后面添加额外的投影,以便使数组项仅包含值,而不是带有
\u id
键的文档,但只需执行上述操作即可获得基本结果

还有一些未解决的问题实际上要求直接“限制推动”,但在此期间,以这种方式使用是一种可行的选择

:还有一个是在撰写原始答案后介绍的,并在原始内容中被“杰出的JIRA问题”提及。虽然您可以通过较小的结果集获得相同的结果,但它仍然需要将所有内容“推入”阵列,然后将最终阵列输出限制为所需的长度

所以这就是主要的区别,也是为什么对大的结果进行分析通常是不实际的。但是,当然也可以在需要的情况下交替使用

关于这两种替代用法,还有更多的细节


起初的 如前所述,这不是不可能的,但肯定是一个可怕的问题

实际上,如果您主要关心的是生成的数组将非常大,那么最好的方法是将每个不同的“对话ID”作为单个查询提交,然后合并结果。在MongoDB 2.6语法中,可能需要根据您的语言实现进行一些调整:

var结果=[];
db.messages.aggregate([
{“$组”:{
“\u id”:“$conversation\u id”
}}
]).forEach(功能(文档){
db.messages.aggregate([
{“$match”:{“对话ID”:doc.\u ID},
{“$limit”:10},
{“$组”:{
“\u id”:“$conversation\u id”,
“msgs”:{“$push”:“$\u id”}
}}
]).forEach(函数(res){
结果:push(res);
});
});
但这一切都取决于你是否正试图避免这种情况。接下来是真正的答案:


这里的第一个问题是,没有函数“限制”被“推”到数组中的项数。这当然是我们想要的,但功能目前还不存在

第二个问题是,即使将所有项推送到一个数组中,也不能在聚合管道中使用或任何类似的运算符。因此,目前还没有通过简单的操作从生成的数组中获得“前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 }
{u id:1,“对话id:123}
{
// { "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 ] }