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 地理查询?遇到过哪些坑?比如经纬度搞反、距离算不准之类的,评论区聊聊,我来帮你出主意~

相关推荐
汤姆yu19 小时前
基于springboot的热门文创内容推荐分享系统
java·spring boot·后端
星光一影19 小时前
教育培训机构消课管理系统智慧校园艺术舞蹈美术艺术培训班扣课时教务管理系统
java·spring boot·mysql·vue·mybatis·uniapp
lkbhua莱克瓦2419 小时前
MySQL介绍
java·开发语言·数据库·笔记·mysql
武昌库里写JAVA19 小时前
在iview中使用upload组件上传文件之前先做其他的处理
java·vue.js·spring boot·后端·sql
董世昌4119 小时前
什么是事件冒泡?如何阻止事件冒泡和浏览器默认事件?
java·前端
好度20 小时前
配置java标准环境?(详细教程)
java·开发语言
嘻哈baby20 小时前
AI让我变强了还是变弱了?一个后端开发的年终自省
后端
teacher伟大光荣且正确20 小时前
关于Qt QReadWriteLock(读写锁) 以及 QSettings 使用的问题
java·数据库·qt
舒一笑20 小时前
2025:从“代码搬运”到“意图编织”,我在 AI 浪潮中找回了开发的“爽感”
后端·程序员·产品
nightseventhunit20 小时前
base64字符串String.getByte导致OOM Requested array size exceeds VM limit
java·oom