Подтвердить что ты не робот

Возвращать только согласованные элементы субдокумента внутри вложенного массива

Основная коллекция - розничный торговец, который содержит массив для магазинов. Каждый магазин содержит массив предложений (вы можете купить в этом магазине). Этот массив массивов имеет массив размеров. (См. Пример ниже)

Теперь я пытаюсь найти все предложения, доступные в размере L.

{
    "_id" : ObjectId("56f277b1279871c20b8b4567"),
    "stores" : [
        {
        "_id" : ObjectId("56f277b5279871c20b8b4783"),
        "offers" : [
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "XS",
                    "S",
                    "M"
                ]
            },
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "S",
                    "L",
                    "XL"
                ]
            }
        ]
    }
}

Я попробовал этот запрос: db.getCollection('retailers').find({'stores.offers.size': 'L'})

Я ожидаю некоторый вывод вроде этого:

 {
"_id" : ObjectId("56f277b1279871c20b8b4567"),
"stores" : [
    {
        "_id" : ObjectId("56f277b5279871c20b8b4783"),
        "offers" : [
            {
                "_id" : ObjectId("56f277b1279871c20b8b4567"),
                "size": [
                    "S",
                    "L",
                    "XL"
                ]
            }
        ]
    }
}

Но вывод моего запроса содержит также несовпадающее предложение с size XS, X и M.

Как я могу заставить MongoDB возвращать только предложения, которые соответствуют моему запросу?

Приветствия и благодарности.

4b9b3361

Ответ 1

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

Реальный ответ, конечно, заключается в том, что, если вы действительно не сберегаете большую пропускную способность, отфильтровывая такие детали, вы не должны даже пытаться или, по крайней мере, превышать первое позиционное соответствие.

MongoDB имеет оператор positional $, который возвращает элемент массива по совпадающему индексу из условия запроса. Однако это возвращает только "первый" совпадающий индекс "внешнего" элемента массива.

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
)

В этом случае это означает только позицию массива "stores". Поэтому, если было несколько записей "магазинов", тогда будет возвращен только "один" из элементов, содержащих ваше согласованное условие. Но, который ничего не делает для внутреннего массива "offers", и поэтому каждое "предложение" в массиве matchd "stores" все равно будет возвращено.

MongoDB не имеет возможности "фильтровать" это в стандартном запросе, поэтому следующее не работает:

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$.offers.$': 1 }
)

Единственными инструментами, которые MongoDB на самом деле должен сделать для этого уровня манипуляции, является структура агрегации. Но анализ должен показать вам, почему вы "вероятно" не должны этого делать, а вместо этого просто фильтруйте массив в коде.


В порядке выполнения этого для каждой версии.

Сначала с MongoDB 3.2.x с помощью операции $filter:

db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$project": {
    "stores": {
      "$filter": {
        "input": {
          "$map": {
            "input": "$stores",
            "as": "store",
            "in": {
              "_id": "$$store._id",
              "offers": {
                "$filter": {
                  "input": "$$store.offers",
                  "as": "offer",
                  "cond": {
                    "$setIsSubset":  [ ["L"], "$$offer.size" ]
                  }
                }
              }
            }
          }
        },
        "as": "store",
        "cond": { "$ne": [ "$$store.offers", [] ]}
      }
    }
  }}
])

Затем с MongoDB 2.6.x и выше с $map и $setDifference:

db.getCollection('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$project": {
    "stores": {
      "$setDifference": [
        { "$map": {
          "input": {
            "$map": {
              "input": "$stores",
              "as": "store",
              "in": {
                "_id": "$$store._id",
                "offers": {
                  "$setDifference": [
                    { "$map": {
                      "input": "$$store.offers",
                      "as": "offer",
                      "in": {
                        "$cond": {
                          "if": { "$setIsSubset": [ ["L"], "$$offer.size" ] },
                          "then": "$$offer",
                          "else": false
                        }
                      }
                    }},
                    [false]
                  ]
                }
              }
            }
          },
          "as": "store",
          "in": {
            "$cond": {
              "if": { "$ne": [ "$$store.offers", [] ] },
              "then": "$$store",
              "else": false
            }
          }
        }},
        [false]
      ]
    }
  }}
])

И, наконец, в любой версии выше MongoDB 2.2.x, где была внедрена структура агрегации.

db.getCollectoin('retailers').aggregate([
  { "$match": { "stores.offers.size": "L" } },
  { "$unwind": "$stores" },
  { "$unwind": "$stores.offers" },
  { "$match": { "stores.offers.size": "L" } },
  { "$group": {
    "_id": {
      "_id": "$_id",
      "storeId": "$stores._id",
    },
    "offers": { "$push": "$stores.offers" }
  }},
  { "$group": {
    "_id": "$_id._id",
    "stores": {
      "$push": {
        "_id": "$_id.storeId",
        "offers": "$offers"
      }
    }
  }}
])

Давайте разложим пояснения.

