将$geoNear与另一个集合结合使用

5

我有两个集合,restomeal(每个餐点文档都有所属的餐厅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
    }
}]);

示例餐厅文档:

{ 
  _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家餐厅及其关联的菜单文档,并删除没有菜单的餐厅。但是,如果所有餐厅都没有菜单,单次获取可能会返回0个文档。我如何确保它一直搜索,直到返回有10个菜品的餐厅为止?


这需要更多的上下文来展示其他集合的细节。您可以使用 $geoNear 将“distanceField”有效地“投影”到最接近查询点的搜索结果中。它还具有“query”选项,其中给定的条件可以在返回的“限制”结果中考虑。然而,您所询问的可能还需要 $lookup,因此上下文在这里很重要。如果您实际上拥有 MongoDB 3.4,则还有另一个好处/可能性。因此,请详细说明您的问题中的细节。请提供一些小样本文档。 - Neil Lunn
1个回答

3
这实际上有几种方法可供考虑,每种方法都有其自身的优点或缺点。

嵌入

最清晰和最简单的方法是将“菜单”和“数量”直接嵌入餐厅父文档中。
这实际上也很合理,因为您似乎被困在关系建模术语中,而MongoDB不是关系型数据库管理系统,通常也不应该作为一个。相反,我们发挥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 将其“提取”,然后“计数”。

与“最近”的查询相关的是,不像上面的示例,我们不能将这些额外的限制条件 “放在‘near’查询本身内部”,这意味着在 $geoNear 返回的默认 100 个结果中,有些项目“可能不符合”附加约束条件,您只能在 $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留给它自己的管道阶段。
一个显著的问题在于结果的“分页”。这是因为“下一页”需要基本上“跳过”先前页面的结果。为此,最好实现“向前翻页”的概念,就像在这篇文章中所描述的那样:在MongoDB中实现分页 总体思路是通过$nin“排除”之前“查看”的结果。这实际上是可以使用$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 }
])

至少这样做可以避免得到与上一页相同的结果。但这需要更多的工作量,比之前展示的嵌入模型所需的工作量要大得多。


结论

一般情况下,"嵌入式"是这种情况的更好选择。您有一个“小”的相关项,数据实际上与父项直接关联,通常您希望同时获取菜单和餐厅信息。

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

也许将来约束条件会改变,但现在由于我们无法对“预连接”的源使用更具关系型设计的必需查询,所以这不可行作为选项。因此,您可以基本上采用其中任一种方式进行操作,但我个人认为最好是像这里嵌入式建模。

非常感谢您详细和全面的回答。我会采取第一种方法。 - Maria

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接