本文主要是介绍ES旅游案例(完整的关键字搜索、条件过滤、附近酒店距离、公告竞价排位案例),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
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 {/*** 注入业务层*/@Autowiredprivate 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 {/*** 注入*/@AutowiredRestHighLevelClient client;/**** @param params* @return*/@Overridepublic PageResult search(RequestParams params) {try {// 1、准备requeueSearchRequest 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、准备requeueSearchRequest request = new SearchRequest("hotel");// 2、准备DSLbuildBaicQuery(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 {/*** 注入*/@AutowiredRestHighLevelClient client;/*** 拼接筛选条件** @param params* @return*/@Overridepublic PageResult search(RequestParams params) {try {// 1、准备requeueSearchRequest request = new SearchRequest("hotel");// 2、准备DSLbuildBaicQuery(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);}
}
这篇关于ES旅游案例(完整的关键字搜索、条件过滤、附近酒店距离、公告竞价排位案例)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!