「地理空间搜索:从坐标到地图」

讲解 ES 地理空间搜索:geo_point 与 geo_shape 数据类型、距离查询与距离排序、边界框与多边形过滤、地理聚合,以及 Kibana Maps 可视化对接。

「附近的门店」「三公里内的外卖」「按距离排序」是搜索服务的常见需求。Elasticsearch 提供 geo_point 与 geo_shape 两套地理数据类型,配合距离查询、地理排序与地理聚合,能支撑从坐标检索到地图可视化的完整链路。本文覆盖地理数据建模、查询、聚合与可视化。

1. 地理数据类型

一句话总结: geo_point 表示点坐标,geo_shape 表示多边形区域,两者决定后续能做什么样的查询。

1.1 geo_point 映射

{
  "mappings": {
    "properties": {
      "location": { "type": "geo_point" },
      "delivery_zone": { "type": "geo_shape" }
    }
  }
}

geo_point 存经纬度点,支持距离查询、距离排序与边界框过滤;geo_shape 存点线面,支持多边形相交、包含等空间关系判断。门店坐标用 geo_point,配送范围用 geo_shape,两者常常同文档共存。

1.2 写入坐标的三种形式

{ "location": { "lat": 39.915, "lon": 116.404 } }

{ "location": "39.915,116.404" }

{ "location": [116.404, 39.915] }

geo_point 接受对象、字符串(纬度,经度)与数组(经度,纬度)三种形式。数组形式必须遵守「经度在前」的 GeoJSON 约定,与直觉相反,写入前务必确认。字符串形式的逗号分隔也是纬度在前,与数组相反,混用最容易写错。

1.3 geo_shape 与 GeoJSON

{
  "delivery_zone": {
    "type": "polygon",
    "coordinates": [
      [
        [116.3, 39.8], [116.6, 39.8],
        [116.6, 40.1], [116.3, 40.1], [116.3, 39.8]
      ]
    ]
  }
}

geo_shape 遵循 GeoJSON 规范:polygon 坐标是经度在前、闭合的环。多边形必须封闭(首尾点相同),带洞的多边形用两个环表达。geo_shape 索引默认基于四叉树/前缀树做空间索引,精度参数 precision 影响索引大小与查询精度。

2. 距离查询

一句话总结: geo_distance 按指定中心与半径筛出范围内文档,是「附近」检索的基础。

2.1 geo_distance 查询

{
  "query": {
    "geo_distance": {
      "distance": "3km",
      "location": { "lat": 39.915, "lon": 116.404 }
    }
  }
}

geo_distance 以中心点为圆心、distance 为半径做圆形范围过滤。distance 支持 m/km/mi 等单位。该查询默认不参与打分(等价 filter),只做空间筛选,配合排序输出按距离排名的结果。

2.2 geo_distance_range 环形带

{
  "query": {
    "bool": {
      "filter": [
        {
          "geo_distance": {
            "distance": "10km",
            "location": { "lat": 39.915, "lon": 116.404 }
          }
        },
        {
          "range": { "price": { "lte": 100 } }
        }
      ]
    }
  }
}

需要「3~10 公里」的环形带时,用两个 geo_distance 组合(within 10km 排除 within 3km),或用 geo_distance_range。实际场景多是距离 + 业务条件(价格、评分)的组合过滤,放进 bool filter 走位图缓存。

2.3 地球曲率与精度

距离计算在球面上进行,默认使用 Haversine 公式。小范围(城市内)可设置 distance_type 为 plane 平面近似换取更快速度,误差在数百公里级才会明显。查询精度与地理索引精度相关,写入时 orientation 与精度设置不当时边界点可能漏判。

3. 距离排序与衰减

一句话总结: 距离排序输出由近到远的结果,地理衰减函数让距离参与打分。

3.1 按距离排序

{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { "lat": 39.915, "lon": 116.404 },
        "order": "asc",
        "unit": "km"
      }
    }
  ],
  "size": 20
}

