MongoDB: агрегация с redact с $in не выполняется

У меня есть две следующие Objects в коллекции mongo с именем cars:

{vendor: "BMW",
model: "325i",
class: "coupe",
equipment: [
    {name: "airbag",
     cost: 120
    },
    {name: "led",
     cost: 170
    },
    {name: "abs",
     cost: 150
    }
 ]
}

{vendor: "Mercedes",
model: "C 250",
class: "coupe",
equipment: [
    {name: "airbag",
     cost: 180
    },
    {name: "esp",
     cost: 170
    },
    {name: "led",
     cost: 120
    }
 ]
}

Я играю с новой функцией агрегации redact, представленной в Mongo 2.6 Я хочу выполнить запрос, чтобы получить список автомобилей с отфильтрованными частями оборудования. В словах: Дайте мне все автомобили типа купе и только информацию об оборудовании LED и AIRBAG.

Я пробовал эти агрегации:

db.cars.aggregate(
    [
        {$match: {"class": "coupe"}},
        {$redact:
        {
            $cond:
            {
                if: { $in : ["$name", ["airbag", "led"]]},
                then: "$$DESCEND",
                else: "$$PRUNE"
            }
        }
        }
    ]
)

Но это приводит к следующей ошибке:

assert: команда не выполнена: { "errmsg": "exception: недействительный оператор '$ in'", "code": 15999, "ok": 0}: aggregate failed

Что я делаю неправильно? Есть ли другой способ достижения цели.

Я ожидал бы вернуть этот результат от Mongo:

{vendor: "BMW",
model: "325i",
class: "coupe",
equipment: [
    {name: "airbag",
     cost: 120
    },
    {name: "led",
     cost: 170
    }
 ]
}

{vendor: "Mercedes",
model: "C 250",
class: "coupe",
equipment: [
    {name: "airbag",
     cost: 180
    },
    {name: "led",
     cost: 120
    }
 ]
}

Любые предложения о том, как достичь этого?

Cheers, Ralf

Ответ 1

$redact оператор конвейера действительно не тот, который вы хотите для этого случая. То, что он хочет сделать, рекурсивно "спускается" через структуру документа и оценивает условия на каждом "уровне", чтобы увидеть, какие действия он предпримет.

В вашем случае на верхнем уровне документа нет поля "имя", чтобы соответствовать условию, поэтому в результате весь документ "обрезается".

Вы после фильтрации массивов, и в этом случае, когда вы не хотите использовать $unwind и $match, то вы можете использовать новые операторы, такие как $map:

db.cars.aggregate([
    { "$match": { "class": "coupe" }},
    { "$project": {
        "equipment": {
            "$setDifference": [
                { "$map": {
                    "input": "$equipment",
                    "as": "el",
                    "in": {
                        "$cond": [
                             { "$or": [
                                { "$eq": [ "$$el.name", "airbag" ] },
                                { "$eq": [ "$$el.name", "led" ] }
                             ]},
                             { "$cond": [ 1, "$$el", 0 ] },
                             false
                        ]
                    }
                }},
                [false]
            ]
        }
    }}
])

Оператор $map работает с массивом и оценивает логическое условие для всех элементов, в этот случай в $cond. То же самое, что $in, которое не является логическим оператором в этом смысле, использует $or, который является логическим оператором для структуры агрегации.

Как любой элемент, который не удовлетворяет условию, вернет false, теперь вам нужно "удалить" все значения false. Это помогает $setDifference, который будет делать это путем сравнения.

В результате вы хотите:

{
    "_id" : ObjectId("53b2725120edfc7d0df2f0b1"),
    "equipment" : [
            {
                    "name" : "airbag",
                    "cost" : 120
            },
            {
                    "name" : "led",
                    "cost" : 170
            }
    ]
}
{
    "_id" : ObjectId("53b2725120edfc7d0df2f0b2"),
    "equipment" : [
            {
                    "name" : "airbag",
                    "cost" : 180
            },
            {
                    "name" : "led",
                    "cost" : 120
            }
    ]
}

Если вы действительно намеревались использовать $redact, то всегда есть этот надуманный пример:

db.cars.aggregate([
    { "$match": { "class": "coupe" }},
    { "$redact": {
        "$cond": {
            "if": {
                "$or": [
                    { "$eq": [
                       { "$ifNull": ["$name", "airbag"] },
                       "airbag"  
                    ]},
                    { "$eq": [
                       { "$ifNull": ["$name", "led"] },
                       "led"  
                    ]},
                ]
            },
            "then": "$$DESCEND",
            "else": "$$PRUNE"
        }
    }}
])

Таким образом, в основном, убедитесь, что если поле не существует вообще при нисходящем, оно будет "искусственно" создавать тот, который будет соответствовать, так что уровень не будет "обрезанным". Если поле существует, используется найденное значение, и если это не соответствует условию, оно будет обрезано.