将 $geoNear 与另一个集合结合起来

Mar*_*ria 5 javascript mongoose mongodb node.js aggregation-framework

我有 2 个集合,resto并且meal(每个膳食文档都有其所属的 Resto id)。我想去附近至少有一顿饭的餐馆。现在,我可以获取附近的餐馆,但我如何组合才能确保他们至少吃一顿饭?

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

示例恢复文档:

{ 
  _id: "100", 
  location: { coordinates: [ -63, 42 ], type: "Point" },
  name: "Burger King"
}
Run Code Online (Sandbox Code Playgroud)

膳食文件样本:

{ 
  resto_id: "100", // restaurant that this meal belongs to
  name: "Fried Chicken",
  price: 12.99
}
Run Code Online (Sandbox Code Playgroud)

我可以创建一个管道,获取 10 家餐厅,每家餐厅都加入了相关的用餐文档,并删除不提供餐食的餐厅。但是,如果所有文档都没有吃饭,则一次获取可能会返回 0 个文档。我如何确保它继续搜索,直到返回 10 个用餐餐厅?

Nei*_*unn 3

这实际上有几种方法需要考虑,这些方法都有各自的好处或相关的陷阱。

嵌入

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

这实际上也是相当合理的,因为您似乎陷入了关系建模术语的思考中,其中 MongoDB 不是 RDBMS,而且通常也不“应该”将其用作 RDBMS。相反,我们发挥 MongoDB 的优势。

那么结构将是这样的:

{ 
  _id: "100", 
  location: { coordinates: [ -63, 42 ], type: "Point" },
  name: "Burger King",
  menuCount: 1,
  menu: [
    {
      name: "Fried Chicken",
      price: 12.99
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

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

restoModel.find({
  "location": {
    "$nearSphere": {
      "$geometry": {
        "type": "Point",
        "coordinates": coordinates
      },
      "$maxDistance": 1000
    }
  },
  "menuCount": { "$gt": 1 }
}).skip(0).limit(10)
Run Code Online (Sandbox Code Playgroud)

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

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

restoModel.update(
  { "_id": id, "menu.name": { "$ne": "Pizza" } },
  {
    "$push": { "menu": { "name": "Pizza", "price": 19.99 } },
    "$inc": { "menuCount": 1 }
  }
)
Run Code Online (Sandbox Code Playgroud)

这会在尚不存在的位置添加新项目,并增加菜单项的数量,所有这些都在一个原子操作中完成,这也是您嵌入更新同时影响父级和子级的关系的另一个原因。

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

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 }
])
Run Code Online (Sandbox Code Playgroud)

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

这里值得注意的问题是结果的“分页”。这是因为“下一页”本质上需要“跳过”前一页的结果。为此,最好实现“前向分页”概念,正如本文中所述:在 MongoDB 中实现分页

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

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 }
])
Run Code Online (Sandbox Code Playgroud)

那么至少你不会得到与上一页相同的结果。但这需要更多的工作,而且比前面所示的嵌入式模型可以完成的工作要多得多。


结论

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

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

也许约束将来会改变,但现在的限制使得这作为一种选择不可行,因为我们无法使用更相关的设计对“预连接”源执行所需的查询。

因此,您基本上可以通过所提供的两种方式中的任何一种来完成此操作,但为了我的钱,我将建模为嵌入此处。