_geo_distance 排序按到中心点的距离升序,unit 控制返回的距离单位。排序结果会附带排序值(距离),可直接用于展示「距您 2.3km」。深翻页仍推荐 search_after,排序值就是距离,游标稳定。

3.2 gauss 地理衰减

{
  "query": {
    "function_score": {
      "query": { "match": { "category": "restaurant" } },
      "functions": [
        {
          "gauss": {
            "location": {
              "origin": { "lat": 39.915, "lon": 116.404 },
              "scale": "5km",
              "decay": 0.5
            }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

gauss 函数作用于地理字段时,距离 origin 越近分数越高,5km 处衰减到一半。适合「相关性与距离综合排序」:餐厅相关词匹配决定基础分,距离做平滑加权,近的排在前面但远的不被完全排除。

3.3 排序与打分的选择

纯距离排名用 _geo_distance 排序;相关 + 距离混合排序用 function_score + gauss。注意 gauss 的 scale 要按业务半径设,scale 设得太小会只有极近点得高分,太大则衰减无意义。地理字段参与打分会读 Doc Values,排序量大时注意性能。

4. 边界框与多边形过滤

一句话总结: 边界框与多边形查询把检索范围限制到地图区域,用于区域筛选与权限圈定。

4.1 geo_bounding_box

{
  "query": {
    "geo_bounding_box": {
      "location": {
        "top_left":  { "lat": 40.1, "lon": 116.3 },
        "bottom_right": { "lat": 39.8, "lon": 116.6 }
      }
    }
  }
}

geo_bounding_box 用左上/右下两点定义矩形区域,是最快的地理过滤方式,底层走倒排索引的单元格编码,适合与地图可视区域联动(地图平移时刷新当前视野内的数据)。

4.2 geo_polygon 与 geo_shape 查询

{
  "query": {
    "geo_shape": {
      "location": {
        "shape": {
          "type": "polygon",
          "coordinates": [[ [116.3,39.8], [116.7,39.8], [116.7,40.1], [116.3,39.8] ]]
        },
        "relation": "intersects"
      }
    }
  }
}

geo_polygon 查询点是否在多边形内(注意 polygon 点按 geo_point 存储);geo_shape 查询支持点与区域的包含、相交、不相交等空间关系(intersects / within / contains / disjoint)。配送范围权限、禁飞区、商圈圈定都靠这类查询。

4.3 空间关系的选择

{
  "query": {
    "geo_shape": {
      "delivery_zone": {
        "relation": "within"
      }
    }
  }
}

relation 语义要看清方向:intersects 表示图形相交,within 表示点在区域内,contains 表示区域包含查询图形。用错方向(把 within 当 contains)会得到相反结果。查询端 shape 每次传入图形,索引端提前建立好区域字段性能更好。

5. 地理聚合

一句话总结: 地理聚合把坐标折叠成距离带、范围框与中心点,用于地图热力与分布统计。

5.1 geo_distance 聚合

{
  "size": 0,
  "aggs": {
    "distance_bands": {
      "geo_distance": {
        "field": "location",
        "origin": { "lat": 39.915, "lon": 116.404 },
        "unit": "km",
        "ranges": [
          { "to": 1 }, { "from": 1, "to": 3 },
          { "from": 3, "to": 5 }, { "from": 5 }
        ]
      }
    }
  }
}

geo_distance 聚合按距离带分桶,统计「1km 内、13km、35km」各有多少门店,常用于分析配送覆盖。聚合结果可叠加折线图看覆盖衰减曲线。

5.2 geo_bounds 与 geo_centroid

{
  "size": 0,
  "aggs": {
    "bounds": { "geo_bounds": { "field": "location" } },
    "center": { "geo_centroid": { "field": "location" } }
  }
}

geo_bounds 输出一组坐标的包围盒(最小经度/纬度、最大经度/纬度),用于把地图视野自动缩放到数据范围;geo_centroid 输出重心,用于展示分组数据的中心点。两者结合能自动框选一组数据的展示区域。

5.3 网格热力

{
  "size": 0,
  "aggs": {
    "grid": {
      "geohash_grid": {
        "field": "location",
        "precision": 6
      }
    }
  }
}

geohash_grid 按 geohash 网格分桶统计每个小格内的数据量,是地图热力图的数据来源。precision 越大网格越细、桶越多,聚合开销越大。热力数据量巨大时建议只对当前视野内的数据做聚合,并用 BKD 的过滤器裁剪。

6. 性能与索引优化

一句话总结: 地理性能取决于字段类型、查询裁剪与 Doc Values 使用,先粗筛再精算。

6.1 查询裁剪策略

第一步:geo_bounding_box 粗筛到视野区域(倒排索引,快)
第二步:geo_distance 精算距离(Doc Values 计算)
第三步:gauss 参与打分排序(聚合与打分)

先边界框粗筛再距离精算,能避免对全量坐标做球面距离计算。边界框过滤走倒排索引的网格编码,比 Doc Values 逐文档计算快一个量级,这是地理查询优化最重要的顺序。

6.2 索引结构与精度

geo_shape 的索引精度默认 50m,precision 越小索引越大、查询越精确;geo_point 的 Doc Values 按压缩的经纬度存储,支持高效排序与聚合。大批量写入时注意地理字段的坐标校验成本,坐标范围越界(纬度超 ±90)会被拒绝。

6.3 常见性能坑

场景问题缓解
全量距离计算无粗筛,慢先 bounding_box
gauss 无 scale衰减失效按业务半径设 scale
数组坐标顺序反定位到错误半球统一经度,纬度
geo_shape 精度过细索引爆炸按需设 precision
大范围 geohash_grid桶数量爆炸限视野、降 precision

7. 地图可视化对接

一句话总结: Kibana Maps 直接消费 geo_point 字段,聚合接口为热力与聚类提供数据。

7.1 Kibana Maps 数据源

Kibana Maps 选择数据视图后,把 geo_point 字段拖入图层即可打点;文档级图层按字段渲染坐标点,聚合级图层用 geohash_grid 输出热力。数据视图需先把字段映射为 geo_point,否则地图图层无法识别坐标。

7.2 GeoJSON 与外部对接

{
  "type": "Feature",
  "properties": { "name": "门店A" },
  "geometry": {
    "type": "Point",
    "coordinates": [116.404, 39.915]
  }
}

ES 响应可转成 GeoJSON Feature 供前端地图库(Leaflet、Mapbox)直接渲染:每个命中文档转成一个 Point Feature,properties 放业务字段。geo_shape 索引的区域也能以 GeoJSON 形式导出叠加在底图上做范围展示。

7.3 地图联动查询

地图平移缩放时,前端把视野的 bounds(左上/右下坐标)作为 geo_bounding_box 查询参数请求接口,只加载当前视野数据。视野过大时聚合层按网格抽稀,避免一次渲染上万点。距离计算在服务端完成,前端只展示「距您 X km」的排序结果。

8. 总结

一句话总结: 地理搜索以 geo_point 与 geo_shape 建模,距离查询排序、边界框粗筛、地理聚合与地图可视化构成从坐标到地图的完整链路。

环节要点
数据类型geo_point 存点,geo_shape 存面
坐标约定数组与 GeoJSON 经度在前,字符串纬度在前
距离查询geo_distance 圆形过滤,配合 bool filter
排序_geo_distance 纯距离,gauss 混合打分
区域过滤bounding_box 粗筛,polygon/shape 精确圈定
地理聚合distance 带、bounds、centroid、geohash_grid
性能先粗筛后精算,精度与桶数收敛
可视化Kibana Maps 打点,GeoJSON 供前端渲染

地理搜索把坐标变成第一等公民:门店、外卖、配送范围都能被查询、排序与聚合。核心顺序是先用边界框把数据裁到视野,再用距离精算排序,最后用 gauss 或聚合做综合分析。查询与打分的上游语法可阅读《Query DSL 与相关性打分》,字段与索引设计参考《数据建模与 Mapping 设计》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 「搜索服务架构:从索引到容错」
  2. 「安全加固与访问控制:从角色到审计」
  3. 「高级文本检索:同义词、补全与纠错」