仅返回嵌套数组中匹配的子文档元素


76

主要集合是零售商,其中包含商店数组。每个商店都包含一系列优惠(您可以在此商店中购买)。此提供的数组具有大小的数组。(请参见下面的示例)

现在,我尝试查找所有可用尺寸的报价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})。但随后,响应中也包含错误的报价
-Vico

我应该使用的骨料$match$unwind我的问题?
维科

Answers:


135

因此,您实际上已经在查询中选择了“文档”。但是,您要查找的是“过滤包含的数组”,以便返回的元素仅与查询条件匹配。

真正的答案当然是,除非您通过过滤掉这些细节确实节省了很多带宽,否则您甚至不应该尝试,或者至少不要超过第一个位置匹配。

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",以examime和$filter该内容。

这里的简单比较是"size"数组是否包含我要查找的元素”。在此逻辑上下文中,要做的简短事情是使用该$setIsSubset操作将一个数组(“集合”)["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);
})

因此,与使用聚合管道执行此操作相比,使用返回的对象“后”查询处理要轻松得多。如前所述,唯一的“真实”差异是您要丢弃“服务器”上的其他元素,而不是在接收到“每个文档”时将其删除,这样可以节省一点带宽。

但是,除非您在仅包含 $match和的现代版本中执行此操作,否则$project服务器上处理的“成本”将大大超过通过首先剥离不匹配的元素来减少网络开销的“收益”。

在所有情况下,您都会得到相同的结果:

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

1
我已经实现了与此非常相似的东西(不同之处在于,在此示例中,我需要找到与“大小”完全匹配的数组),并且实际上仅使用几个文档(不是1000个或数百万个),效率就不高超过5秒进行计算。将查看后处理是否会更有效。
dter

1
还是将大小作为单独的集合而不是嵌套数组来维护呢?不会在提供出色查询功能的同时提高性能
PirateApp

谢谢,对我来说,关键是“放松”操作员。现在我明白了。
Stefano Scarpanti

谢谢您的回答!它也对我有帮助:)
Andrew T

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步骤并查看其打印方式。玩得开心!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.