在聚合管道中过滤数组为空时保留主文档

Posted

技术标签:

【中文标题】在聚合管道中过滤数组为空时保留主文档【英文标题】:Keep the main document when filtered array is empty in aggregation pipeline 【发布时间】:2015-05-08 14:09:07 【问题描述】:

这确实是对previously asked question 的补充。在@JohnnyHK 的帮助下,我现在可以根据特定标准从数组中删除不需要的子文档:deleted != null。我发现当数组中没有项目时我的$unwind 管道坏了,所以我还实现了一个$cond 来添加一个数组为空的虚拟对象。到目前为止的代码如下:

Collection.aggregate([
         $match:
             _id: ObjectID(collection_id) 
        ,
        
          $project:
            
              _id: 1,
              name: 1,
              images: 
                $cond:
                  [
                    
                      $eq:
                        [
                          "$images",
                          []
                        ]
                    ,
                    [
                      dummyImg // Variable containing dummy object
                    ],
                    '$images'
                  ]
              ,
            
        ,
         $unwind: "$images" ,
         $match:
             "images.deleted": null 
        ,

        // Regroup the docs by _id to reassemble the images array
        $group: 
            _id: '$_id',
            name: $first: '$name',
            images: $push: '$images'
        

    ], function (err, result) 
    if (err) 
        console.log(err);
        return;
    
    console.log(result);
);

现在当数组不为空但仅包含已删除为空的对象时会出现问题。我$unwind 图像,但$match 没有找到任何匹配项,所以不能执行最终的$group

我的想法是在管道的开头推入虚拟对象,然后在最后计算图像。如果虚拟对象是唯一的,则虚拟对象将保留,但如果有其他图像对象通过管道,则需要删除它。

如果这是一条明智的路线,我会很高兴得到一些指示。如果我的想法偏离了方向,我们将不胜感激地收到任何能引导我走向正确方向的提示。

谢谢。

【问题讨论】:

很高兴看到有人在发布问题之前进行真正的研究,并解释他们学到了什么。更多人应该这样发帖。 【参考方案1】:

现代 MongoDB 版本当然只需应用 $filter$addFields 将过滤后的可能为空的数组结果写入文档:

Collection.aggregate([
   "$addFields": 
    "images": 
      "$filter": 
        "input": "$filter",
        "as": "i",
        "cond":  "$eq": [ "$$i.deleted", null ] 
      
    
  
])

您之前给出的答案不是一个非常现代或有效的答案,并且有其他更好的方法来处理从数组中过滤内容而不是 $unwind$match$group

与 MongoDB 2.6 一样,您可以使用唯一的数组标识符来做到这一点:

