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

时间:2015-06-25 09:00:21

标签: mongodb mongodb-query aggregation-framework

我正在尝试对示例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作为阵列保留,并且只删除奖励后获得的奖励1。因此,例如,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"
        }
    ]
}

是否可以在$redact $unwind: "$award2"的情况下实现arsham

2 个答案:

答案 0 :(得分:5)

如果您在文档中包含文档的原始状态作为示例,这可能会更有帮助,因为这清楚地显示了您来自"然后到"你要去的地方#34;作为一个目标,除了您给定的所需输出。

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

{
    "_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]            
        ]}
    }}
])

并且确实没有"漂亮"绕过itermediatary阶段的$unwind甚至第二个$project的使用方式,因为$map(以及$setDifference过滤器)返回的是&#34 ;还是一个数组"。所以$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" }
    }}
])

但话又说回来,所有这些"阵列重复"和放松的成本"与此处的所有操作相关联。因此,前两种方法中的任何一种都是您真正想要的,以避免这种情况。

答案 1 :(得分:0)

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

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();