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

Posted

技术标签:

【中文标题】仅返回嵌套数组中匹配的子文档元素【英文标题】:Return only matched sub-document elements within a nested array 【发布时间】:2016-07-13 18:02:15 【问题描述】:

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

现在我尝试查找所有尺寸为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 只返回与我的查询匹配的报价?

问候和感谢。

【问题讨论】:

你的意思是这样的吗? db.getCollection('retailers').find('stores.offers.size': 'L', 'stores.offers': 1)。但随后响应也包含错误的报价 我应该使用$match$unwind 的聚合来解决我的问题吗? 【参考方案1】:

由于您的数组是嵌入的,我们不能使用 $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 步骤并查看它的打印方式。 玩得开心!

【讨论】:

【参考方案2】:

因此,您的查询实际上选择了“文档”,就像它应该的那样。但是您正在寻找的是“过滤包含的数组”,以便返回的元素仅匹配查询的条件。

真正的答案当然是,除非你真的通过过滤掉这些细节来节省大量带宽,否则你甚至不应该尝试,或者至少在第一次位置匹配之后。

MongoDB 有一个positional $ operator,它将在查询条件的匹配索引处返回一个数组元素。但是,这只会返回“最外层”数组元素的“第一个”匹配索引。

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 是这里的方式,因为它的设计考虑了目的。由于数组有多个级别,因此您需要在每个级别上应用它。因此,首先您要深入了解"stores" 中的每个"offers" 以检查和$filter 该内容。

这里的简单比较是"size" 数组中是否包含我要查找的元素”。在这个逻辑上下文中,要做的简短的事情是使用$setIsSubset 操作将["L"] 的数组(“集合”)与目标数组进行比较。如果条件是 true(它包含 "L" ),则 "offers" 的数组元素将被保留并在结果中返回。

在更高级别的$filter 中,您将查看前一个$filter 的结果是否为"offers" 返回了一个空数组[]。如果不为空,则返回该元素,否则将其删除。

MongoDB 2.6.x

这与现代流程非常相似,只是由于此版本中没有$filter,因此您可以使用$map 检查每个元素,然后使用$setDifference 过滤掉返回为@987654357 的任何元素@。

所以$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);
)

因此,使用返回的对象“post”查询处理远没有使用聚合管道执行此操作那么迟钝。如前所述,唯一的“真正”区别是您将丢弃“服务器”上的其他元素,而不是在收到时“按文档”删除它们,这可能会节省一点带宽。

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

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


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

【讨论】:

我已经实现了一些与此非常相似的东西(不同之处在于我需要在这个例子中找到一个与“size”完全匹配的数组)并且它实际上只用几个文档就没有效率(不是 1000 或数百万),计算时间超过 5 秒。看看后处理是否会更有效。 或者如果 size 被维护为一个单独的集合而不是一个嵌套数组呢?在提供惊人的查询功能的同时不会提高性能 谢谢,对我来说关键是“展开”运算符。现在我明白了。 感谢您的回答!它也帮助了我:) 现在它在 Mongo 4.4 中不起作用 db.inventory.find( , size: 1, "size.uom": 1 ) // 从 4.4 开始无效 Ref - docs.mongodb.com/manual/release-notes/4.4-compatibility 【参考方案3】:

它也可以在没有聚合的情况下使用。 这里是解决方案链接:https://mongoplayground.net/p/Q5lxPvGK03A

db.collection.find(
  "stores.offers.size": "L"
,

  "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通过***属性和嵌套数组键查找文档,并返回匹配文档的一部分

mongoose#populate 在数组内的嵌套对象中返回 null

返回嵌套数组中的数组元素