每次产品拍着我肩膀说 "加个附近商家功能呗",我都想把数据库里的经纬度字段拎出来给它看 ------ 循环遍历算距离?数据多了能卡到下班!直到我把 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_right
和bottom_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 地理查询,就这三板斧!
-
存得对 :用
geo_point
类型存经纬度,别搞反经纬度顺序; -
查得准 :矩形查询用
geo_bounding_box
(圈地),距离查询用geo_distance
(附近); -
写得顺 :Java 代码注意 "纬度在前,经度在后",组合条件用
BoolQuery
,排序用GeoDistanceSort
。
现在再遇到产品提 "附近功能",我直接把这段代码甩给他看:"放心,10 分钟搞定,还不卡!" 你们项目里有没有用过 ES 地理查询?遇到过哪些坑?比如经纬度搞反、距离算不准之类的,评论区聊聊,我来帮你出主意~