文章目录


实现四部分功能:

  • 酒店搜索和分页
  • 酒店结果过滤
  • 我周边的酒店
  • 酒店竞价排名

启动hotel-demo项目,其默认端口是8089,访问​http://localhost:8089​,就能看到项目页面了:

【Spring Cloud】ES实战:黑马旅游案例_搜索

1.1 酒店搜索和分页

案例需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页

1.1.1 需求分析

在项目的首页,有一个大大的搜索框,还有分页按钮:

【Spring Cloud】ES实战:黑马旅游案例_旅游_02

点击搜索按钮,可以看到浏览器控制台发出了请求:

【Spring Cloud】ES实战:黑马旅游案例_旅游_03

请求参数如下:

【Spring Cloud】ES实战:黑马旅游案例_spring cloud_04

由此可以知道,我们这个请求的信息如下:

  • 请求方式:​​POST​
  • 请求路径:​​/hotel/list​
  • 请求参数:​​JSON对象​​,包含4个字段:
  • ​key​​:搜索关键字
  • ​page​​:页码
  • ​size​​:每页大小
  • ​sortBy​​:排序,目前暂不实现
  • 返回值:分页查询,需要返回分页结果​​PageResult​​,包含两个属性:
  • ​total​​:总条数
  • ​List<HotelDoc>​​:当前页的数据

因此,我们实现业务的流程如下:

  • 步骤一:定义实体类,接收请求参数的JSON对象
  • 步骤二:编写controller,接收页面的请求
  • 步骤三:编写业务实现,利用RestHighLevelClient实现搜索、分页

返回顶部


1.1.2 定义实体类

实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。

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;
}
}

返回顶部


1.1.3 定义controller

定义一个HotelController,声明查询接口,满足下列要求:

  • 请求方式:Post
  • 请求路径:/hotel/list
  • 请求参数:对象,类型为RequestParam
  • 返回值:PageResult,包含两个属性
  • ​Long total​​:总条数
  • ​List<HotelDoc> hotels​​:酒店数据

因此,我们在cn.itcast.hotel.web中定义HotelController:

@RestController
@RequestMapping("/hotel")
public class HotelController {

@Autowired
private IHotelService hotelService;
// 搜索酒店数据
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams params){
return hotelService.search(params);
}
}

返回顶部


1.1.4 实现搜索业务

我们在controller调用了IHotelService,并没有实现该方法,因此下面我们就在IHotelService中定义方法,并且去实现业务逻辑。

1)在cn.itcast.hotel.service中的​IHotelService​接口中定义一个方法:

/**
* 根据关键字搜索酒店信息
* @param params 请求参数对象,包含用户输入的关键字
* @return 酒店文档列表
*/
PageResult search(RequestParams params);

2)实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel中的​HotelDemoApplication​中声明这个Bean:

/**
* 客户端
*
* @return {@link RestHighLevelClient}
*/
@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方法:

@Autowired
private RestHighLevelClient client;

@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
String key = params.getKey();
if (key == null || "".equals(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}

// 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 handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

// 结果解析
private PageResult handleResponse(SearchResponse response) {
// 4.解析响应
SearchHits searchHits = response.getHits();
// 1.1.获取总条数
long total = searchHits.getTotalHits().value;
// 1.2.文档数组
SearchHit[] hits = searchHits.getHits();
// 1.3.遍历
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 放入集合
hotels.add(hotelDoc);
}
// 1.4.封装返回
return new PageResult(total, hotels);
}

可以看到运行程序后,打开浏览器进行关键词的搜索,我们可以看到提交的请求信息:

【Spring Cloud】ES实战:黑马旅游案例_实体类_05

同时还有获取返回的结果数据,当前页的数据信息:

【Spring Cloud】ES实战:黑马旅游案例_实体类_06

返回顶部


1.2 酒店结果过滤

需求:添加品牌、城市、星级、价格等过滤功能

1.2.1 需求分析

在页面搜索框下面,会有一些过滤项:

【Spring Cloud】ES实战:黑马旅游案例_实体类_07

传递的参数如图:

【Spring Cloud】ES实战:黑马旅游案例_elasticsearch_08

包含的过滤条件有:

  • ​brand​​:品牌值
  • ​city​​:城市
  • ​minPrice~maxPrice​​:价格范围
  • ​starName​​:星级

