地理围栏功能怎么做?ES查询语句咋写?

GIS基础理论
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

地理围栏功能怎么做?ES查询语句咋写? 这个问题通常出现在 WebGIS、车辆轨迹、外勤打卡、门店服务范围、告警区域判断等项目中:前端画了一个多边形围栏,后端需要判断某个点、某条轨迹或一批位置数据是否落在围栏内,并且希望查询速度足够快。

本文以 Elasticsearch 的地理空间查询为核心,讲清楚地理围栏功能的基本设计、索引字段怎么建、ES 查询语句怎么写,以及实际项目中最容易踩坑的坐标系、经纬度顺序、边界误差和性能问题。

引言:地理围栏功能到底在判断什么

地理围栏不是一个神秘功能,本质上是一个空间关系判断问题。常见判断包括:

  • 一个点是否在某个多边形围栏内。
  • 一个点是否在某个圆形范围内。
  • 一批车辆位置中,哪些车辆进入了指定区域。
  • 某条轨迹是否穿过某个围栏。
  • 某个订单地址是否落在门店配送范围内。

在 Elasticsearch 中,地理围栏查询通常依赖两个字段类型:

  • geo_point:用于存储点位置,例如车辆当前位置、用户定位点、门店坐标。
  • geo_shape:用于存储复杂空间对象,例如多边形围栏、行政区边界、配送区域。

如果只是查“某个点是否在圆形半径内”,一般用 geo_distance。如果要查“点是否落在多边形围栏内”,常见做法是点数据用 geo_point,查询条件使用 geo_polygon;或者围栏本身入库为 geo_shape,再用 geo_shape 查询。

背景:一个典型地理围栏业务流程

假设我们有一个车辆定位系统,需要实现“查询进入某个园区围栏的车辆”。数据结构大致如下:

  • 每辆车定时上传当前位置。
  • 后端把当前位置写入 Elasticsearch。
  • 管理员在 WebGIS 地图上绘制园区边界。
  • 系统根据园区边界查询当前位于围栏内的车辆。

这个流程可以拆成三个关键环节:

  1. 定位点数据入库,字段必须使用正确的地理字段类型。
  2. 前端绘制的围栏坐标必须转换为 ES 能识别的经纬度格式。
  3. 后端根据业务场景选择合适的 ES 地理查询语句。
地理围栏功能 ES查询语句 geo_point与geo_polygon流程图
地理围栏功能的典型实现流程:绘制围栏、存储位置、执行 ES 空间查询并返回命中对象。

原理:ES 地理围栏查询的核心字段类型

1. geo_point:适合存储点位置

geo_point 用来表示一个经纬度点,适合车辆当前位置、人员定位、门店坐标、传感器位置等数据。

创建索引时可以这样定义:

PUT vehicle_location
{
  "mappings": {
    "properties": {
      "vehicle_id": {
        "type": "keyword"
      },
      "driver_name": {
        "type": "keyword"
      },
      "location": {
        "type": "geo_point"
      },
      "gps_time": {
        "type": "date"
      }
    }
  }
}

写入一条车辆位置数据:

POST vehicle_location/_doc/1
{
  "vehicle_id": "CAR_001",
  "driver_name": "张三",
  "location": {
    "lat": 31.2304,
    "lon": 121.4737
  },
  "gps_time": "2025-01-10T08:30:00+08:00"
}

这里要特别注意:使用对象格式时是 latlon;如果使用数组格式,Elasticsearch 通常按 [lon, lat] 理解,也就是先经度后纬度。

2. geo_shape:适合存储多边形、线和复杂区域

geo_shape 用来表示复杂几何对象,例如多边形围栏、行政区边界、缓冲区结果、线路范围等。

如果你的业务是“围栏很多,点也很多”,并且经常需要管理、查询、复用围栏,那么可以单独建立围栏索引:

PUT fence_index
{
  "mappings": {
    "properties": {
      "fence_id": {
        "type": "keyword"
      },
      "fence_name": {
        "type": "text"
      },
      "geometry": {
        "type": "geo_shape"
      }
    }
  }
}

写入一个多边形围栏:

POST fence_index/_doc/fence_001
{
  "fence_id": "fence_001",
  "fence_name": "园区A",
  "geometry": {
    "type": "polygon",
    "coordinates": [
      [
        [121.4700, 31.2280],
        [121.4780, 31.2280],
        [121.4780, 31.2350],
        [121.4700, 31.2350],
        [121.4700, 31.2280]
      ]
    ]
  }
}

