【问题标题】:MongoDB $redact to filter out some elements of an arrayMongoDB $redact 过滤掉数组的一些元素
【发布时间】:2015-06-25 09:00:20
【问题描述】:

我正在尝试对示例 bios 集合 http://docs.mongodb.org/manual/reference/bios-example-collection/ 进行查询:

检索他们在获得图灵奖之前获得的所有人及其奖项。

我想出了这个查询:

db.bios.aggregate([
    {$match: {"awards.award" : "Turing Award"}},
    {$project: {"award1": "$awards", "award2": "$awards", "first_name": "$name.first", "last_name": "$name.last"}},
    {$unwind: "$award1"},
    {$match: {"award1.award" : "Turing Award"}},
    {$unwind: "$award2"},
    {$redact: {
        $cond: {
           if: { $eq: [ { $gt: [ "$award1.year", "$award2.year"] }, true]},
           then: "$$KEEP",
           else: "$$PRUNE"
           }
        }
    }
])

这就是答案:

/* 0 */
{
    "result" : [ 
    {
        "_id" : 1,
        "award1" : {
            "award" : "Turing Award",
            "year" : 1977,
            "by" : "ACM"
        },
        "award2" : {
            "award" : "W.W. McDowell Award",
            "year" : 1967,
            "by" : "IEEE Computer Society"
        },
        "first_name" : "John",
        "last_name" : "Backus"
    }, 
    {
        "_id" : 1,
        "award1" : {
            "award" : "Turing Award",
            "year" : 1977,
            "by" : "ACM"
        },
        "award2" : {
            "award" : "National Medal of Science",
            "year" : 1975,
            "by" : "National Science Foundation"
        },
        "first_name" : "John",
        "last_name" : "Backus"
    }, 
    {
        "_id" : 4,
        "award1" : {
            "award" : "Turing Award",
            "year" : 2001,
            "by" : "ACM"
        },
        "award2" : {
            "award" : "Rosing Prize",
            "year" : 1999,
            "by" : "Norwegian Data Association"
        },
        "first_name" : "Kristen",
        "last_name" : "Nygaard"
    }, 
    {
        "_id" : 5,
        "award1" : {
            "award" : "Turing Award",
            "year" : 2001,
            "by" : "ACM"
        },
        "award2" : {
            "award" : "Rosing Prize",
            "year" : 1999,
            "by" : "Norwegian Data Association"
        },
        "first_name" : "Ole-Johan",
        "last_name" : "Dahl"
    }
],
"ok" : 1
}

我不喜欢这个解决方案的地方是我放松了$award2。相反,我很乐意将 Award2 保留为一个数组,并且只删除在 Award1 之后收到的那些奖项。因此,例如,John Backus 的答案应该是:

{
    "_id" : 1,
    "first_name" : "John",
    "last_name" : "Backus",
    "award1" : {
        "award" : "Turing Award",
        "year" : 1977,
        "by" : "ACM"
    },
    "award2" : [ 
        {
            "award" : "W.W. McDowell Award",
            "year" : 1967,
            "by" : "IEEE Computer Society"
        }, 
        {
            "award" : "National Medal of Science",
            "year" : 1975,
            "by" : "National Science Foundation"
        }
    ]
}

是否可以在不使用$unwind: "$award2" 的情况下使用$redact 来实现它?

