参考:
https://blog.csdn.net/tang_jian_dong/article/details/104446526
https://blog.csdn.net/u013041642/article/details/94416631
在elasticsearch中默认支持了地理坐标排序,非常方便。
项目中我们位置相关的功能主要有:位置由近到远排序、计算距离值、距离与价格一起权重打分
springboot项目
<!-- es -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
实体类
package com.dto;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldIndex;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
//商品
@Document(indexName = "sku", type = "sku", shards = 5, replicas = 1, indexStoreType = "fs", refreshInterval = "-1")
public class EsSku {
@Id
@Field(type = FieldType.Integer)
private Integer skuId;
/**
* 商品名称
*/
@Field(type = FieldType.String)
private String skuName;
/**
* 坐标,sql:CONCAT(ss.latitude,',',ss.longitude) position,坐标值得校验不然保存报错
*/
@GeoPointField
private String position;
/**
* 距离
*/
@Field(type = FieldType.String, index = FieldIndex.no)
private String distance;
/**
* 价格
*/
@Field(type = FieldType.Integer, index = FieldIndex.not_analyzed)
private Integer price;
get/set
}
dao
package com.repository;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface EsSkuRepository extends ElasticsearchRepository<EsSku, Integer> {
}
service
package com.impl;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.GeoDistanceQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@Service
public class SearchServiceImpl implements SearchService {
private static final Logger LOGGER = LoggerFactory.getLogger(SearchServiceImpl.class);
@Resource
private EsSkuRepository esSkuRepository;
private PageBean<EsSku> searchSku() {
String keywords = "";
Double lat = 39.939915;
Double lng = 116.469635;
boolean hasPosition = lat != null && lng != null;
Double distance = 1000;
Integer pageNum = 0;
Integer pageSize = 10;
// 分页
Pageable pageable = new PageRequest(pageNum, pageSize);
// 需要查询的字段
QueryBuilder keywordsBuilder = null;
// 如果是拼音则匹配全部同音汉字,如果是汉字则不匹配同音字
if (StringUtils.isEmpty(keywords)) {
// 0~10元之间评分为1, 1万元时评分为0.5,价格越高评分越低
ScoreFunctionBuilder priceFunctionBuilder = ScoreFunctionBuilders.gaussDecayFunction("price", 500, 500000).setOffset(500);
if (hasPosition) {
// 1km之内评分为1, 10km时评分为0.5,距离越远评分越低
Map<String, Double> origin = Maps.newHashMap();
origin.put("lat", lat);
origin.put("lon", lng);
ScoreFunctionBuilder gaussDecayFunction = ScoreFunctionBuilders.gaussDecayFunction("position", origin, "10km").setOffset("1km");
keywordsBuilder = QueryBuilders.functionScoreQuery(gaussDecayFunction).add(priceFunctionBuilder).boostMode(CombineFunction.MULT);
} else {
keywordsBuilder = QueryBuilders.functionScoreQuery(priceFunctionBuilder).boostMode(CombineFunction.MULT);
}
} else {
keywordsBuilder = QueryBuilders.multiMatchQuery(keywords, "skuName");
}
// 坐标排序
GeoDistanceQueryBuilder geoBuilder = null;
GeoDistanceSortBuilder sortGeoBuilder = null;
if (hasPosition) {
geoBuilder = QueryBuilders.geoDistanceQuery("position")// 查询字段
.point(lat, lng)// 设置经纬度
.distance(distance, DistanceUnit.KILOMETERS)// 设置距离查询的距离范围
.geoDistance(GeoDistance.ARC);
sortGeoBuilder = SortBuilders.geoDistanceSort("position").point(lat, lng).unit(DistanceUnit.KILOMETERS);
sortGeoBuilder.order(SortOrder.ASC);
}
// 创建查询对象
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().must(keywordsBuilder);
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
nativeSearchQueryBuilder.withQuery(queryBuilder).withPageable(pageable);
// 如果开启定位,则只查询定位内数据
if (hasPosition) {
nativeSearchQueryBuilder.withFilter(geoBuilder);
}
SearchQuery searchQuery = nativeSearchQueryBuilder.build();
// 结果
Page<EsSku> page = esSkuRepository.search(searchQuery);
List<EsSku> list = page.getContent();
// 计算距离
if (CollectionUtils.isNotEmpty(list) && hasPosition) {
for (EsSku esSku : list) {
Double latitude = Double.valueOf(esSku.getLatitude());
Double longitude = Double.valueOf(esSku.getLongitude());
double calculateDistance = GeoDistance.ARC.calculate(lat, lng, latitude, longitude, DistanceUnit.KILOMETERS);
String formatDistance = formatDistance(calculateDistance);
esSku.setDistance(formatDistance);
}
}
return new PageBean<EsSku>(pageNum + 1, pageSize, (int) page.getTotalElements(), list);
}
// 美化距离文本
private String formatDistance(double distance) {
if (distance == 0) {
return "0m";
} else if (distance > 1) {
distance = (double) Math.round(distance * 100) / 100;
String s = new StringBuilder().append(distance).append("km").toString();
return s;
} else {
distance = distance * 1000;
String s = new StringBuilder().append((int) distance).append("m").toString();
return s;
}
}
}
pagebean
package com.dto;
import java.util.List;
/**
* 分页功能中的一页的信息
*/
public class PageBean<T> {
// 指定的或是页面参数
private int pageNum; // 当前页
private int pageSize; // 每页显示多少条
// 查询数据库
private int recordCount; // 总记录数
private List<T> recordList; // 本页的数据列表
// 计算
private int pageCount; // 总页数
private int beginPageIndex; // 页码列表的开始索引(包含)
private int endPageIndex; // 页码列表的结束索引(包含)
/**
* 只接受前4个必要的属性,会自动的计算出其他3个属生的值
*
* @param pageNum
* @param pageSize
* @param recordCount
* @param recordList
*/
public PageBean(int pageNum, int pageSize, int recordCount, List<T> recordList) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.recordCount = recordCount;
this.recordList = recordList;
// 计算总页码
pageCount = (recordCount + pageSize - 1) / pageSize;
// 计算 beginPageIndex 和 endPageIndex
// >> 总页数不多于10页,则全部显示
if (pageCount <= 10) {
beginPageIndex = 1;
endPageIndex = pageCount;
}
// >> 总页数多于10页,则显示当前页附近的共10个页码
else {
// 当前页附近的共10个页码(前4个 + 当前页 + 后5个)
beginPageIndex = pageNum - 4;
endPageIndex = pageNum + 5;
// 当前面的页码不足4个时,则显示前10个页码
if (beginPageIndex < 1) {
beginPageIndex = 1;
endPageIndex = 10;
}
// 当后面的页码不足5个时,则显示后10个页码
if (endPageIndex > pageCount) {
endPageIndex = pageCount;
beginPageIndex = pageCount - 10 + 1;
}
}
}
public List<T> getRecordList() {
return recordList;
}
public void setRecordList(List<T> recordList) {
this.recordList = recordList;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageCount() {
return pageCount;
}
public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getRecordCount() {
return recordCount;
}
public void setRecordCount(int recordCount) {
this.recordCount = recordCount;
}
public int getBeginPageIndex() {
return beginPageIndex;
}
public void setBeginPageIndex(int beginPageIndex) {
this.beginPageIndex = beginPageIndex;
}
public int getEndPageIndex() {
return endPageIndex;
}
public void setEndPageIndex(int endPageIndex) {
this.endPageIndex = endPageIndex;
}
}