GeoJSON 的坐标顺序是 [经度, 纬度],也就是 [lon, lat]。多边形首尾点必须闭合,最后一个点需要和第一个点一致。

步骤:地理围栏功能怎么做

步骤一:明确围栏类型

开发前先判断你的围栏是哪一种类型:

围栏类型 典型场景 推荐 ES 查询
圆形围栏 查询门店 3 公里内用户、车辆附近告警 geo_distance
矩形范围 地图视窗范围查询、简单框选 geo_bounding_box
多边形围栏 园区边界、配送范围、行政区边界 geo_polygongeo_shape
复杂区域围栏 多个面、洞、多行政区合并 geo_shape

步骤二:为点位数据建立 geo_point 索引

如果你的核心需求是“查哪些对象在围栏内”,对象位置通常建成 geo_point

PUT vehicle_location
{
  "mappings": {
    "properties": {
      "vehicle_id": {
        "type": "keyword"
      },
      "status": {
        "type": "keyword"
      },
      "location": {
        "type": "geo_point"
      },
      "gps_time": {
        "type": "date"
      }
    }
  }
}

批量写入数据时,建议统一使用对象格式,减少经纬度顺序写反的风险:

POST vehicle_location/_bulk
{"index":{"_id":"1"}}
{"vehicle_id":"CAR_001","status":"online","location":{"lat":31.2304,"lon":121.4737},"gps_time":"2025-01-10T08:30:00+08:00"}
{"index":{"_id":"2"}}
{"vehicle_id":"CAR_002","status":"online","location":{"lat":31.2400,"lon":121.4900},"gps_time":"2025-01-10T08:31:00+08:00"}

步骤三:圆形地理围栏用 geo_distance 查询

如果围栏是“某个中心点半径 3 公里”,ES 查询语句可以这样写:

GET vehicle_location/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "status": "online"
          }
        },
        {
          "geo_distance": {
            "distance": "3km",
            "location": {
              "lat": 31.2304,
              "lon": 121.4737
            }
          }
        }
      ]
    }
  }
}

这里建议把空间条件放在 filter 中,而不是 must 中。因为地理围栏判断通常不需要相关性评分,使用 filter 更符合过滤查询的语义。

步骤四:矩形范围查询用 geo_bounding_box

如果前端地图只是根据当前视窗范围加载点位,可以使用 geo_bounding_box

GET vehicle_location/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "geo_bounding_box": {
            "location": {
              "top_left": {
                "lat": 31.2400,
                "lon": 121.4600
              },
              "bottom_right": {
                "lat": 31.2200,
                "lon": 121.4900
              }
            }
          }
        }
      ]
    }
  }
}

这种方式适合地图视窗查询,但它不是严格的多边形围栏判断。只要点落在矩形框内就会命中,即使真实业务围栏是一个不规则区域。

步骤五:多边形围栏用 geo_polygon 查询点

如果围栏是前端绘制的多边形,而你的点字段是 geo_point,可以使用 geo_polygon 查询:

GET vehicle_location/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "geo_polygon": {
            "location": {
              "points": [
                { "lat": 31.2280, "lon": 121.4700 },
                { "lat": 31.2280, "lon": 121.4780 },
                { "lat": 31.2350, "lon": 121.4780 },
                { "lat": 31.2350, "lon": 121.4700 }
              ]
            }
          }
        }
      ]
    }
  }
}

这就是很多人搜索“地理围栏功能 ES查询语句”时真正需要的写法:点数据保存在 ES 中,围栏坐标由前端传入,后端直接拼接空间过滤条件。

如果前端传来的是 GeoJSON 坐标数组,格式通常是:

[
  [121.4700, 31.2280],
  [121.4780, 31.2280],
  [121.4780, 31.2350],
  [121.4700, 31.2350],
  [121.4700, 31.2280]
]

geo_polygon 示例里使用的是对象格式 { "lat": 纬度, "lon": 经度 }。后端转换时不要把经纬度写反。

步骤六:围栏入库后用 geo_shape 查询

如果围栏需要长期保存和复用,建议把围栏作为 geo_shape 存入 ES 或 PostGIS。对于 ES 方案,可以用 geo_shape 查询空间关系。

例如,我们有一个区域索引,里面存储了多个围栏:

PUT fence_index
{
  "mappings": {
    "properties": {
      "fence_name": {
        "type": "keyword"
      },
      "geometry": {
        "type": "geo_shape"
      }
    }
  }
}

查询哪些围栏包含某个点:

GET fence_index/_search
{
  "query": {
    "geo_shape": {
      "geometry": {
        "shape": {
          "type": "point",
          "coordinates": [121.4737, 31.2304]
        },
        "relation": "contains"
      }
    }
  }
}

