地理围栏功能怎么做?ES查询语句咋写?
地理围栏功能怎么做?ES查询语句咋写? 这个问题通常出现在 WebGIS、车辆轨迹、外勤打卡、门店服务范围、告警区域判断等项目中:前端画了一个多边形围栏,后端需要判断某个点、某条轨迹或一批位置数据是否落在围栏内,并且希望查询速度足够快。
本文以 Elasticsearch 的地理空间查询为核心,讲清楚地理围栏功能的基本设计、索引字段怎么建、ES 查询语句怎么写,以及实际项目中最容易踩坑的坐标系、经纬度顺序、边界误差和性能问题。
引言:地理围栏功能到底在判断什么
地理围栏不是一个神秘功能,本质上是一个空间关系判断问题。常见判断包括:
- 一个点是否在某个多边形围栏内。
- 一个点是否在某个圆形范围内。
- 一批车辆位置中,哪些车辆进入了指定区域。
- 某条轨迹是否穿过某个围栏。
- 某个订单地址是否落在门店配送范围内。
在 Elasticsearch 中,地理围栏查询通常依赖两个字段类型:
- geo_point:用于存储点位置,例如车辆当前位置、用户定位点、门店坐标。
- geo_shape:用于存储复杂空间对象,例如多边形围栏、行政区边界、配送区域。
如果只是查“某个点是否在圆形半径内”,一般用 geo_distance。如果要查“点是否落在多边形围栏内”,常见做法是点数据用 geo_point,查询条件使用 geo_polygon;或者围栏本身入库为 geo_shape,再用 geo_shape 查询。
背景:一个典型地理围栏业务流程
假设我们有一个车辆定位系统,需要实现“查询进入某个园区围栏的车辆”。数据结构大致如下:
- 每辆车定时上传当前位置。
- 后端把当前位置写入 Elasticsearch。
- 管理员在 WebGIS 地图上绘制园区边界。
- 系统根据园区边界查询当前位于围栏内的车辆。
这个流程可以拆成三个关键环节:
- 定位点数据入库,字段必须使用正确的地理字段类型。
- 前端绘制的围栏坐标必须转换为 ES 能识别的经纬度格式。
- 后端根据业务场景选择合适的 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"
}
这里要特别注意:使用对象格式时是 lat 和 lon;如果使用数组格式,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_polygon 或 geo_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 常见取值包括 intersects、within、contains、disjoint。具体可用关系与字段类型、版本实现有关,实际开发时要结合当前 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"
}
后端处理逻辑建议如下:
- 校验坐标数组长度,多边形至少需要 4 个点,并且首尾闭合。
- 校验经纬度范围,经度应在 -180 到 180,纬度应在 -90 到 90。
- 确认坐标系是 WGS84 经纬度,而不是 GCJ-02、BD-09 或投影坐标。
- 把 GeoJSON 的
[lon, lat]转换成 ES 查询需要的对象格式。 - 把状态、时间、车辆类型等普通条件和空间条件一起放入
bool.filter。 - 限制返回字段和分页大小,避免一次返回过多数据。
常见坑:地理围栏 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 执行查询、地图回显结果。只要坐标系统一、经纬度顺序正确、索引字段类型合理,地理围栏功能就可以稳定落地。