1. ホーム
  2. mongodb

[解決済み] ネストされた配列の中で、マッチしたサブドキュメントの要素のみを返す

2022-02-14 17:50:18

質問

メインコレクションは小売業者で、店舗用の配列が含まれています。各店舗には、オファー (この店舗で購入できるもの) の配列が含まれています。このオファー配列は、サイズの配列を持っています。(以下の例を参照ください)

次に、このサイズで利用可能なすべてのオファーを見つけようとします。 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に返すように強制するにはどうしたらいいですか?

はじめまして、ありがとうございます。

解決方法は?

つまり、あなたが持っているクエリは、実際にその通りに "document" を選択するのです。しかし、あなたが求めているのは、クエリの条件に一致する要素のみが返されるように、含まれる配列にフィルタをかけることです。

本当の答えは、もちろん、そのような詳細な情報をフィルタリングすることで本当に多くの帯域幅を節約しているのでなければ、試すべきでもなく、少なくとも最初の位置の一致を超えるものでなければならないということです。

MongoDBには ポジション $ 演算子 を使用すると、クエリ条件にマッチしたインデックスの配列要素を返します。ただし、これは配列の一番外側の要素にマッチした最初のインデックスを返すだけです。

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

この場合は "stores" の配列位置のみです。つまり、もし複数の "stores" があった場合、マッチした条件を含む要素のうち "one" だけが返されることになるのです。 しかし の内側の配列に対しては何もしません。 "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 この目的のためだけに ない は、この目的のためにアグリゲーションフレームワークを使用します。

手順としては、各配列を分解し、不要なものをフィルタリングして、元に戻すだけなので、確かに簡単なように見えます。主な注意点は、quot;two".にあります。 $group の段階で、内側の配列を再構築し、次の段階で外側の配列を再構築します。また _id の値はすべてのレベルに存在するので、これらはグループ化の各レベルに含まれる必要があるだけです。

しかし、問題なのは $unwind 非常にコストがかかる . それでも目的はあるのですが、主な使用目的は、ドキュメントごとにこの種のフィルタリングを行うことではありません。実際、最近のリリースでは、配列の要素がグループ化キーの一部となる必要がある場合にのみ使用されるはずです。


まとめ

このように配列の複数のレベルでマッチを取得するのは簡単な処理ではありませんし、実際、次のようなことが起こり得ます。 非常に高いコスト を実装しています。

この目的のために使用すべきなのは、2つの最新のリストだけです。これらのリストでは、quot;クエリに加えてquot;シングル"パイプラインステージが採用されているからです。 $match を行うために、quot;filtering" を行います。その結果、標準的なフォームの .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);
})

つまり、返されたオブジェクトをquot;post;クエリ処理することは、集約パイプラインを使用してこれを行うよりもはるかに難解ではありません。また、前述のとおり、唯一の違いは、他の要素を受信時にドキュメントごとに削除するのではなく、quot;サーバーで破棄していることです。

しかし、最新のリリースでこれを行うのでなければ のみ $match そして $project の場合、サーバーでの処理にかかるコストは、一致しない要素を最初に取り除くことでネットワークのオーバーヘッドを削減することによる利益を大きく上回ることになります。

どのような場合でも、同じ結果になります。

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