1. 引言

本文将深入探讨 Elasticsearch 的地理空间功能。我们不会涉及 Elasticsearch 实例和 Java 客户端的搭建,而是聚焦于如何存储地理数据以及通过地理查询检索数据。

接下来,我们直接了解可用的地理数据类型。

2. 地理数据类型

Elasticsearch 支持两种主要地理数据类型:**geo_point(由经纬度坐标组成)和 geo_shape(可表示矩形、线条、多边形等复杂形状)**。使用地理查询前,必须手动创建索引映射并显式设置字段映射。⚠️ 动态映射不适用于地理类型字段

下面我们详细解析每种数据类型。

2.1. Geo Point 数据类型

geo_point 是最基础的类型,表示地图上的经纬度点。它可用于多种场景:

  • 判断点是否在矩形区域内
  • 按距离范围搜索对象
  • 在复杂 geo_shape 查询中搜索索引点

简单来说,geo_point 就像地图上的一个图钉。此外,它还支持按位置分组文档(如特定区域或距离某点的范围),并按距离排序(从近到远)。

首先需要创建索引映射:

PUT /index_name
{
    "mappings": {
        "TYPE_NAME": {
            "properties": {
                "location": { 
                    "type": "geo_point" 
                } 
            } 
        } 
    } 
}

现在可以开始存储地理点数据了。

2.2. Geo Shape 数据类型

geo_point 不同,geo_shape 支持存储和搜索多边形、矩形等复杂形状。要查询包含非地理点的形状文档,必须使用 geo_shape 类型。

同样,我们先创建索引映射:

PUT /index_name
{
    "mappings": {
        "TYPE_NAME": {
            "properties": {
                "location": {
                    "type": "geo_shape"
                }
            }
        }
    }
}

Elasticsearch 将 geo_shape 表示为三角网格,以实现高空间分辨率。

接下来我们看看如何存储数据。

3. 存储 Geo Point 数据的多种方式

假设我们已将 location 字段映射为 geo_point 类型。

3.1. 经纬度对象

显式定义经纬度键值对:

PUT index_name/_doc
{
    "location": { 
        "lat": 23.02,
        "lon": 72.57
    }
}

✅ 最清晰无歧义的方式。

3.2. 经纬度字符串

用字符串简化表示:

{
    "location": "23.02,72.57"
}

⚠️ 注意顺序:字符串格式为 lat,lon,而数组格式、GeoJSON 和 WKT 格式为 lon,lat

3.3. 经纬度数组

使用数组表示:

{
    "location": [72.57, 23.02]
}

⚠️ 顺序反转:数组格式中经度在前,纬度在后(为兼容 GeoJSON 标准)。

3.4. Geo Hash

使用地理哈希值代替坐标:

{
    "location": "tsj4bys"
}

虽然哈希值简洁且适合邻近搜索,但可读性差。可使用 在线工具 转换经纬度到哈希值。

4. 存储 Geo Shape 数据的多种方式

假设我们已将 region 字段映射为 geo_shape 类型。

4.1. 点(Point)

创建最简单的形状——点:

POST /index/_doc
{
    "region" : {
        "type" : "point",
        "coordinates" : [72.57, 23.02]
    }
}

region 字段包含 typecoordinates 两个子字段,用于标识数据类型。

4.2. 线(LineString)

插入线段:

POST /index/_doc
{
    "region" : {
        "type" : "linestring",
        "coordinates" : [[77.57, 23.02], [77.59, 23.05]]
    }
}

线段由起点和终点两个坐标定义。聚合多条 LineString 可用于构建导航系统。

4.3. 多边形(Polygon)

插入多边形:

POST /index/_doc
{
    "region" : {
        "type" : "polygon",
        "coordinates" : [
            [ [10.0, 0.0], [11.0, 0.0], [11.0, 1.0], [10.0, 1.0], [10.0, 0.0] ]
        ]
    }
}

⚠️ 首尾坐标必须相同以形成闭合多边形。

4.4. 其他 GeoJSON/WKT 格式

Elasticsearch 支持丰富的 GeoJSON/WKT 结构:

  • MultiPoint
  • MultiLineString
  • MultiPolygon
  • GeometryCollection
  • Envelope(非标准 GeoJSON,但 ES 和 WKT 支持)

完整列表见 官方文档

关键点:必须提供 typecoordinates 字段才能正确索引。由于结构复杂,Elasticsearch 不支持对 geo_shape 字段排序或直接检索,只能通过 _source 字段获取。

5. 在 Elasticsearch 中插入地理数据

现在插入文档并学习地理查询。首先添加 Java 客户端依赖

<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.9.0</version>
</dependency>

5.1. 创建显式映射的索引

插入数据前需定义索引映射:

client.indices().create(builder -> builder.index(WONDERS_OF_WORLD)
  .mappings(bl -> bl
    .properties("region", region -> region.geoShape(gs -> gs))
    .properties("location", location -> location.geoPoint(gp -> gp))
  )
);

这里创建了两个字段:geo_shape 类型的 regiongeo_point 类型的 location

5.2. 插入 geo_point 文档

创建 Java 类表示 geo_point 数据:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Location {
    private String name;
    private List<Double> location;
}

name 表示位置名称,location 是包含经纬度的列表(使用 Lombok 简化代码)。

使用 index() 方法插入文档:

Location pyramidsOfGiza = new Location("Pyramids of Giza", List.of(31.1328, 29.9761));
IndexResponse response = client.index(builder -> builder
  .index(WONDERS_OF_WORLD)
  .document(pyramidsOfGiza));

也可直接使用 JSON 字符串:

