web-dev-qa-db-ja.com

ネストされた配列内の一致したサブドキュメント要素のみを返します

主なコレクションは小売店で、店舗用の配列が含まれています。各ストアにはオファーの配列が含まれています(このストアで購入できます)。これは、配列にサイズの配列があります。 (以下の例を参照)

次に、サイズ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に返すように強制するにはどうすればよいですか?

挨拶と感謝。

55
Vico

したがって、あなたが持っているクエリは、実際に「ドキュメント」を選択するはずです。しかし、探しているのは、返された要素がクエリの条件にのみ一致するように、含まれる「配列をフィルタリングする」ことです。

本当の答えは、もちろん、そのような詳細を除外することによって本当に多くの帯域幅を節約しているのでなければ、少なくとも最初の位置の一致を超えて試すべきではないということです。

MongoDBには、クエリ条件から一致したインデックスの配列要素を返す positional $ operator があります。ただし、これは「最も外側の」配列要素の「最初の」一致インデックスのみを返します。

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

この場合、"stores"配列の位置のみを意味します。そのため、複数の「ストア」エントリがある場合、一致した条件を含む要素の「1」のみが返されます。 But、これは"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 であり、この目的のためだけにnotこの目的のために「ちょうど」集約フレームワークを使用します。

実際には、各配列を単に「分解」し、不要なものをフィルタリングしてから元に戻すことで、プロセスは単純に見えます。主な注意点は、「2つ」 $group ステージで、「最初」は内部アレイを再構築し、次は外部アレイを再構築します。すべてのレベルに個別の_id値があるため、これらはすべてのグループ化レベルに含める必要があります。

しかし、問題は$unwind非常にコストがかかることです。まだ目的はありますが、主な使用目的は、ドキュメントごとにこの種のフィルタリングを行わないことです。実際、最新のリリースでは、配列の要素が「グループ化キー」自体の一部になる必要がある場合にのみ使用する必要があります。


結論

したがって、このような配列の複数のレベルで一致を取得するのは簡単なプロセスではなく、実際には、正しく実装されていないと非常にコストがかかる可能性があります.

「フィルタリング」を行うために「クエリ」$matchに加えて「単一」パイプラインステージを使用するため、この目的には2つの最新リストのみを使用する必要があります。結果として得られる効果は、.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);
})

そのため、返されたオブジェクトの「ポスト」クエリ処理での作業は、これを行うために集約パイプラインを使用するよりもはるかに鈍くなりません。前述のように、唯一の「本当の」違いは、受信時に「ドキュメントごとに」それらを削除するのではなく、「サーバー」上の他の要素を破棄することです。

ただし、only$matchおよび$projectを使用した最新のリリースでこれを行っている場合を除き、サーバーでの処理の「コスト」は「ゲイン」を大きく上回ります。一致しない要素を最初に除去することにより、ネットワークのオーバーヘッドを削減します。

いずれの場合も、同じ結果が得られます。

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

配列が埋め込まれているため、$ 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()

このクエリは、配列を巻き戻し(2回)、サイズを一致させてから、ドキュメントを前の形式に変形します。 $ groupステップを削除して、印刷方法を確認できます。楽しむ!

9
profesor79