将 $geoNear 与另一个集合组合

Posted

技术标签:

【中文标题】将 $geoNear 与另一个集合组合【英文标题】:Combine $geoNear with Another Collection 【发布时间】:2017-11-28 06:19:30 【问题描述】:

我有 2 个集合,restomeal(每个膳食文档都有它所属的恢复 id)。我想获取附近至少有 1 顿饭的餐厅。现在,我可以找到附近的餐馆,但我如何结合起来确保他们至少有一顿饭?

restoModel.aggregate([
    "$geoNear": 
        "near": 
            "type": "Point",
            "coordinates": coordinates
        ,
        "minDistance": 0,
        "maxDistance": 1000,
        "distanceField": "distance",
        "spherical": true,
        "limit": 10 // fetch 10 restos at a time
    
]);

样本恢复文档:

 
  _id: "100", 
  location:  coordinates: [ -63, 42 ], type: "Point" ,
  name: "Burger King"

餐单样本:

 
  resto_id: "100", // restaurant that this meal belongs to
  name: "Fried Chicken",
  price: 12.99

我可以创建一个管道,获取 10 家餐厅,每个餐厅都与其相关联的用餐文件相连,并删除没有用餐的餐厅。但是如果所有文件都没有吃饭,那么一次 fetch 可能会返回 0 个文件。我如何确保它一直在搜索,直到返回 10 个用餐餐厅?

【问题讨论】:

这需要更多上下文来显示其他集合的详细信息。您可以使用$geoNear 有效地将“距离字段”“投影”到与查询点“最近”的搜索结果中。它还有一个“查询”选项,可以在返回的“有限”结果中考虑给定的标准。但是,您所问的问题可能也需要$lookup,因此这里的上下文很重要。如果您实际上拥有 MongoDB 3.4,那么还有另一个好处/可能性。因此,请详细说明您问题中的细节。请提供小样本文件。 【参考方案1】:

这实际上有一些方法需要考虑,它们各有优缺点。

嵌入

最干净和最简单的方法是在餐厅的父文档中实际嵌入“菜单”和“计数”。

这实际上也很合理,因为您似乎停留在关系建模术语中,其中 MongoDB 不是 RDBMS,也不“应该”通常将其用作一个。相反,我们会发挥 MongoDB 的优势。

结构会是这样的:

 
  _id: "100", 
  location:  coordinates: [ -63, 42 ], type: "Point" ,
  name: "Burger King",
  menuCount: 1,
  menu: [
    
      name: "Fried Chicken",
      price: 12.99
    
  ]

这实际上查询起来非常简单,实际上我们可以简单地使用常规的$nearSphere 来申请,因为我们真的不需要聚合条件:

restoModel.find(
  "location": 
    "$nearSphere": 
      "$geometry": 
        "type": "Point",
        "coordinates": coordinates
      ,
      "$maxDistance": 1000
    
  ,
  "menuCount":  "$gt": 1 
).skip(0).limit(10)

简单有效。这实际上正是您应该使用 MongoDB 的原因,因为“相关”数据已经嵌入到父项中。这当然有“权衡”,但最大的优势在于速度和效率。

在父项中维护菜单项以及当前计数也很简单,因为我们可以在添加新项时简单地“增加”计数:

restoModel.update(
   "_id": id, "menu.name":  "$ne": "Pizza"  ,
  
    "$push":  "menu":  "name": "Pizza", "price": 19.99  ,
    "$inc":  "menuCount": 1 
  
)

在一个原子操作中添加尚不存在的新项并增加菜单项的数量,这也是您嵌入关系的另一个原因,其中更新同时对父项和子项都有影响时间。

这确实是您应该追求的。当然,您实际可以嵌入的内容是有限制的,但这只是一个“菜单”,与我们可以定义的其他类型的关系相比,它的大小当然相对较小。

MongoDB 的 Elliot 实际上说得最好 “战争与和平的全部内容作为文本适合在 4MB 内”,当时 BSON 文档的限制是 4MB。现在它有 16MB 的大小,足以处理大多数客户可能会费心浏览的任何“菜单”。