查询哪些围栏与一个多边形相交:

GET fence_index/_search
{
  "query": {
    "geo_shape": {
      "geometry": {
        "shape": {
          "type": "polygon",
          "coordinates": [
            [
              [121.4700, 31.2280],
              [121.4780, 31.2280],
              [121.4780, 31.2350],
              [121.4700, 31.2350],
              [121.4700, 31.2280]
            ]
          ]
        },
        "relation": "intersects"
      }
    }
  }
}

relation 常见取值包括 intersectswithincontainsdisjoint。具体可用关系与字段类型、版本实现有关,实际开发时要结合当前 Elasticsearch 版本文档验证。

步骤七:后端接口建议这样设计

一个实用的地理围栏查询接口可以设计为:

POST /api/vehicle/search-in-fence
{
  "fence_type": "polygon",
  "coordinates": [
    [121.4700, 31.2280],
    [121.4780, 31.2280],
    [121.4780, 31.2350],
    [121.4700, 31.2350],
    [121.4700, 31.2280]
  ],
  "status": "online",
  "start_time": "2025-01-10T00:00:00+08:00",
  "end_time": "2025-01-10T23:59:59+08:00"
}

后端处理逻辑建议如下:

  1. 校验坐标数组长度,多边形至少需要 4 个点,并且首尾闭合。
  2. 校验经纬度范围,经度应在 -180 到 180,纬度应在 -90 到 90。
  3. 确认坐标系是 WGS84 经纬度,而不是 GCJ-02、BD-09 或投影坐标。
  4. 把 GeoJSON 的 [lon, lat] 转换成 ES 查询需要的对象格式。
  5. 把状态、时间、车辆类型等普通条件和空间条件一起放入 bool.filter
  6. 限制返回字段和分页大小,避免一次返回过多数据。

常见坑:地理围栏 ES 查询最容易错在哪里

1. 经纬度顺序写反

这是地理围栏功能中最常见的问题。GeoJSON 坐标是 [经度, 纬度],而很多地图 SDK 或业务对象会写成 lat, lng。一旦写反,查询结果可能为空,也可能跑到另一个城市甚至另一个国家。

排查方法:

  • 上海附近经度大约是 121,纬度大约是 31。
  • 北京附近经度大约是 116,纬度大约是 39。
  • 如果看到 [31, 121] 这类坐标,基本就是写反了。

2. 坐标系不一致

Elasticsearch 的地理查询通常按 WGS84 经纬度理解。如果你的前端地图使用高德、腾讯或百度坐标,可能涉及 GCJ-02 或 BD-09 坐标。坐标系不一致会导致围栏边界和定位点出现偏移。

常见表现包括:

  • 地图上看车辆在围栏内,ES 查询却查不到。
  • 围栏边界整体偏移几十米到几百米。
  • 同一批点在不同底图上位置不一致。

解决思路是:入库前统一坐标系,查询前也使用同一坐标系。不要让围栏是 GCJ-02,而点位是 WGS84。

3. 多边形没有闭合

GeoJSON 多边形要求第一个坐标点和最后一个坐标点一致。如果围栏没有闭合,可能导致写入失败或空间关系判断异常。

例如正确写法:

[
  [121.4700, 31.2280],
  [121.4780, 31.2280],
  [121.4780, 31.2350],
  [121.4700, 31.2350],
  [121.4700, 31.2280]
]

4. 把复杂空间分析全部交给 ES

Elasticsearch 很适合做高性能检索和过滤,但它不是完整的 GIS 空间分析平台。如果你的需求包含缓冲区叠加、拓扑修复、面融合、复杂相交面积统计,PostGIS 或专业 GIS 工具通常更合适。

一个稳妥架构是:PostGIS 负责围栏编辑、拓扑校验和复杂空间分析;Elasticsearch 负责高频点位检索、条件过滤和搜索接口。

5. 查询结果没有做时间过滤

车辆定位、人员定位、设备定位都有时间属性。如果只查 location,可能会查到历史点,而不是当前点。

建议在查询中加入时间条件,例如只查最近 5 分钟的在线点:

GET vehicle_location/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "gps_time": {
              "gte": "now-5m"
            }
          }
        },
        {
          "geo_distance": {
            "distance": "3km",
            "location": {
              "lat": 31.2304,
              "lon": 121.4737
            }
          }
        }
      ]
    }
  }
}

方法比较:geo_distance、geo_polygon、geo_shape 怎么选

