ES 地理查询玩明白,产品要的 “附近的店” 再也难不倒我!(附 DSL+Java 实战)

每次产品拍着我肩膀说 "加个附近商家功能呗",我都想把数据库里的经纬度字段拎出来给它看 ------ 循环遍历算距离?数据多了能卡到下班!直到我把 ES 地理查询玩明白,才发现这活居然能这么丝滑。今天就带大家从 "产品需求" 到 "代码落地",把 ES 地理查询的精髓嚼碎了讲,不管是 DSL 还是 Java,看完就能用!

先打个基础:ES 咋存 "地理位置"?

要查地理信息,得先让 ES 知道 "这玩意儿是经纬度"。ES 里专门有个geo_point类型,存数据的时候得按规矩来 ------ 要么是[经度, 纬度]的数组(注意!先经后纬,别搞反了),要么是"纬度,经度"的字符串,比如存一家奶茶店:

json

json 复制代码
// 新增索引,指定地址字段为geo_point
PUT /shop_index
{
  "mappings": {
    "properties": {
      "shop_name": {"type": "text"},
      "shop_addr": {"type": "geo_point"}, // 关键!地理字段类型
      "price_level": {"type": "integer"}
    }
  }
}

// 插入一条数据:上海人民广场附近的奶茶店
POST /shop_index/_doc/1
{
  "shop_name": "快乐柠檬",
  "shop_addr": [121.481074, 31.235925], // [经度, 纬度]
  "price_level": 2
}

记住这个geo_point,后面所有查询都得靠它 "引路"~

实战 1:矩形范围查询 ------"我要查朝阳区所有咖啡店"

产品有时候会提 "圈一块地,把里面的店都列出来",比如 "朝阳区东三环到东四环之间的咖啡店"。这时候就该geo_bounding_box(矩形范围查询)上场了 ------ 本质就是用两个对角的经纬度,框出一个矩形,只要店铺在这个框里,就能被查到。

DSL 写法:一句话框出矩形

比如我要查 "经度 121.45 到 121.50,纬度 31.22 到 31.25" 之间的店(大概是上海人民广场周边),DSL 长这样:

json

json 复制代码
GET /shop_index/_search
{
  "query": {
    "geo_bounding_box": {
      "shop_addr": { // 要查询的地理字段
        "top_left": [121.45, 31.25],  // 左上角经纬度
        "bottom_right": [121.50, 31.22] // 右下角经纬度
      },
      "boost": 1.0 // 权重,可不写
    }
  },
  "sort": [{"_score": {"order": "desc"}}] // 按相关性排序
}

这里有个小技巧:如果觉得 "左上角 / 右下角" 记着麻烦,也能用top_rightbottom_left,只要能框出矩形就行。执行完之后,刚才插的 "快乐柠檬" 就会被查出来 ------ 谁让它在这个框里呢~
## 实战 2:距离查询 ------"500 米内的奶茶店,速来!"

比矩形查询更常用的,是 "附近 N 米内的 XX",比如外卖 APP 的 "500 米内奶茶店"。这时候geo_distance(距离查询)就是王炸,直接指定 "中心点 + 距离",ES 会自动算所有店铺到这个点的直线距离,筛选出符合条件的。

DSL 写法:精准控距,还能玩花样

比如用户在 "上海人民广场(121.481074, 31.235925)",想找 1 公里内的平价奶茶店(价格等级≤2),DSL 可以这么写:

json

json 复制代码
GET /shop_index/_search
{
  "query": {
    "bool": { // 组合查询:既要在距离内,又要符合价格条件
      "must": [
        {
          "geo_distance": {
            "distance": "1km", // 距离:支持km(公里)、m(米)、mi(英里)
            "shop_addr": [121.481074, 31.235925] // 用户当前经纬度(中心点)
          }
        },
        {
          "range": {
            "price_level": {"lte": 2} // 价格等级≤2(平价)
          }
        }
      ]
    }
  },
  "sort": [
    {
      "_geo_distance": { // 按"距离由近到远"排序(用户最关心近的)
        "shop_addr": [121.481074, 31.235925],
        "order": "asc",
        "unit": "m" // 排序时的单位(米)
      }
    }
  ]
}

这里的distance参数特别灵活,比如想查 "300 米内" 就写"300m",想查 "2 英里内" 就写"2mi",ES 会自动换算,不用咱们自己算地球半径~

实战 3:Java 代码落地 ------ 后端同学看这里!

光会 DSL 还不够,后端得把它写成 Java 代码。咱们用最常用的RestHighLevelClient(ES 官方推荐)来写,分 "矩形查询" 和 "距离查询" 两个例子,直接复制到项目里改改就能用。

先准备:引入依赖(Maven)

首先得有 ES 客户端依赖,注意版本要和你的 ES 服务器对应(比如我用的 7.14.0):

xml

xml 复制代码
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>7.14.0</version>
</dependency>
<dependency>
  <groupId>org.elasticsearch</groupId>
  <artifactId>elasticsearch</artifactId>
  <version>7.14.0</version>
</dependency>

Java 实现:矩形范围查询

比如查询 "上海人民广场周边矩形内的店铺",代码里要构建GeoBoundingBoxQueryBuilder

java

运行