String jsonObject = """
    {
        "name":"Lighthouse of alexandria",
        "location":{ "lat": 31.2139, "lon": 29.8856 }
    }
    """;
IndexResponse response = client.index(idx -> idx
  .index(WONDERS_OF_WORLD)
  .withJson(new StringReader(jsonObject)));

5.3. 插入 geo_shape 文档

直接使用 JSON 字符串插入 geo_shape 数据:

String jsonObject = """
    {
        "name":"Agra",
        "region":{
            "type":"envelope",
            "coordinates":[[75,30.2],[80.1,25]]
        }
    }
    """;
IndexResponse response = client.index(idx -> idx
    .index(WONDERS_OF_WORLD)
    .withJson(new StringReader(jsonObject)));

现在可以开始地理查询了。

6. 在 Elasticsearch 中查询地理数据

6.1. 地理边界框查询(Geo Bounding Box)

假设要在地图矩形区域内查找所有点,使用边界框查询:

{
   "query":{
      "geo_bounding_box":{
         "location":{
            "top_left":[30.0,31.0],
            "bottom_right":[32.0,28.0]
         }
      }
   }
}

Java 实现示例:

SearchRequest.Builder builder = new SearchRequest.Builder().index(WONDERS_OF_WORLD);
builder.query(query -> query
  .geoBoundingBox(geoBoundingBoxQuery ->
    geoBoundingBoxQuery.field("location")
      .boundingBox(geoBounds -> geoBounds.tlbr(bl4 -> bl4
        .topLeft(geoLocation -> geoLocation.coords(List.of(30.0, 31.0)))
        .bottomRight(geoLocation -> geoLocation.coords(List.of(32.0, 28.0))))
      )
  )
);

边界框查询支持与 geo_point 相同的多种格式,详见 官方文档

执行查询:

SearchResponse<Location> searchResponse = client.search(build, Location.class);
log.info("Search response: {}", searchResponse);

6.2. 地理形状查询(Geo Shape)

查询 geo_shape 文档必须使用 GeoJSON。例如查找特定坐标内的所有文档:

{
  "query":{
    "bool":{
      "filter":[
        {
          "geo_shape":{
            "region":{
              "shape":{
                "type":"envelope",
                "coordinates":[[74.0,31.2],[81.1,24.0]]
              },
              "relation":"within"
            }
          }
        }
      ]
    }
  }
}

relation 字段定义空间关系操作符

  • INTERSECTS(默认):返回与查询几何体相交的文档
  • DISJOINT:返回与查询几何体无重叠的文档
  • WITHIN:返回完全在查询几何体内的文档
  • CONTAINS:返回包含查询几何体的文档

Java 实现示例:

StringReader jsonData = new StringReader("""
    {
        "type":"envelope",
        "coordinates": [[74.0, 31.2], [81.1, 24.0 ] ]
    }
    """);

SearchRequest searchRequest = new SearchRequest.Builder()
  .query(query -> query.bool(boolQuery -> boolQuery
    .filter(query1 -> query1
      .geoShape(geoShapeQuery -> geoShapeQuery.field("region")
        .shape(
          geoShapeFieldQuery -> geoShapeFieldQuery.relation(GeoShapeRelation.Within)
            .shape(JsonData.from(jsonData))
  ))))).build();

执行查询:

SearchResponse<Object> search = client.search(searchRequest, Object.class);
log.info("Search response: {}", search);

由于 geo_shape 结构多变,返回结果映射为泛型 Object

6.3. 地理距离查询(Geo Distance)

查找指定点距离范围内的所有文档:

{
    "query":{
        "geo_distance":{
          "location":{
              "lat":29.976,
              "lon":31.131
          },
          "distance":"10 miles"
        }
    }
}

Java 实现示例:

SearchRequest searchRequest = new SearchRequest.Builder().index(WONDERS_OF_WORLD)
  .query(query -> query
    .geoDistance(geoDistanceQuery -> geoDistanceQuery
      .field("location").distance("10 miles")
      .location(geoLocation -> geoLocation
        .latlon(latLonGeoLocation -> latLonGeoLocation
          .lon(29.88).lat(31.21)))
    )
  ).build();

距离查询支持 多种坐标格式

6.4. 地理多边形查询(Geo Polygon)

在多边形区域内查找所有点:

{
    "query":{
        "bool":{
          "filter":[
            {
                "geo_shape":{
                    "location":{
                        "shape":{
                            "type":"polygon",
                             "coordinates":[[[68.859, 22.733],[68.859, 24.733],[70.859, 23]]]
                        },
                        "relation":"within"
                    }
                }
            }
          ]
        }
    }
}

Java 实现示例:

JsonData jsonData = JsonData.fromJson("""
    {
        "type":"polygon",
        "coordinates":[[[68.859,22.733],[68.859,24.733],[70.859,23]]]
    }
    """);

SearchRequest build = new SearchRequest.Builder()
  .query(query -> query.bool(
    boolQuery -> boolQuery.filter(
      query1 -> query1.geoShape(geoShapeQuery -> geoShapeQuery.field("location")
        .shape(
          geoShapeFieldQuery -> geoShapeFieldQuery.relation(GeoShapeRelation.Within)
            .shape(jsonData)))))
 ).build();

⚠️ 此查询仅支持 geo_point 数据类型

7. 总结

本文详细介绍了 Elasticsearch 地理数据的索引映射选项(geo_pointgeo_shape),展示了多种存储地理数据的方式,并通过地理查询和 Java API 演示了如何过滤结果。

完整代码见 GitHub 仓库


原始标题:Geospatial Support in ElasticSearch