将$ geoNear与Another Collection结合使用

时间:2017-06-25 01:03:07

标签: javascript node.js mongodb mongoose aggregation-framework

我有2个集合,restomeal(每个膳食文档都有它所属的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
    }
}]);

样本resto doc:

{ 
  _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餐餐厅?

1 个答案:

答案 0 :(得分:2)

这实际上有几种方法需要考虑,这些方法有其自身的利益或陷阱。

嵌入

最干净,最简单的方法就是实际嵌入"菜单"和"计数"而是在餐厅的父文件中。

这实际上也是非常合理的,因为你似乎陷入了关系建模术语的思考,其中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的原因,因为"相关"数据已嵌入父项中。当然还有#34;权衡"对此,但最大的优势在于速度和效率。

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

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留给它自己的管道阶段。

这里一个值得注意的问题是" paging"结果这是因为"下一页"需要基本上"跳过"前一页的结果。为此,最好实现"前向寻呼"概念,就像这篇文章中描述的那样: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 }
])

然后至少你没有得到与上一页相同的结果。但它的工作量要多一些,而且工作量远远超过前面所示的嵌入式模型。

结论

一般情况导致"嵌入"作为此用例的更好选择。你有一个"小"相关项目的数量,实际上与父母直接相关的数据更有意义,因为通常你想要菜单和餐馆信息。

自3.4以来MongoDB的现代版本允许"view" to be created,但一般前提是基于聚合管道的使用。因此我们可以预先加入"但是,由于任何查询操作都有效地获取要处理的基础聚合管道语句,因此无法应用$nearSphere等标准查询运算符,因为标准查询是实际上"追加"到定义的管道。以类似的方式,您也无法使用$geoNear和#34;视图"。

也许这些限制将来会发生变化,但是现在这些限制使得这个选项不可行,因为我们无法在"预先加入"具有更多关系设计的来源。

所以你基本上可以用两种方式中的任何一种来做,但是为了我的钱,我会在这里模拟嵌入。