java 复制代码
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class EsGeoQueryService {

    // 注入RestHighLevelClient(提前配置好客户端)
    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * 矩形范围查询:查指定经纬度矩形内的店铺
     * @param topLeftLon 左上角经度
     * @param topLeftLat 左上角纬度
     * @param bottomRightLon 右下角经度
     * @param bottomRightLat 右下角纬度
     */
    public void queryShopByRectangle(double topLeftLon, double topLeftLat, 
                                     double bottomRightLon, double bottomRightLat) throws IOException {
        // 1. 创建查询请求(指定索引)
        SearchRequest searchRequest = new SearchRequest("shop_index");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 2. 构建矩形查询条件
        sourceBuilder.query(QueryBuilders.geoBoundingBoxQuery("shop_addr")
                .topLeft(topLeftLat, topLeftLon) // 注意!Java里是(纬度,经度),和DSL反过来
                .bottomRight(bottomRightLat, bottomRightLon));

        // 3. 设置查询参数(比如返回10条数据)
        sourceBuilder.size(10);
        searchRequest.source(sourceBuilder);

        // 4. 执行查询,处理结果
        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        // 解析结果(这里简化,实际项目要遍历hits)
        System.out.println("矩形查询到的店铺数量:" + response.getHits().getTotalHits().value);
    }
}

这里有个 要注意:Java 的topLeft()方法参数是 "纬度在前,经度在后",和 DSL 的[经度,纬度]正好反过来!第一次写的时候差点查不到数据,血的教训~

Java 实现:距离查询(最常用)

比如 "用户当前位置附近 1 公里内的平价奶茶店",代码里用GeoDistanceQueryBuilder

java

运行

scss 复制代码
/**
 * 距离查询:查用户附近N距离内的平价店铺
 * @param userLon 用户经度
 * @param userLat 用户纬度
 * @param distance 距离(比如"1km")
 * @param maxPrice 最高价格等级
 */
public void queryShopByDistance(double userLon, double userLat, 
                                String distance, int maxPrice) throws IOException {
    SearchRequest searchRequest = new SearchRequest("shop_index");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

    // 1. 构建距离查询条件(核心)
    GeoDistanceQueryBuilder distanceQuery = QueryBuilders.geoDistanceQuery("shop_addr")
            .point(userLat, userLon) // 用户位置:纬度在前,经度在后
            .distance(distance) // 距离,支持"1km""300m"等
            .distanceType(GeoDistanceType.PLANE); // 计算方式:PLANE(平面计算,快)、ARC(球面计算,准)

    // 2. 组合价格条件(平价:价格等级≤maxPrice)
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
            .must(distanceQuery) // 必须满足距离条件
            .must(QueryBuilders.rangeQuery("price_level").lte(maxPrice)); // 必须满足价格条件

    sourceBuilder.query(boolQuery);

    // 3. 按距离由近到远排序(用户体验更好)
    sourceBuilder.sort(new GeoDistanceSortBuilder("shop_addr", userLat, userLon)
            .order(SortOrder.ASC) // 升序:近的在前
            .unit(DistanceUnit.KILOMETERS)); // 排序单位:公里

    // 4. 执行查询
    searchRequest.source(sourceBuilder);
    SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // 5. 解析结果(示例:打印店铺名和距离)
    for (SearchHit hit : response.getHits().getHits()) {
        Map<String, Object> sourceMap = hit.getSourceAsMap();
        String shopName = (String) sourceMap.get("shop_name");
        // 获取排序后的距离(单位:公里,保留2位小数)
        double distanceKm = (double) hit.getSortValues()[0];
        System.out.printf("店铺:%s,距离:%.2f公里%n", shopName, distanceKm);
    }
}

这里的distanceType可以根据需求选:如果是 "同城内的短距离查询",用PLANE(计算快)就行;如果是 "跨城市的长距离查询",用ARC(球面计算更准确),不过性能会稍差一点,一般业务用PLANE足够了。

总结:ES 地理查询,就这三板斧!

  1. 存得对 :用geo_point类型存经纬度,别搞反经纬度顺序;

  2. 查得准 :矩形查询用geo_bounding_box(圈地),距离查询用geo_distance(附近);

  3. 写得顺 :Java 代码注意 "纬度在前,经度在后",组合条件用BoolQuery,排序用GeoDistanceSort

现在再遇到产品提 "附近功能",我直接把这段代码甩给他看:"放心,10 分钟搞定,还不卡!" 你们项目里有没有用过 ES 地理查询?遇到过哪些坑?比如经纬度搞反、距离算不准之类的,评论区聊聊,我来帮你出主意~

相关推荐
shark_chili3 小时前
深入理解CPU缓存:编写高性能Java代码的终极指南
后端
十八旬3 小时前
苍穹外卖项目实战(day-5完整版)-记录实战教程及问题的解决方法
java·开发语言·spring boot·redis·mysql
m0_749299954 小时前
Nginx主配置文件
java·服务器·nginx
╭╰4024 小时前
苍穹外卖优化-续
java·spring·mybatis
金銀銅鐵4 小时前
[Java] 枚举常量的精确类型一定是当前枚举类型吗?
java·后端
chenrui3104 小时前
Spring Boot 和 Spring Cloud: 区别与联系
spring boot·后端·spring cloud
邂逅星河浪漫4 小时前
Spring Boot常用注解-详细解析+示例
java·spring boot·后端·注解
青鱼入云4 小时前
java面试中经常会问到的mysql问题有哪些(基础版)
java·mysql·面试
Darenm1114 小时前
python进程,线程与协程
java·开发语言