方法 适合场景 优点 注意点
geo_distance 圆形范围查询 语句简单,适合附近搜索 不能表达不规则围栏
geo_bounding_box 地图视窗范围、矩形框选 查询条件简单,适合快速筛选 不是精确多边形判断
geo_polygon 用多边形查询 geo_point 点位 适合前端临时绘制围栏查点 不适合非常复杂的面和拓扑分析
geo_shape 围栏入库、复杂面查询、区域相交 表达能力更强,适合管理空间对象 建模和数据校验要求更高
PostGIS 复杂 GIS 分析、拓扑修复、面积统计 空间函数完整,GIS 能力强 搜索与全文检索能力不如 ES 直接

简单选择建议:

  • “查附近几公里”用 geo_distance
  • “查地图当前视窗内的数据”用 geo_bounding_box
  • “前端画一个多边形,查里面的点”用 geo_polygon
  • “围栏本身要保存、查询、复用和管理”用 geo_shape
  • “要做严格 GIS 空间分析”优先考虑 PostGIS。

检查清单:上线前请逐项确认

  • 字段类型:点位字段是否建成 geo_point,围栏字段是否建成 geo_shape
  • 坐标顺序:GeoJSON 是否使用 [lon, lat],对象格式是否使用 lat/lon
  • 坐标系:点位和围栏是否统一为同一坐标系。
  • 多边形闭合:多边形最后一个点是否等于第一个点。
  • 时间条件:定位数据是否过滤了最新时间范围。
  • 分页限制:是否限制 size,避免一次返回海量点。
  • 边界测试:是否测试了围栏内、围栏外、边界上三个位置。
  • 异常处理:是否处理空坐标、非法坐标、自相交多边形。
  • 性能验证:是否用真实数据量测试查询耗时和返回结果。

地理围栏功能不要只测“一个点在面内”的理想情况。至少要测试坐标写反、坐标系偏移、边界点、超大围栏、无效多边形和高并发查询。

FAQ

1. 地理围栏功能必须用 geo_shape 吗?

不一定。如果你的业务只是用一个临时多边形查询点位,点字段用 geo_point,查询时用 geo_polygon 就可以。只有当围栏本身需要长期存储、管理和做复杂空间关系查询时,才更推荐 geo_shape

2. ES 查询语句里经纬度到底是 lat lon 还是 lon lat?

如果使用对象格式,例如 { "lat": 31.2304, "lon": 121.4737 },字段名已经说明了含义。如果使用 GeoJSON 或数组坐标,一般是 [lon, lat],也就是先经度后纬度。实际项目中建议统一封装转换函数,不要在多个接口里手写转换。

3. 为什么地图上看点在围栏内,ES 却查不到?

优先检查三个问题:坐标系是否一致,经纬度是否写反,多边形是否闭合。国内互联网地图常见坐标系与 WGS84 不完全一致,如果点和围栏来源不同,很容易出现偏移。

4. geo_polygon 和 geo_shape 有什么区别?

geo_polygon 更像是用一个多边形条件去过滤 geo_point 点位,适合临时围栏查点。geo_shape 是一种字段类型和查询能力,适合存储和查询复杂几何对象,例如多边形围栏、线路、行政区边界。

5. 地理围栏查询性能慢怎么办?

先确认字段类型是否正确,其次把空间条件放在 filter 中,再配合时间、状态、组织机构等普通过滤条件缩小范围。对于超大规模实时轨迹数据,还需要考虑冷热数据分索引、只查询最新点、限制返回字段和分页。

6. 轨迹穿越围栏能直接用 geo_point 查询吗?

如果只是判断轨迹采样点是否进入围栏,可以把每个采样点作为 geo_point 查询。但如果要判断一条线段是否穿过围栏,即使两个采样点都在围栏外,中间线段也可能穿越围栏,这类问题更适合使用 geo_shape 或 PostGIS 的线面相交分析。

结论

地理围栏功能怎么做,关键不是只会写一条 ES 查询语句,而是要把字段类型、坐标系、坐标顺序、围栏类型和业务时效性一起设计好。

如果是圆形范围,用 geo_distance;如果是地图矩形视窗,用 geo_bounding_box;如果是前端绘制多边形查点,用 geo_polygon;如果围栏需要长期管理和复杂空间关系查询,用 geo_shape 或 PostGIS。

实际项目中,建议先用少量真实坐标做端到端验证:前端画围栏、后端接收坐标、ES 执行查询、地图回显结果。只要坐标系统一、经纬度顺序正确、索引字段类型合理,地理围栏功能就可以稳定落地。