一、Elasticsearch简介
Elasticsearch是一个分布式、高扩展、高实时的搜索与数据分析引擎。Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful Web接口。
1.1 Elasticsearch核心概念
- 索引index:存储document文档数据的结构,类似于关系型数据库中的数据库
- 类型type:用于存储document的逻辑结构,相对于index来说,type是index的下级,类似于关系型数据库中的表
- 文档document:json格式,类似于关系型数据库中的行数据
Elasticsearch使用的是一种称为倒排索引的结构,采用Lucene倒排索作为底层。这种结构适用于快速的全文搜索, 一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。一个Elasticsearch索引是由多个Lucene索引组成。
二、Elasticsearch环境搭建
本地需要配置JDK环境(使用jdk1.8及以上版本)和node环境(用于启动Elasticsearch Head)。除此之外需要安装配置(本文基于Elasticsearch7.6.1版本进行讲解):
- Elasticsearch
- Elasticsearch Head
- Ik
- Kibana
华为云镜像下载-Elasticsearch
Github下载-Elasticsearch Head
华为云镜像下载-Logstash(本文不需要)
Github下载-Ik
华为云镜像下载-Kibana
所有下载均下载7.6.1版本即可。
2.1 Elasticsearch跨域请求配置
在Elasticsearch安装目录的\config\elasticsearch.yml文件中配置跨域请求,以便Elasticsearch Head访问。
http.cors.enabled: true
http.cors.allow-origin: "*"
2.2 Ik分词器配置
在Elasticsearch安装目录的\config\plugins下新建ik目录,将下载好的ik压缩包解压即可。
2.3 Kibana汉化
在Kibana安装目录的\config\kibana.yml文件中配置默认语言为中文。
i18n.locale: "zh-CN"
2.4 启动Elasticsearch
双击Elasticsearch安装目录下的\bin\elasticsearch.bat文件,浏览器访问:
图2-1 Elasticsearch启动成功界面.png
2.5 启动Elasticsearch Head
在Elasticsearch Head目录下,命令行输入npm run start启动Elasticsearch Head,浏览器访问:
图2-2 Elasticsearch Head启动成功界面.png
2.6 启动Kibana
双击Kibana安装目录下的\bin\kibana.bat文件,浏览器访问:
图2-3 Kibana启动成功界面.png
三、Kibana操作ES
Kibana操作ES参考以下参考链接即可。
- text类型会分词,keyword类型不会分词
四、Spring Boot整合Elasticsearch
引入Elasticsearch和HighLevelClient的Maven依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.1</version>
</dependency>
指定Elasticsearch的版本为7.6.1。
<properties>
<elasticsearch.version>7.6.1</elasticsearch.version>
</properties>
RestHighLevelClient配置如下所示:
@Configuration
public class ESConfig {
@Bean
public RestHighLevelClient restHighLevelClient() {
RestClientBuilder builder = RestClient.builder(new HttpHost("127.0.0.1", 9200, "http"));
return new RestHighLevelClient(builder);
}
}
Elasticsearch全局配置:
spring.elasticsearch.rest.uris=http://127.0.0.1:9200
五、商铺搜索建议、GEO位置搜索
本文实现源码如下,欢迎Star和Fork。
参考链接如下所示:
B站狂神说-ElasticSearch教程
巨坑:ElasticSearch 无法解析序列化的 GeoPoint 字段
URL在线转码
参考链接一:ES7学习笔记(十二)高亮 和 搜索建议
参考链接二:ES7学习笔记(十三)GEO位置搜索
5.1 创建店铺索引
使用Kibana创建店铺(Shop)索引,字段包括shopName(店铺名)、address(店铺地址)、tags(店铺标签)、location地址位置字段和suggest搜索建议字段,本文将tags数组作为搜索建议字段的值。
PUT /shop
{
"settings":{
"analysis":{
"analyzer":{
"default":{
"type":"ik_max_word"
}
}
}
},
"mappings":{
"dynamic_date_formats": [
"MM/dd/yyyy",
"yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm:ss"
],
"properties":{
"shopName":{
"type":"text"
},
"address":{
"type":"text"
},
"tags":{
"type":"text"
},
"suggest":{
"type":"completion"
},
"location":{
"type":"geo_point"
}
}
}
}
5.2 新增店铺文档
使用PostMan进行测试,调用新增文档接口,新增文档:
图5-1 新增文档一.png 图5-2 新增文档二.png
后端处理新增文档请求,使用Gson解析数据,使用Json解析会序列化失败,原因参考:https://agentd.cn/archives/es-geopoint。
后端实现核心代码如下所示:
@PostMapping(value = "/es/document/create/shop")
public String createShopDocument(@RequestBody Shop shop) throws IOException {
IndexRequest indexRequest = new IndexRequest("shop");
indexRequest.timeout(TimeValue.timeValueSeconds(1));
shop.setSuggest(new Completion(shop.getTags()));
indexRequest.source(new Gson().toJson(shop), XContentType.JSON);
IndexResponse indexResponse = highLevelClient.index(indexRequest, RequestOptions.DEFAULT);
return indexResponse.toString();
}
5.3 基于搜索建议和GEO搜索店铺
使用PostMan进行测试,调用店铺文档搜索接口,使用店铺名搜索,同时传入当前地理位置的经纬度和距离len进行距离过滤。
图5-3 搜索店铺成功.png 图5-4 搜索店铺失败.png
后端实现核心代码如下所示:
@PostMapping(value = "/es/document/search/shop")
public String shopSearch(@RequestBody ShopSearchInfo shopSearchInfo) throws IOException {
SearchRequest searchRequest = new SearchRequest("shop");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
/**
* 搜索建议
*/
CompletionSuggestionBuilder suggestionBuilder = SuggestBuilders
.completionSuggestion(Shop.SUGGEST)
.prefix(shopSearchInfo.getShopName());
SuggestBuilder suggestBuilder = new SuggestBuilder();
//自定义搜索名
suggestBuilder.addSuggestion("shopSearch", suggestionBuilder);
/**
* GEO位置搜索
*/
GeoPoint geoPoint = new GeoPoint(shopSearchInfo.getLatitude(), shopSearchInfo.getLongitude());
//geo距离查询
QueryBuilder queryBuilder = QueryBuilders.geoDistanceQuery(Shop.LOCATION)
.distance(shopSearchInfo.getLen(), DistanceUnit.KILOMETERS)
.point(geoPoint);
sourceBuilder.suggest(suggestBuilder);
sourceBuilder.query(queryBuilder);
searchRequest.source(sourceBuilder);
SearchResponse response = highLevelClient.search(searchRequest, RequestOptions.DEFAULT);
List<String> geoResList = new ArrayList<>();
SearchHit[] searchHits = response.getHits().getHits();
if (searchHits == null || searchHits.length <= 0) {
return "未搜索到相关信息!";
}
for (SearchHit hit : searchHits) {
geoResList.add(hit.getId());
}
CompletionSuggestion suggestion = response.getSuggest().getSuggestion("shopSearch");
List<CompletionSuggestion.Entry.Option> optionList = suggestion.getOptions();
if (optionList == null || optionList.size() <= 0) {
return "未搜索到相关信息!";
}
List<String> suggestResList = new ArrayList<>();
for (CompletionSuggestion.Entry.Option option : optionList) {
suggestResList.add(option.getHit().getId());
}
/**
* 寻找相同元素
*/
geoResList.retainAll(suggestResList);
if (geoResList == null || geoResList.size() <= 0) {
return "未搜索到相关信息!";
}
List<SearchResultInfo> resList = new ArrayList<>();
for (String shopID : geoResList) {
GetRequest request = new GetRequest("shop", shopID);
GetResponse searchResponse = highLevelClient.get(request, RequestOptions.DEFAULT);
Map<String, Object> resultMap = searchResponse.getSourceAsMap();
if (resultMap == null || resultMap.size() <= 0) {
continue;
}
String shopName = (String) resultMap.get(Shop.SHOPNAME);
Map<String, Double> geoInfo = (Map<String, Double>) resultMap.get(Shop.LOCATION);
double _lon1 = geoInfo.get("lon");
double _lat1 = geoInfo.get("lat");
double _lon2 = shopSearchInfo.getLongitude();
double _lat2 = shopSearchInfo.getLatitude();
double len = this.getDistance(_lat1, _lon1, _lat2, _lon2);
SearchResultInfo resultInfo = new SearchResultInfo();
resultInfo.setShopID(shopID).setShopName(shopName).setLen(len);
resList.add(resultInfo);
}
/**
* 按照距离排序 - 倒序
*/
resList = resList.stream().distinct().sorted(Comparator
.comparing(SearchResultInfo::getLen)).collect(Collectors.toList());
return JSONObject.toJSONString(resList);
}
public double getDistance(double _lat1, double _lon1, double _lat2, double _lon2) {
double lat1 = (Math.PI / 180) * _lat1;
double lat2 = (Math.PI / 180) * _lat2;
double lon1 = (Math.PI / 180) * _lon1;
double lon2 = (Math.PI / 180) * _lon2;
//地球半径
double R = 6378.1;
double d = Math.acos(Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1)) * R;
return new BigDecimal(d).setScale(4, BigDecimal.ROUND_HALF_UP).doubleValue();
}
网友评论