我们需要做两件事情:

  • 修改请求参数的对象 RequestParams,接收上述参数
  • 修改业务逻辑,在搜索条件之外,添加一些过滤条件

返回顶部


1.2.2 修改实体类

修改在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;
}

返回顶部


1.2.3 修改搜索业务

在HotelService的search方法中,只有一个地方需要修改:requet.source().query( … )其中的查询条件。

在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:

  • 品牌过滤:是keyword类型,用​​term​​查询
  • 星级过滤:是keyword类型,用​​term​​查询
  • 价格过滤:是数值类型,用​​range​​查询
  • 城市过滤:是keyword类型,用​​term​​查询

多个查询条件组合,肯定是boolean查询来组合:

  • 关键字搜索放到​​must​​中,参与算分
  • 其它过滤条件放到​​filter​​中,不参与算分

因为条件构建的逻辑比较复杂,这里先封装为一个函数:

【Spring Cloud】ES实战:黑马旅游案例_elasticsearch_09

buildBasicQuery的代码如下:

private void buildBasicQuery(RequestParams params, SearchRequest request) {
// 1.构建BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.关键字搜索
String key = params.getKey();
if (key == null || "".equals(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// 3.城市条件
if (params.getCity() != null && !params.getCity().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// 4.品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// 5.星级条件
if (params.getStarName() != null && !params.getStarName().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// 6.价格
if (params.getMinPrice() != null && params.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders
.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}
// 7.放入source
request.source().query(boolQuery);
}

可以看到我们的选项转换为了提交信息的参数信息:

【Spring Cloud】ES实战:黑马旅游案例_elasticsearch_10

查询效果:

【Spring Cloud】ES实战:黑马旅游案例_实体类_11

返回顶部


1.3 我周边的酒店

需求:寻找附近的酒店

1.3.1 需求分析

在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:

【Spring Cloud】ES实战:黑马旅游案例_搜索_12

并且,在前端会发起查询请求,将你的坐标发送到服务端:

【Spring Cloud】ES实战:黑马旅游案例_spring cloud_13

我们要做的事情就是基于这个location坐标,然后按照距离对周围酒店排序。实现思路如下:

  • 修改​​RequestParams​​​参数,接收​​location​​字段
  • 修改​​search​​​方法业务逻辑,如果​​location​​​有值,添加根据​​geo_distance​​排序的功能

返回顶部


1.3.2 修改实体类

修改在cn.itcast.hotel.pojo包下的实体类RequestParams:

package cn.itcast.hotel.pojo;

import lombok.Data;

@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;
// 当前的地理坐标
private String location;
}

1.3.3 距离排序API

我们以前学习过排序功能,包括两种:

  • 普通字段排序
  • 地理坐标排序

我们地理坐标排序只学过DSL语法,如下:

GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
},
{
"_geo_distance" : {
"FIELD" : "纬度,经度",
"order" : "asc",
"unit" : "km"
}
}
]
}

对应的java代码示例:

【Spring Cloud】ES实战:黑马旅游案例_旅游_14

返回顶部


1.3.4 添加距离排序

cn.itcast.hotel.service.impl​HotelService​​search​方法中,添加一个排序功能:

// 2.2 分页
Integer page = params.getPage();
Integer size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 2.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));
}

完整代码:

@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
buildBasicQuery(params, request);

// 2.2.分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);

// 2.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)
);
}

// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

浏览器页面点击地图右下角定位,等待后就可以看到位置信息(Chrome可能出错,建议换个浏览器试试):

【Spring Cloud】ES实战:黑马旅游案例_旅游_15

返回顶部


1.3.5 排序距离显示

重启服务后,测试我的酒店功能:

【Spring Cloud】ES实战:黑马旅游案例_elasticsearch_16

发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?

排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的:

【Spring Cloud】ES实战:黑马旅游案例_elasticsearch_17

因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。

我们要做两件事:

  • 修改​​HotelDoc​​,添加排序距离字段,用于页面显示
  • 修改​​HotelService​​​类中的​​handleResponse​​​方法,添加对​​sort值​​的获取

1)修改HotelDoc类,添加距离字段

package cn.itcast.hotel.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
// 排序时的 距离值
private Object distance;

public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}

2)修改HotelService中的handleResponse方法

