ES旅游案例
下面,我们通过ES旅游的案例来实战演练下之前学习的知识。
我们实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8090,就能看到项目页面了:
由于页面内容的确实,所以现在要使用
postman
软件进行接口的测试,访问的数据页数一样的
点击搜索按钮查看页面的请求数据
由此可以知道,我们这个请求的信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们实现业务的流程如下:
- 步骤一:定义实体类,接收请求参数的JSON对象
- 步骤二:编写controller,接收页面的请求
- 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页
1、定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
1)请求参数

前端请求的json结构如下:
{
"key": "搜索关键字",
"page": 1,
"size": 3,
"sortBy": "default"
}
因此,我们在cn.itcast.hotel.pojo
包下定义一个实体类:
package cn.itcast.hotel.pojo;
import lombok.Data;
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
2)返回值
分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们在cn.itcast.hotel.pojo
中定义返回结果:
package cn.itcast.hotel.pojo;
import lombok.Data;
import java.util.List;
@Data
public class PageResult {
private Long total;
private List<HotelDoc> hotels;
public PageResult() {
}
public PageResult(Long total, List<HotelDoc> hotels) {
this.total = total;
this.hotels = hotels;
}
}
2、定义Controller
定义一个HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:/hotel/list
- 请求参数:对象,类型为RequestParam
- 返回值:PageResult,包含两个属性
Long total
:总条数List<HotelDoc> hotels
:酒店数据
因此,我们在cn.itcast.hotel.web
中定义HotelController:
package cn.itcast.hotel.web;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 项目名称:hotel-demo
* 描述:请求控制器
*
* @author zhong
* @date 2022-06-04 13:20
*/
@RestController
@RequestMapping("/hotel")
public class HotelController {
/**
* 注入业务层
*/
@Autowired
private IHotelService hotelService;
/**
* 请求查询分页信息并返回
* @return
*/
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams params){
return hotelService.search(params);
}
}
3、创建业务层以及实现方式
我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。
1)在cn.itcast.hotel.service
中的IHotelService
接口中定义一个方法:
快捷键介绍:创建接口后,需要创建接口的实现类,可以按住键盘的
Ctrl+Alt+B
进行跳转到实现类上
/**
* 根据关键字搜索酒店信息
* @param params 请求参数对象,包含用户输入的关键字
* @return 酒店文档列表
*/
PageResult search(RequestParams params);
2)实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel
中的HotelDemoApplication
中声明这个Bean:
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
3)在cn.itcast.hotel.service.impl
中的HotelService
中实现search方法:
package cn.itcast.hotel.service.impl;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
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.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
/**
* 注入
*/
@Autowired
RestHighLevelClient client;
/**
*
* @param params
* @return
*/
@Override
public PageResult search(RequestParams params) {
try {
// 1、准备requeue
SearchRequest request = new SearchRequest("hotel");
// 2、准备DSL
// 2.1、关键字搜索
String key = params.getKey();
if(key==null || "".equals(key)){
request.source().query(QueryBuilders.matchAllQuery());
}else{
request.source().query(QueryBuilders.matchQuery("all",key));
}
// 2.2、分页搜索
Integer page = params.getPage();
Integer size = params.getSize();
request.source().from((page -1)*size).size(size);
// 3、发送请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4、解析响应
return extracted(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
/**
* 封装的提统一使用的重构步骤
* @param search
*/
private PageResult extracted(SearchResponse search) {
// 4、解析响应数据
SearchHits hits = search.getHits();
// 4.1、获取总条数
long value = hits.getTotalHits().value;
// 4.2、获取文档数组
SearchHit[] hitsArray = hits.getHits();
List<HotelDoc> hotelDocs = new ArrayList<>();
// 4.3、遍历数组
for (SearchHit documentFields : hitsArray) {
// 获取文档
String sourceAsString = documentFields.getSourceAsString();
// 将文档放序列化为json对象
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
hotelDocs.add(hotelDoc);
}
return new PageResult(value,hotelDocs);
}
}
4、酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
我们需要做两件事情:
- 修改请求参数的对象RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
4.1、添加实体类属性
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
@Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
// 下面是新增的过滤条件参数
private String city;
private String brand;
private String starName;
private Integer minPrice;
private Integer maxPrice;
}
4.2、修改搜索业务
在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
- 城市过滤:是keyword类型,用term查询
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
因为条件构建的逻辑比较复杂,这里先封装为一个函数:
buildBasicQuery的代码如下:
/**
* 拼接筛选条件
* @param params
* @return
*/
@Override
public PageResult search(RequestParams params) {
try {
// 1、准备requeue
SearchRequest request = new SearchRequest("hotel");
// 2、准备DSL
buildBaicQuery(params, request);
// 2.2、分页搜索
int page = params.getPage();
int size = params.getSize();
request.source().from((page -1)*size).size(size);
// 3、发送请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4、解析响应
return extracted(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
/**
* 重构筛选条件
* @param params
* @param request
*/
private void buildBaicQuery(RequestParams params, SearchRequest request) {
// 将查询的条件较多,所以封装在一起
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.1、关键字搜索
String key = params.getKey();
if(key==null || "".equals(key)){
boolQuery.must(QueryBuilders.matchAllQuery());
}else{
boolQuery.must(QueryBuilders.matchQuery("all",key));
}
// 城市条件查询,不要参与算分
if(params.getCity() != null && !params.getCity().equals("")){
boolQuery.filter(QueryBuilders.matchQuery("city", params.getCity()));
}
// 匹配条件
if(params.getBrand() != null && !params.getBrand().equals("")){
boolQuery.filter(QueryBuilders.matchQuery("brand", params.getBrand()));
}
// 星级条件
if(params.getStarName() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.matchQuery("starName", params.getStarName()));
}
// 价格判断
if(params.getMinPrice() != null && !params.getMaxPrice().equals("")){
// 大于等于和小于等于
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
}
request.source().query(boolQuery);
}
使用postman软件进行测试
5、附近酒店查询
需求:我附近的酒店
酒店信息的坐标key是
location
们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
5.1、修改实体类
在返回封装的实体类上进行一个坐标距离的添加
// 我当前的地理坐标
private String location;
5.2、距离排序API
我们以前学习过排序功能,包括两种:
- 普通字段排序
- 地理坐标排序
我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法,如下:
GET /indexName/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance" : {
"FIELD" : "纬度,经度",
"order" : "asc",
"unit" : "km"
}
}
]
}
5.3、添加距离排序
// 坐标范围排序
String location = params.getLocation();
if(location != null && !location.equals("")){
request.source().sort(SortBuilders.geoDistanceSort("location",new GeoPoint(location))
.order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));
}
5.4、完善解析数据,回显酒店距离
6、酒店竞价排名
需求:让指定的酒店在搜索结果中排名置顶
我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而function_score包含3个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean类型:
- true:是广告
- false:不是广告
这样function_score包含3个要素就很好确定了:
- 过滤条件:判断isAD 是否为true
- 算分函数:我们可以用最简单暴力的weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
因此,业务的实现步骤包括:
-
给HotelDoc类添加isAD字段,Boolean类型
-
挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
-
修改search方法,添加function score功能,给isAD值为true的酒店增加权重
6.1、修改HotelDoc实体
添加多一个
isAD
布尔字段
private Boolean isAD;
6.3、在Dev Tools编写页面手动的添加这几个字段的属性为公告的
添加广告标记
# 手动添加公告字段
POST /hotel/_update/1514269829
{
"doc":{
"isAD":true
}
}
POST /hotel/_update/541619
{
"doc":{
"isAD":true
}
}
POST /hotel/_update/485775
{
"doc":{
"isAD":true
}
}
6.4、修改原有的查询方式
java对应的api如下
添加一个新的算分方法
我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
// 2、算分控制
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
// 原始算分方法
boolQuery,
// function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中一个function score原始
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD",true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
完整的业务代码
package cn.itcast.hotel.service.impl;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
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.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
/**
* 注入
*/
@Autowired
RestHighLevelClient client;
/**
* 拼接筛选条件
*
* @param params
* @return
*/
@Override
public PageResult search(RequestParams params) {
try {
// 1、准备requeue
SearchRequest request = new SearchRequest("hotel");
// 2、准备DSL
buildBaicQuery(params, request);
// 2.2、分页搜索
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 坐标范围排序
String location = params.getLocation();
if (location != null && !location.equals("")) {
request.source().sort(SortBuilders.geoDistanceSort("location", new GeoPoint(location))
.order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));
}
// 3、发送请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4、解析响应
return extracted(response);
} catch (IOException e) {
throw new RuntimeException();
}
}
/**
* 重构筛选条件
*
* @param params
* @param request
*/
private void buildBaicQuery(RequestParams params, SearchRequest request) {
// 将查询的条件较多,所以封装在一起
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.1、关键字搜索
String key = params.getKey();
if (key == null || "".equals(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// 城市条件查询,不要参与算分
if (params.getCity() != null && !params.getCity().equals("")) {
boolQuery.filter(QueryBuilders.matchQuery("city", params.getCity()));
}
// 匹配条件
if (params.getBrand() != null && !params.getBrand().equals("")) {
boolQuery.filter(QueryBuilders.matchQuery("brand", params.getBrand()));
}
// 星级条件
if (params.getStarName() != null && !params.getStarName().equals("")) {
boolQuery.filter(QueryBuilders.matchQuery("starName", params.getStarName()));
}
// 价格判断
if (params.getMinPrice() != null && !params.getMaxPrice().equals("")) {
// 大于等于和小于等于
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
}
// 2、算分控制
FunctionScoreQueryBuilder functionScoreQueryBuilder =
QueryBuilders.functionScoreQuery(
// 原始算分方法
boolQuery,
// function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中一个function score原始
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD",true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
request.source().query(boolQuery);
}
/**
* 封装的提统一使用的重构步骤
*
* @param search
*/
private PageResult extracted(SearchResponse search) {
// 4、解析响应数据
SearchHits hits = search.getHits();
// 4.1、获取总条数
long value = hits.getTotalHits().value;
// 4.2、获取文档数组
SearchHit[] hitsArray = hits.getHits();
List<HotelDoc> hotelDocs = new ArrayList<>();
// 4.3、遍历数组
for (SearchHit documentFields : hitsArray) {
// 获取文档
String sourceAsString = documentFields.getSourceAsString();
// 将文档放序列化为json对象
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
// 获取到距离排序的值
Object[] rawSortValues = documentFields.getSortValues();
if (rawSortValues.length > 0) {
Object sortValue = rawSortValues[0];
// 添加距离
hotelDoc.setDistance(sortValue);
System.out.println("查询返回的公里数:" + sortValue);
}
hotelDocs.add(hotelDoc);
}
return new PageResult(value, hotelDocs);
}
}