【问题讨论】:

    标签: mongodb mongodb-query aggregation-framework


    【解决方案1】:

    如果您在问题中包含文档的原始状态作为示例,可能会更有帮助,因为这清楚地显示了“您来自哪里”,然后是“您想要到达的地方”除了给定的期望输出之外,还有一个目标。

    这只是一个提示,但您似乎是从这样的文档开始的:

    {
        "_id" : 1,
        "name": { 
            "first" : "John",
            "last" : "Backus"
        },
        "awards" : [
            {
                "award" : "W.W. McDowell Award",
                "year" : 1967,
                "by" : "IEEE Computer Society"
            }, 
            {
                "award" : "National Medal of Science",
                "year" : 1975,
                "by" : "National Science Foundation"
            },
            { 
                "award" : "Turing Award",
                "year" : 1977,
                "by" : "ACM"
            },
            {
                "award" : "Some other award",
                "year" : 1979,
                "by" : "Someone Else"
            }
        ]
    }
    

    所以这里的真正要点是,虽然您可能已经在这里找到了$redact(这比使用$project 来处理逻辑条件然后使用$match 过滤该逻辑匹配要好一些)这可能不是您想要在此处进行比较的最佳工具。

    在继续之前,我只想指出$redact 的主要问题。无论您在这里做什么,逻辑(没有展开)本质上都是在$$DESCEND 上“直接”比较,以便在任何级别处理“年份”值的数组元素。

    该递归也将使“award1”条件无效,因为它具有相同的字段名称。即使重命名该字段也会破坏逻辑,因为缺少它的预计值不会大于测试值。

    简而言之,$redact 被排除在外,因为你不能用它适用的逻辑说“只从这里拿走”。

    替代方法是使用$map$setDifference 从数组中过滤内容,如下所示:

    db.bios.aggregate([
        { "$match": { "awards.award": "Turing Award" } },
        { "$project": {
            "first_name": "$name.first",
            "last_name": "$name.last",
            "award1": { "$setDifference": [
                { "$map": {
                    "input": "$awards",
                    "as": "a",
                    "in": { "$cond": [
                        { "$eq": [ "$$a.award", "Turing Award" ] },
                        "$$a",
                        false
                    ]}
                }},
                [false]
            ]},
            "award2": { "$setDifference": [
                { "$map": {
                    "input": "$awards",
                    "as": "a",
                    "in": { "$cond": [
                        { "$ne": [ "$$a.award", "Turing Award" ] },
                        "$$a",
                        false
                    ]}
                }},
                [false]
            ]}
        }},
        { "$unwind": "$award1" },
        { "$project": {
            "first_name": 1,
            "last_name": 1,
            "award1": 1,
            "award2": { "$setDifference": [
                { "$map": {
                    "input": "$award2",
                    "as": "a",
                    "in": { "$cond": [
                         { "$gt": [ "$award1.year", "$$a.year" ] },
                         "$$a",
                         false
                     ]}
                }},
                [false]            
            ]}
        }}
    ])
    

    真的没有“漂亮”的方式来解决在中间阶段使用$unwind,甚至是第二个$project,因为$map(和$setDifference过滤器)返回的是什么“仍然是一个数组”。因此,$unwind 是使“数组”成为单数(前提是您的条件仅匹配 1 个元素)条目以用于比较。

    试图“压缩”单个$project 中的所有逻辑只会导致第二个输出中出现“数组的数组”,因此仍然需要一些“展开”,但至少这种方式展开(希望如此) 1 匹配并没有那么昂贵,并且可以保持输出干净。


    但这里真正要注意的另一件事是,您根本没有真正“聚合”这里的任何东西。这只是文档操作,因此您可能会考虑直接在客户端代码中进行操作。正如这个 shell 示例所示:

    db.bios.find(
        { "awards.award": "Turing Award" },
        { "name": 1, "awards": 1 }
    ).forEach(function(doc) {
        doc.first_name = doc.name.first;
        doc.last_name = doc.name.last;
        doc.award1 = doc.awards.filter(function(award) {
            return award.award == "Turing Award"
        })[0];
        doc.award2 = doc.awards.filter(function(award) {
            return doc.award1.year > award.year;
        });
        delete doc.name;
        delete doc.awards;
        printjson(doc);
    })
    

    无论如何,两种方法都会输出相同的结果:

    {
        "_id" : 1,
        "first_name" : "John",
        "last_name" : "Backus",
        "award1" : {
                "award" : "Turing Award",
                "year" : 1977,
                "by" : "ACM"
        },
        "award2" : [
                {
                        "award" : "W.W. McDowell Award",
                        "year" : 1967,
                        "by" : "IEEE Computer Society"
                },
                {
                        "award" : "National Medal of Science",
                        "year" : 1975,
                        "by" : "National Science Foundation"
                }
        ]
    }
    

    这里唯一真正的区别是,通过使用.aggregate(),“award2”的内容在从服务器返回时已经被过滤,这可能与使用客户端处理方法没有太大区别,除非将被删除的项目包括每个文档相当大的列表。


    为了记录,这里真正需要对现有聚合管道的唯一更改是在末尾添加 $group 以将数组条目“重新组合”到单个文档中:

    db.bios.aggregate([
        { "$match": { "awards.award": "Turing Award" } },
        { "$project": {
            "first_name": "$name.first", 
            "last_name": "$name.last",
            "award1": "$awards",
            "award2": "$awards"
        }},
        { "$unwind": "$award1" },
        { "$match": {"award1.award" : "Turing Award" }},
        { "$unwind": "$award2" },
        { "$redact": {
            "$cond": {
                 "if": { "$gt": [ "$award1.year", "$award2.year"] },
                 "then": "$$KEEP",
                 "else": "$$PRUNE"
            }
        }},
        { "$group": {
            "_id": "$_id",
            "first_name": { "$first": "$first_name" },
            "last_name": { "$first": "$last_name" },
            "award1": { "$first": "$award1" },
            "award2": { "$push": "$award2" }
        }}
    ])
    

    但话又说回来,这里的所有操作都与所有“数组重复”和“展开成本”相关联。因此,前两种方法中的任何一种都是您真正想要避免的。

    【讨论】:

      【解决方案2】:

      您可以使用带有嵌套表达式的单个项目阶段来通过避免多个阶段来完成此任务:

      db.bios.aggregate([
          {$match : {"awards.award" : "Turing Award"}},
          {$project : {
              award1 : { $arrayElemAt : [{
                          $filter : {
                              input : "$awards",
                              as : "award",
                              cond : {$eq : ["$$award.award","Turing Award"]}
                          }}, 0]},
              award2 : { $let : {
                          vars : {
                          turing_year : { $let : {
                                          vars : {
                                          turingAward :{"$arrayElemAt" : [{"$filter" : {
                                              input : "$awards",
                                              as : "award",
                                              cond : {$eq : ["$$award.award","Turing Award"]}
                                          }}, 0]}},
                                          in : "$$turingAward.year"}}},
                          in : {
                              $filter : {
                                  input : "$awards",
                                  as : "award",
                                  cond : {$lt : ["$$award.year", "$$turing_year"]}
                              }
                          }
                  }},
              first_name : "$name.first",
              last_name : "$name.last"}
          }]).pretty();
      

      请查看文档here 以获取一组有用的数组运算符。

      但是,对于这个查询,聚合看起来并不漂亮,而且逻辑很简单,可以在代码本身中实现,而不会对性能产生太大影响;只是同意Blakes Seven's answer。但是,MongoDB 的一大优点是我们可以设计模式来支持我们的访问模式并保持我们的代码干净。如果在实际场景中需要这样的功能,我们可以简单地在文档中包含一个名为“turing_award_year”的字段。这会影响对集合的 CRUD 操作,但代码会很干净,现在我们可以使用这样的查询,它既漂亮又易于维护:

      db.bios.aggregate(
          [
              {$match : {"awards.award" : "Turing Award"}},
              {$project : {
                  award1 : { $arrayElemAt : [{
                              $filter : {
                                  input : "$awards",
                                  as : "award",
                                  cond : {$eq : ["$$award.award","Turing Award"]}
                              }}, 0]
                  },
                  award2 : { $filter : {
                                  input : "$awards",
                                  as : "award",
                                  cond : {$lt : ["$$award.year", "$turing_award_year"]}
                          }
                  }
                  ,
                  first_name : "$name.first",
                  last_name : "$name.last"
              }}
          ]
      ).pretty();
      

      【讨论】:

        猜你喜欢
        • 2022-06-14
        • 2022-11-02
        • 1970-01-01
        • 2018-03-14
        • 1970-01-01
        • 2023-02-18
        • 2022-12-06
        • 1970-01-01
        • 2013-12-16
        相关资源
        最近更新 更多