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

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

现在,我尝试查找大小为 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 仅返回与我的查询匹配的优惠?

问候和感谢。

最佳答案

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

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

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 5:67920

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"] 的数组(“set”)与目标数组进行比较。如果条件是 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 的现代版本中执行此操作,否则在服务器上处理的“成本”将大大超过通过首先剥离不匹配元素来减少网络开销的“ yield ”。

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

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

https://stackoverflow.com/questions/36229123/

相关文章:

performance - MongoDB 'count()' 非常慢。我们如何改进/解决它?

mongodb - 服务器在 SASL 身份验证步骤 : Authentication failed

mongodb - meteor :如何备份我的 mongo 数据库

mongodb - 将 MongoDB 集合的子集保存到另一个集合

mysql - 将数据库从 mysql 转换为 mongoDb

mongodb - 如何使用 docker-compose 为 mongo 数据库播种?

mongodb - 如何在 MongoDB 中重命名集合?

node.js - 为什么建议不要在 Node.js 代码的任何地方关闭 MongoDB 连接?

mongodb - 在 mongodb 中使用 findOne 获取具有最大 id 的元素

performance - Mongodb聚合框架比map/reduce更快吗?