将 $geoNear 与另一个集合组合
Posted
技术标签:
【中文标题】将 $geoNear 与另一个集合组合【英文标题】:Combine $geoNear with Another Collection 【发布时间】:2017-11-28 06:19:30 【问题描述】:我有 2 个集合,resto
和 meal
(每个膳食文档都有它所属的恢复 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 集合的一部分