MongoDB 3.2.x и выше

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

Простым сравнением здесь является "Имеет ли массив "size" элемент, который я ищу". В этом логическом контексте короткой задачей является использование операции $setIsSubset для сравнения массива ( "set" ) из ["L"] с целевым массивом. Если это условие true (оно содержит "L" ), тогда элемент массива для "offers" сохраняется и возвращается в результате.

На более высоком уровне $filter вы затем посмотрите, не вернул ли результат из предыдущего $filter пустой массив [] для "offers". Если он не пуст, то элемент возвращается или в противном случае он удаляется.

MongoDB 2.6.x

Это очень похоже на современный процесс, за исключением того, что в этой версии нет $filter, вы можете использовать $map для проверки каждого элемента, а затем использовать $setDifference, чтобы отфильтровать любые элементы, которые были возвращены как false.

Итак, $map собирается вернуть весь массив, но операция $cond просто решает, вернуть ли элемент или вместо этого значение false. При сравнении $setDifference с одним элементом "set" из [false] все элементы false в возвращаемом массиве будут удалены.

Во всех остальных случаях логика такая же, как и выше.

MongoDB 2.2.x и выше

Итак, ниже MongoDB 2.6 единственным инструментом для работы с массивами является $unwind, и для этого вы должны не использовать для этой цели "просто".

Процесс действительно кажется простым, просто "отделяя" каждый массив, отфильтровывая то, что вам не нужно, а затем снова объединяет его. Основное внимание уделяется "двум" $group этапам, с "первым" для повторной сборки внутреннего массива, а затем для повторной сборки внешнего массив. Существуют разные значения _id на всех уровнях, поэтому их нужно просто включить на каждом уровне группировки.

Но проблема в том, что $unwind очень дорогостоящий. Несмотря на то, что у него еще есть цель, основное предназначение использования - не делать такого рода фильтрацию на один документ. Фактически в современных версиях это использование должно быть только тогда, когда элемент массива (ов) должен стать частью самого "ключа группировки".


Заключение

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

Для этой цели должны использоваться только два современных списка, поскольку они используют "единый" этап трубопровода в дополнение к "запросу" $match, чтобы выполнить "фильтрацию". Результирующий эффект на порядок выше, чем стандартные формы .find().

В общем, эти списки по-прежнему имеют для них сложную сложность, и даже если вы действительно резко сокращаете контент, возвращаемый такой фильтрацией, таким образом, чтобы значительно улучшить пропускную способность, используемую между сервером и клиентом, тогда вы лучше фильтруете результат первоначального запроса и базовой проекции.

db.getCollection('retailers').find(
    { 'stores.offers.size': 'L'},
    { 'stores.$': 1 }
).forEach(function(doc) {
    // Technically this is only "one" store. So omit the projection
    // if you wanted more than "one" match
    doc.stores = doc.stores.filter(function(store) {
        store.offers = store.offers.filter(function(offer) {
            return offer.size.indexOf("L") != -1;
        });
        return store.offers.length != 0;
    });
    printjson(doc);
})

Таким образом, работа с обработкой запросов "post" возвращаемого объекта гораздо менее тупо, чем использование конвейера агрегации для этого. И, как утверждается, единственным "реальным" различием было бы то, что вы отбрасываете другие элементы на "сервере", а не удаляете их "на один документ" при их получении, что может сэкономить небольшую полосу пропускания.

Но если вы не делаете этого в современном выпуске с только $match и $project, тогда "стоимость" обработки на сервере значительно переведет "выигрыш" для сокращения накладные расходы сети, сначала разделив непревзойденные элементы.

Во всех случаях вы получаете тот же результат:

{
        "_id" : ObjectId("56f277b1279871c20b8b4567"),
        "stores" : [
                {
                        "_id" : ObjectId("56f277b5279871c20b8b4783"),
                        "offers" : [
                                {
                                        "_id" : ObjectId("56f277b1279871c20b8b4567"),
                                        "size" : [
                                                "S",
                                                "L",
                                                "XL"
                                        ]
                                }
                        ]
                }
        ]
}

Ответ 2

поскольку ваш массив внедрен, мы не можем использовать $elemMatch, вместо этого вы можете использовать структуру агрегации для получения ваших результатов:

db.retailers.aggregate([
{$match:{"stores.offers.size": 'L'}}, //just precondition can be skipped
{$unwind:"$stores"},
{$unwind:"$stores.offers"},
{$match:{"stores.offers.size": 'L'}},
{$group:{
    _id:{id:"$_id", "storesId":"$stores._id"},
    "offers":{$push:"$stores.offers"}
}},
{$group:{
    _id:"$_id.id",
    stores:{$push:{_id:"$_id.storesId","offers":"$offers"}}
}}
]).pretty()

что делает этот запрос, так это разматывает массивы (дважды), затем сопоставляет размер и затем преобразует документ в предыдущую форму. Вы можете удалить шаги $group и посмотреть, как они печатаются. Веселитесь!