最近有兩個需求是通過地圖選擇坐標,查詢指定距離內的信息,和通過坐標集合組成不規則形狀查詢出不規則形狀范圍內的數據,在最起初我看到這個需求的時候第一個想法就是太復雜了,抵觸。但是沒有辦法既然需求下來了只能硬着頭皮上了,在調研之后最終還是決定了使用elasticsearch,下面就開始介紹如何使用elasticsearch來進行相關操作
設置數據格式
地理坐標點(geo-point) 是指地球表面可以用經緯度描述的一個點。地理坐標點可以用來計算兩個坐標位置間的距離,或者判斷一個點是否在一個區域中。地理坐標點不能被動態映射(dynamic mapping)自動檢測,而是需要顯式聲明對應字段類型為 geo_point ,例子中的location字段
PUT platform_foreign_website
{
"mappings": {
"store":{
"properties": {
"id": {
"type": "text"
},
"storeName": {
"type": "text"
},
"location":{
"type": "geo_point"
}
}
}
}
}
存儲示例:
- 半角逗號分割的字符串形式 “lat,lon“
- 明確以 lat 和 lon 作為屬性的對象
- 數組形式表示 [lon,lat]
需要特別注意的就是緯度在前邊經度在后邊(latitude,longitude),數組表示形式是經度在前緯度在后([longitude,latitude])
geo_distance 找出指定位置在給定距離內的數據,相當於指定圓心和半徑找到圓中點
- 找出兩千米范圍內的所有門店
- distance:距離 單位/km
- location:坐標點 圓心所在位置
es代碼示例
//無排序
GET platform_foreign_website/communit/_search
{
"query": {
"constant_score": {
"filter": {
"geo_distance": {
"distance": "2km",
"location": {
"lat": 39.662263,
"lon": 118.197815
}
}
},
"boost": 1.2
}
}
}
//帶排序
GET platform_foreign_website/communit/_search
{
"query": {
"constant_score": {
"filter": {
"geo_distance": {
"distance": "1km",
"location": {
"lat": 39.662263,
"lon": 118.197815
}
}
},
"boost": 1.2
}
},"sort": [
{
"_geo_distance" : {
"location" : [
{
"lat" : 40.010955,
"lon" : 118.68545
}
],
"unit" : "km",
"distance_type" : "arc",
"order" : "asc",
"validation_method" : "STRICT"
}
}
]
}
java代碼示例:
這里選用的是ElasticsearchTemplate 模板,因為沒有分頁取值比較方便,可以根據自己的業務邏輯自行選擇
//無排序
private List<Store> getStores(CommunityToStoreParameter communityToStoreParameter) {
//拼接條件
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
QueryBuilder isdeleteBuilder = QueryBuilders.termQuery("isdelete", false);
// 以某點為中心,搜索指定范圍
GeoDistanceQueryBuilder distanceQueryBuilder = new GeoDistanceQueryBuilder("location");
distanceQueryBuilder.point(communityToStoreParameter.getLatitude(), communityToStoreParameter.getLongitude());
//查詢單位:km
distanceQueryBuilder.distance(communityAndStoresdistance, DistanceUnit.KILOMETERS);
boolQueryBuilder.filter(distanceQueryBuilder);
boolQueryBuilder.must(isdeleteBuilder);
SearchQuery storesearchQuery = new NativeSearchQueryBuilder()
.withIndices(ESIndexAndTypeConstant.INDEX_NAME)
.withTypes(ESIndexAndTypeConstant.STORE_TYPE_NAME)
.withQuery(boolQueryBuilder)
.build();
return elasticsearchTemplate.queryForList(storesearchQuery, Store.class);
}
//有排序
private Page<Store> getStores(Integer page, Integer size, AroundStoreParameter aroundStoreParameter, Double latitude, Double longitude, QueryBuilder isdeleteBuilder) {
Pageable pageable = PageRequest.of(page - 1, size);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// 以某點為中心,搜索指定范圍
GeoDistanceQueryBuilder distanceQueryBuilder = new GeoDistanceQueryBuilder("location");
distanceQueryBuilder.point(latitude, longitude);
//查詢單位:km
distanceQueryBuilder.distance(aroundStoreParameter.getDistance(), DistanceUnit.KILOMETERS);
boolQueryBuilder.filter(distanceQueryBuilder);
boolQueryBuilder.must(isdeleteBuilder);
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
// 按距離升序
GeoDistanceSortBuilder distanceSortBuilder =
new GeoDistanceSortBuilder("location", latitude, longitude);
distanceSortBuilder.unit(DistanceUnit.KILOMETERS);
distanceSortBuilder.order(SortOrder.ASC);
nativeSearchQueryBuilder.withSort(distanceSortBuilder);
nativeSearchQueryBuilder.withPageable(pageable);
return storeRepository.search(nativeSearchQueryBuilder.build());
}
geo_polygon(地理多邊形查詢): 一個查詢,查詢多邊形內的所有的點,實現地圖畫圈篩選
es代碼示例
GET platform_foreign_website/store/_search
{
"query": {
"bool": {
"must": [
{"match_all": {}}
],
"filter": {
"geo_polygon": {
"location": {
"points": [
{ "lat": 39.634863, "lon": 118.137071 },
{ "lat": 39.6349, "lon": 118.13291 },
{ "lat": 39.633121, "lon": 118.128941 },
{ "lat": 39.629607, "lon": 118.130915 },
{ "lat": 39.632696, "lon": 118.132769 },
{ "lat": 39.630513, "lon": 118.136018 }
]
}
}
}
}
}
}
java代碼示例
public PageResultVO storeRange(Integer page, Integer size, List<StoreRangeParameter> storeRangeParameters) {
Pageable pageable = PageRequest.of(page - 1, size);
//坐標集合
List<GeoPoint> geoPoints = storeRangeParameters.stream().map(a ->
new GeoPoint(a.getLatitude(), a.getLongitude())
).collect(Collectors.toList());
//拼接查詢條件
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
QueryBuilder locationBuilder = QueryBuilders.geoPolygonQuery("location", geoPoints);
QueryBuilder isdeleteBuilder = QueryBuilders.termQuery("isdelete", false);
boolQueryBuilder.filter(locationBuilder);
boolQueryBuilder.must(isdeleteBuilder);
SearchQuery communityNativeBuilder = new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(boolQueryBuilder)
.build();
Page<Community> communities = communityRepository.search(communityNativeBuilder);
List<CommunityInfoResultVO> communityInfoResultVOList = communities.getContent().stream().map(community ->
new CommunityInfoResultVO(community.getCommunityName(), community.getLongitude(), community.getLatitude())
).collect(Collectors.toList());
return new PageResultVO<>(communities.getTotalElements(), communityInfoResultVOList);
}
這里說明一下為什么有的是用filter()有的使用must():must查詢需要打分評估進行_score,但是filter,先使用filter把無用的數據過濾掉,在已經處理過的數據中must查詢,能夠有效地提升性能
這里說了 geo_distance(找出與指定位置在給定距離內的點) 和 geo_polygon(找出落在多邊形中的點)兩種方法,elasticsearch其實還有另外兩種 方法 geo_bounding_box(找出落在指定矩形框中的坐標點) 和 geo_distance_range(找出與指定點距離在給定最小距離和最大距離之間的點),因為需求沒有涉及到暫時沒有調研,以后會慢慢晚上
所有這些過濾器的工作方式都相似: 把 索引中所有文檔(而不僅僅是查詢中匹配到的部分文檔,見 fielddata-intro)的經緯度信息都載入內存,然后每個過濾器執行一個輕量級的計算去判斷當前點是否落在指定區域