/**
* 处理响应
*
* @param response 响应
*/
public PageResult handleResponse(SearchResponse response) {
// 4.解析响应
SearchHits searchHits = response.getHits();
// 1.1.获取总条数
long total = searchHits.getTotalHits().value;
// 1.2.文档数组
SearchHit[] hits = searchHits.getHits();
// 1.3.遍历
ArrayList<HotelDoc> lists = new ArrayList<>();
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 获取排序距离值
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0){
Object sortValue = sortValues[0];
// 封装
hotelDoc.setDistance(sortValue);
}
lists.add(hotelDoc);
}
// 4.4 封装返回
return new PageResult(total, lists);
}

重启后测试,发现页面能成功显示距离了(假设此时我的位置在南通):

【Spring Cloud】ES实战:黑马旅游案例_spring cloud_18

解释一下,前端使用v-if 指令(vue),就是说​distance​值为​xxx​(有值 -> 元素存在于​dom树​中)的时候该标签才生效:

【Spring Cloud】ES实战:黑马旅游案例_spring cloud_19

返回顶部


1.4 酒店竞价排名

需求:让指定的酒店在搜索结果中排名置顶

1.4.1 需求分析

要让指定酒店在搜索结果中排名置顶,效果如图:

【Spring Cloud】ES实战:黑马旅游案例_spring cloud_20

页面会给指定的酒店添加广告标记。

我们之前学习过的function_score查询可以影响算分,算分高了,自然排名也就高了。而​function_score​包含3个要素:

  • 过滤条件:哪些文档要加分
  • 算分函数:如何计算​​function score​
  • 加权方式:​​function score​​​ 与​​ query score​​如何运算

这里的需求是:让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。

比如,我们给酒店添加一个字段:isAD​Boolean​类型:

  • ​true​​:是广告
  • ​false​​:不是广告

这样function_score包含3个要素就很好确定了:

  • 过滤条件:判断​​isAD​​​是否为​​true​
  • 算分函数:我们可以用最简单暴力的​​weight​​,固定加权值
  • 加权方式:可以用默认的相乘,大大提高算分

因此,业务的实现步骤包括:

  1. 给​​HotelDoc​​类添加​​isAD​​字段,​​Boolean​​类型
  2. 挑选几个你喜欢的酒店,给它的文档数据添加​​isAD​​字段,值为​​true​
  3. 修改​​search​​方法,添加​​function score​​功能,给​​isAD​​值为​​true​​的酒店增加权重

返回顶部


1.4.2 修改HotelDoc实体

cn.itcast.hotel.pojo包下的HotelDoc类添加isAD字段:

@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
// 排序时的 距离值
private Object distance;
private Boolean isAD;
}

返回顶部


1.4.3 添加广告标记

接下来,我们挑几个酒店,添加isAD字段,设置为true:

POST /hotel/_update/2056126831
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/1989806195
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/2056105938
{
"doc": {
"isAD": true
}
}

返回顶部


1.4.4 添加算分函数查询

接下来我们就要修改查询条件了。之前是用的 boolean 查询,现在要改成 ​function_socre​查询。

function_score查询结构如下:

【Spring Cloud】ES实战:黑马旅游案例_旅游_21

对应的Java API如下:

【Spring Cloud】ES实战:黑马旅游案例_搜索_22

我们可以将之前写的boolean查询作为原始查询条件放到query中,接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。

修改cn.itcast.hotel.service.impl包下的​HotelService​类中的​buildBasicQuery​方法,添加算分函数查询:

private void buildBasicQuery(RequestParams params, SearchRequest request) {
// 1.构建BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键字搜索
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.termQuery("city", params.getCity()));
}
// 品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// 星级条件
if (params.getStarName() != null && !params.getStarName().equals("")) {
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// 价格
if (params.getMinPrice() != null && params.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders
.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}

// 2.算分控制
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查询,相关性算分的查询
boolQuery,
// function score的数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一个function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 过滤条件
QueryBuilders.termQuery("isAD", true),
// 算分函数
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
request.source().query(functionScoreQuery);
}

重启后可以发现,我们上面选择的三个酒店就置顶了:

【Spring Cloud】ES实战:黑马旅游案例_elasticsearch_23

解释一下,前端使用v-if 指令(vue),就是说​isAD​值为​true​(有值 -> 元素存在于​dom树​中)的时候该标签才生效:

【Spring Cloud】ES实战:黑马旅游案例_搜索_24

返回顶部