使用 $lookup 聚合

如果您要保持标准的关系模式,就会有一些问题需要克服。大多数情况下,与“嵌入”的最大区别在于,由于“菜单”的数据在另一个集合中,那么您需要$lookup 才能“拉入”这些数据,然后“计算”有多少。

关于“最近”查询,与上面的示例不同,我们不能将这些附加约束“放在“最近”查询本身中”,这意味着在默认的 100 个结果中返回$geoNear,某些项目“可能不”满足附加约束,您别无选择,只能稍后应用,“在”$lookup 执行后:

restoModel.aggregate([
   "$geoNear": 
    "near": 
      "type": "Point",
      "coordinates": coordinates
    ,
    "spherical": true,
    "limit": 150,
    "distanceField": "distance",
    "maxDistance": 1000
  ,
   "$lookup": 
     "from": "menuitems",
     "localField": "_id",
     "foreignField": "resto_id",
     "as": "menu"
  ,
   "$redact": 
    "$cond": 
      "if":  "$gt": [  "$size": "$menu" , 0 ] ,
      "then": "$$KEEP",
      "else": "$$PRUNE"
    
  ,
   "$limit": 10 
])

因此,您在这里唯一的选择是“增加”“可能”返回的数量,然后执行额外的管道阶段以“加入”、“计算”和“过滤”。还将最终的 $limit 留给它自己的管道阶段。

这里一个值得注意的问题是结果的“分页”。这是因为“下一页”本质上需要“跳过”前一页的结果。为此,最好实现一个“转发分页”的概念,就像这篇文章中描述的那样:Implementing Pagination In MongoDB

一般的想法是通过$nin“排除”先前“看到”的结果。这实际上是可以使用$geoNear"query" 选项来完成的:

restoModel.aggregate([
   "$geoNear": 
    "near": 
      "type": "Point",
      "coordinates": coordinates
    ,
    "spherical": true,
    "limit": 150,
    "distanceField": "distance",
    "maxDistance": 1000,
    "query":  "_id":  "$nin": list_of_seen_ids  
  ,
   "$lookup": 
     "from": "menuitems",
     "localField": "_id",
     "foreignField": "resto_id",
     "as": "menu"
  ,
   "$redact": 
    "$cond": 
      "if":  "$gt": [  "$size": "$menu" , 0 ] ,
      "then": "$$KEEP",
      "else": "$$PRUNE"
    
  ,
   "$limit": 10 
])

那么至少你不会得到与上一页相同的结果。但与前面所示的嵌入式模型相比,它的工作量要多一些,而且要多得多。


结论

一般情况导致“嵌入”成为此用例的更好选择。您有“少量”相关项目,并且数据实际上与父级直接关联更有意义,因为通常您需要同时获得菜单和餐厅信息。

MongoDB 自 3.4 起的现代版本确实允许"view" to be created,但一般前提是基于聚合管道的使用。因此,我们可以在“视图”中“预连接”数据,但是由于任何查询操作都有效地拾取底层聚合管道语句来处理,因此标准查询运算符 $nearSphere 等不能作为标准应用查询实际上是“附加”到定义的管道中。以类似的方式,您也不能将$geoNear 与“视图”一起使用。

也许约束在未来会发生变化,但目前的限制使得这个选项不可行,因为我们无法在“预连接”源上执行所需的查询,并采用更相关的设计。

因此,您基本上可以通过所介绍的两种方式中的任何一种来完成,但为了我的钱,我会在此处建模为嵌入。

【讨论】:

感谢您非常详细和彻底的回答。我采用第一种方法。

以上是关于将 $geoNear 与另一个集合组合的主要内容,如果未能解决你的问题,请参考以下文章

将数组元素与另一个数组合并,该数组是 mongo 集合的一部分

在一个集合中查找与另一个集合中的数字相加的一组数字

SwiftUI,Firestore 获取一个集合并与另一个集合匹配

如何将集合的字段与另一个集合的数组内的字段匹配

将字符串转换为集合以与另一个集合进行比较

如何对嵌入式文档进行 $geoNear 聚合?