Collection.aggregate([
     "$project": 
        "name": 1,
        "images":  "$cond": [
             "$eq": [ "$size":  "$ifNull": [ "$images",[]] , 0] ,
             "$ifNull": [ "$images", [] ] ,
             "$setDifference": [
                 "$map": 
                    "input": "$images",
                    "as": "i",
                    "in":  "$cond": [
                         "$eq": [ "$$i.deleted", null ] ,
                        "$$i",
                        false
                    ]
                ,
                [false]
            ]
        ]
    
],

$map 运算符通过返回由给定表达式评估的每个检查元素来转换文档中的数组。在这里,您可以使用$cond 来测试字段值并决定是按原样返回该字段还是返回false

$setDifference 操作将生成的转换数组与另一个奇异元素数组[false]“比较”。这样做的效果是从数组中删除所有不匹配的项,只留下那个,甚至是一个没有匹配项的空数组。


只要您的文档在文档的多个级别不包含相同的引用属性,以下带有$redact 的内容是安全的。看起来有些滑稽的情况是因为 "deleted": null 属性实际上被投影(出于评估目的)在它不存在的级别。这是必需的,因为$redact 以“递归”方式使用,下降文档树以决定删除什么或“编辑”:

Collection.aggregate([
     "$redact": 
        "$cond": [ 
             "$eq": [  "$ifNull": [ "$deleted", null ] , null ] ,
            "$$DESCEND",
            "$$PRUNE"
        ]
    
]

这确实是为您的特定目的实现的最简单的逻辑。请记住,如果您在文档中添加另一个“已删除”字段以某种方式表示其他含义,您以后可能无法使用它。


如果您真的被 MongoDB 2.6 之前的版本卡住并且无法访问这些操作,那么您当然需要执行 $unwind$match$groupprocess。因此,在开始时需要注意空数组或缺失数组,以及匹配没有匹配条目的数组时。

一种方法:

Collection.aggregate([
    // Cater for missing or empty arrays
     "$project": 
        "name": 1,
        "images":  "$cond": [
             "$eq": [ "$ifNull": [ "$images", [] ] , [] ] ,
             "$const": [ "deleted": false ] ,
            "$images"
        ]
    ,

    // Safe to unwind
     "$unwind": "$images" ,

    // Just count the matched array entries first
     "$group": 
        "_id": "$_id",
        "name":  "$first": "$name" ,
        "images":  "$push": "$images" ,
        "count":  "$sum":  "$cond": [
             "$eq": [ "$images.deleted", null ] ,
            1,
            0
        ]
    ,

    // Unwind again
     "$unwind": "$images" ,

    // Match either non deleted or unmatched array
     "$match":  
        "$or": [
             "images.deleted": null,
              "count": 0 
        ]
    ,

    // Group back with the things that were matched
     "$group": 
        "_id": "$_id",
        "name":  "$first": "$name" ,
        "images":  "$push": "$images" ,
        "count":  "$first": "$count" 
    ,

    // Replace the un-matched arrays with empty ones
     "$project": 
         "name": 1,
         "images":  "$cond": [
              "$eq": [ "$count", 0 ] ,
             [],
             "$images"
         ]
    
],

所以那里有更多的提升,但一般原则是只获得匹配元素的“计数”,当您过滤时,您还可以使用0“计数”保留数组项,但只需替换那些稍后整个数组。

您还可以在此处考虑,如果您首先在文档中维护“activeCount”字段,那么您将无需计算该字段并放弃一些阶段。

当然,这里的另一个论点是,您可以通过在单独的数组中实际维护“活动”和“已删除”项目来省去这个麻烦。每次更新都这样做消除了通过聚合进行过滤的任何需要。我想这一切都取决于你的真实目的。但是思想的食物。


当然,这一切都是根据您的原始数据进行测试的,并进行了一些修改以适应测试用例:


    "_id" : ObjectId("54ec9cac83a214491d2110f4"),
    "name" : "my_images",
    "images" : [
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2311026b0cb289ed04188"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:20:16.961Z")
        ,
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2314a26b0cb289ed04189"),
            "deleted" : ISODate("2015-02-24T15:38:14.826Z"),
            "date_added" : ISODate("2015-02-28T21:21:14.910Z")
        ,
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315526b0cb289ed0418a"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:25.042Z")
        ,
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315d26b0cb289ed0418b"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:33.081Z")
        
    ]
,

    "_id" : ObjectId("54fa6ca87c105bc872cc1886"),
    "name" : "another",
    "images" : [ ]
,

    "_id" : ObjectId("54fa6cef7c105bc872cc1887"),
    "name" : "final",
    "images" : [
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2314a26b0cb289ed04189"),
            "deleted" : ISODate("2015-02-24T15:38:14.826Z"),
            "date_added" : ISODate("2015-02-28T21:21:14.910Z")
        
    ]

所有版本产生的安全结果:


    "_id" : ObjectId("54ec9cac83a214491d2110f4"),
    "name" : "my_images",
    "images" : [
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2311026b0cb289ed04188"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:20:16.961Z")
        ,
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315526b0cb289ed0418a"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:25.042Z")
        ,
        
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315d26b0cb289ed0418b"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:33.081Z")
        
    ]
,

    "_id" : ObjectId("54fa6ca87c105bc872cc1886"),
    "name" : "another",
    "images" : [ ]
,

    "_id" : ObjectId("54fa6cef7c105bc872cc1887"),
    "name" : "final",
    "images" : [ ]

【讨论】:

谢谢@Neil Lunn。这非常有用,但我需要一段时间来消化和实施。一旦我将它纳入我们的应用程序,我会尽快回复。我认为$redact 方法将是答案,尽管已经有其他名为“delete”的字段,所以也许我会更新架构,使它们变成“deletedImg”或类似的。 好的,我现在可以使用了。我使用了您在答案中详述的非常简洁的$redact 方法。我在开头添加了一个$match 管道,然后在最后有一个$project 管道添加到一个占位符图像对象中,其中$redact 留下了一个完全空的图像数组。再次感谢@Neil Lunn。

以上是关于在聚合管道中过滤数组为空时保留主文档的主要内容,如果未能解决你的问题,请参考以下文章

如果给定参数,猫鼬聚合过滤器

当过滤器为空时,它返回空数组

当字符串的一部分为空时,使用 NSPredicate 过滤字符串的 NSArray

在 Java8 中使用 lambda 仅在不为空时过滤值

当多个输入可以为空时,在 Django 中使用 Q 进行过滤

仅当搜索栏文本不为空时,如何运行过滤器功能?