MongoDB $redact 过滤掉数组的一些元素

MongoDB $redact to filter out some elements of an array

我正在尝试对示例 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来实现?

如果您在问题中包含文档的原始状态作为示例,可能会更有帮助,因为这清楚地显示了 "where you are coming from" 然后 "where you want to get to" 作为目标除了给定的所需输出之外。

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

{
    "_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 上的 "directly",以便在任何级别处理 "year" 值上的数组元素。

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

简而言之shell,$redact 被排除在外,因为你不能用它适用的逻辑说 "take from here only"。

另一种方法是使用 $map and $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]            
        ]}
    }}
])

而且确实没有 "pretty" 方法可以绕过在中间阶段使用 $unwind 甚至这里的第二个 $project,因为 $map (和 $setDifference 过滤器 ) returns 什么是 "still an array"。因此 $unwind 是使 "array" 成为单数条目(前提是您的条件仅匹配 1 个元素)所必需的,用于比较。

尝试"squish"单个$project中的所有逻辑只会导致第二个输出中的"arrays of arrays",因此仍然需要一些"unwinding",但在至少以这种方式展开(希望如此)1 场比赛并不是真的那么昂贵,并且可以保持输出干净。


但这里真正要注意的另一件事是,您在这里根本 "aggregating" 什么都不是。这只是文档操作,因此您可能会考虑直接在客户端代码中执行该操作。正如这个 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" 的内容在从服务器返回时已经被过滤了,这可能与做的没有太大区别客户端处理方法,除非要删除的项目包含每个文档的相当大的列表。


郑重声明,此处真正需要对现有聚合管道进行的唯一更改是在 "re-combine" 数组条目的末尾添加一个 $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" }
    }}
])

但是话又说回来,这里有与所有操作相关的所有 "array duplication" 和 "cost of unwind"。因此,为了避免这种情况,前两种方法中的任何一种都是您真正想要的。

您可以使用带有嵌套表达式的单个项目阶段来避免多个阶段来实现此目的:

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 以获得一组有用的数组运算符。

但是,对于此查询,聚合看起来并不漂亮,并且逻辑足够简单,可以在代码本身中实现,而不会对性能产生太大影响;只是同意 。但是,关于 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();