必须用$geonear作为聚合第一阶段输出距离字段,再$match限距、$group网格聚类;网格化需用$floor量化经纬度生成gridkey,避免直接$avg坐标导致中心偏移。

必须用 $geoNear 阶段启动聚合,不能只靠 $near
直接在 find() 中用 $near 只能排序+限距,无法输出距离字段、无法后续分组聚类。真正做热点聚合,必须进聚合管道,且 $geoNear 必须是第一个阶段——MongoDB 强制要求,否则报错 "$geoNear must be the first stage"。
常见错误是先 $match 再 $geoNear,结果直接失败。正确顺序只能是:
-
$geoNear(带distanceField输出距离) - 接着
$match过滤有效距离范围(比如只保留 5km 内的点) - 再
$group或网格化处理
用 $bucket 或 $floor 手动做地理网格聚类,别信“自动聚类”
MongoDB 没有内置的“按密度聚类”操作符。所谓“热点”,本质是把地理空间离散成固定大小的格子(比如 0.01° 经纬度 ≈ 1km),再统计每格内文档数。两种主流做法:
- 用
$bucket对calculatedDistance分桶 —— 适合按“距中心点远近”分层,但不是空间热点 - 更实用的是对坐标做量化:用
$floor把coordinates.0(经度)和coordinates.1(纬度)分别除以格子精度后取整,生成gridKey
示例(1km 网格):
{ $addFields: {
gridKey: {
lon: { $floor: { $divide: ["$location.coordinates.0", 0.01] } },
lat: { $floor: { $divide: ["$location.coordinates.1", 0.01] } }
}
} }
之后就能 $group 按 gridKey 统计总数、求和字段(如 subpopulations.age.6_11)。
2dsphere 索引不是万能的,$geoNear 性能依赖查询范围
$geoNear 虽快,但若不设 maxDistance,它会扫描全量索引节点,响应时间随集合规模线性恶化。尤其当请求全球范围(top=90, bottom=-90)时,基本不可用。
- 必须传
maxDistance(单位米),且值要合理:地图缩放级别越高,值越小 - 配合前端 Leaflet 的
map.getBounds()动态算出矩形范围,再转为$box+$geoWithin预过滤,能大幅减少$geoNear输入量 - 注意:
$geoNear不支持$geoWithin同查,得拆成两步或用$match+$geometry先圈区域
聚合结果里经纬度平均值不准?别直接 $avg 坐标
对经纬度数组直接用 $avg 求中心点,在跨本初子午线或高纬度地区会严重偏移(比如东京+旧金山的平均经度≈0°,实际是太平洋中间)。真实热点中心应是加权质心或最小包围圆。
- 简单修正:用
$push收集所有coordinates,再在应用层用 spherical centroid 算法计算(如 Turf.js 的turf.centroid) - 硬要在 MongoDB 做:需转为笛卡尔三维坐标(
cosφ·cosλ, cosφ·sinλ, sinφ),$avg后再反推回经纬度——但性能差,不推荐 - 多数场景下,用网格左上角作为
clusterLocation已足够用于前端渲染
真正卡住的往往是网格精度选择和前端缓存策略——同一区域反复请求不同缩放级别时,服务端不做格子尺寸动态适配,就会出现热点跳变或空白块。











