Повертає лише відповідні елементи піддокументу у вкладеному масиві


78

Основна колекція - це роздрібний торговець, який містить масив для магазинів. Кожен магазин містить безліч пропозицій (ви можете придбати в цьому магазині). Цей масив пропозицій має масив розмірів. (Див. Приклад нижче)

Зараз я намагаюся знайти всі пропозиції, які доступні за розміром 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"
                ]
            }
        ]
    }
}

Але результат мого запиту містить також невідповідну пропозицію з sizeXS, X та M.

Як я можу змусити MongoDB повертати лише ті пропозиції, які відповідають моєму запиту?

Вітаю та дякую.


Ви мали на увазі щось подібне? db.getCollection('retailers').find({'stores.offers.size': 'L'}, {'stores.offers': 1}). Але тоді відповідь також містить неправильні пропозиції
Віко

Чи слід використовувати агрегат з моєю проблемою $matchта $unwindдля її вирішення?
Віко

Відповіді:


140

Отже, запит, який ви маєте, фактично вибирає "документ", як слід. Але те, що ви шукаєте, - це "відфільтрувати масиви", що містяться таким чином, щоб повернуті елементи відповідали лише умові запиту.

Дійсна відповідь, звичайно, полягає в тому, що якщо ви дійсно не економите багато пропускної здатності, відфільтрувавши такі деталі, тоді вам навіть не слід намагатися, або, принаймні, після першого позиційного збігу.

MongoDB має позиційний $оператор, який поверне елемент масиву за відповідним індексом із умови запиту. Однак це повертає лише "перший" відповідний індекс "зовнішнього" елемента масиву.

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

У цьому випадку це означає "stores"лише позицію масиву. Отже, якщо було декілька записів "зберігання", тоді буде повернено лише "один" з елементів, що містили ваш відповідний стан. Але це нічого не робить для внутрішнього масиву "offers", і як такої кожна "пропозиція" у збіжному "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.getCollection('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операції для порівняння масиву ("набору") з ["L"]цільовим масивом. Там, де ця умова є true(вона містить "L"), тоді елемент масиву для "offers"зберігається і повертається в результаті.

На вищому рівні $filterви шукаєте, чи не $filterповернув результат із попереднього порожнього масиву []для "offers". Якщо він не є порожнім, тоді елемент повертається або інакше він видаляється.

MongoDB 2.6.x

Це дуже схоже на сучасний процес, за винятком того, що $filterв цій версії немає такого, що ви можете використовувати $mapдля перевірки кожного елемента, а потім використовувати $setDifferenceдля фільтрації будь-яких елементів, які були повернуті як false.

Так $mapзбирається повернути весь масив, але $condоперація просто вирішує, повертати елемент або замість цього falseзначення. При порівнянні з $setDifferenceодним елементом буде видалено "набір" [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);
})

Отже, робота з оброблюваним запитом поверненого об'єкта "пост" набагато менш тупа, ніж використання конвеєра агрегації для цього. І як зазначено, єдиною "справжньою" різницею буде те, що ви відкидаєте інші елементи на "сервері", на відміну від видалення їх "за документ" при отриманні, що може заощадити невелику пропускну здатність.

Але якщо ви не робите це в сучасному випуску лише з $match і $project, тоді "вартість" обробки на сервері значно перевершить "виграш" зменшення цих накладних витрат на мережу, позбавляючи спочатку неперевершені елементи.

У всіх випадках ви отримуєте однаковий результат:

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

1
Я реалізував щось дуже подібне до цього (з тією різницею, що мені потрібно було знайти точну відповідність масиву за "розміром" у цьому прикладі), і насправді це не ефективно лише з кількома документами (не 1000 або мільйонами), беручи більше 5 секунд для обчислення. Побачимо, чи постійна обробка буде більш ефективною.
від

1
або що, якщо розмір зберігається як окрема колекція замість вкладеного масиву? не покращило б продуктивність, одночасно надаючи дивовижні можливості запитів
PirateApp

Дякую, ключовим для мене є оператор "розмотування". Тепер я зрозумів.
Стефано Скарпанті

Спасибі за вашу відповідь! Це мені також допомогло :)
Ендрю Т

12

оскільки ваш масив вбудований, ми не можемо використовувати $ 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 і подивитися, як вони друкуються. Мати задоволення!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.