RestAPI
matchAllQuery 查询所有
//查询
@Test
public void testMatchAll() throws IOException {
//1. 构建查询请求
SearchRequest request = new SearchRequest("hotel");
//2. 设置查询条件
request.source().query(
QueryBuilders.matchAllQuery() //match_all
);
//3. 执行请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
System.out.println("结果数量:" + response.getHits().getTotalHits().value);//hits.total.value
SearchHit[] searchHitArr = response.getHits().getHits();
for (SearchHit searchHit : searchHitArr) {
String source = searchHit.getSourceAsString();//hits.hits.get(n).source
System.out.println(JSON.parseObject(source, HotelDoc.class));
}
}
elasticsearch返回的结果是一个JSON字符串,结构包含:
hits
:命中的结果total
:总条数,其中的value是具体的总条数值max_score
:所有结果中得分最高的文档的相关性算分hits
:搜索结果的文档数组,其中的每个文档都是一个json对象_source
:文档中的原始数据,也是json对象
因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:
SearchHits
:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果SearchHits.getTotalHits().value
:获取总条数信息SearchHits.getHits()
:获取SearchHit数组,也就是文档数组SearchHit.getSourceAsString()
:获取文档结果中的_source,也就是原始的json文档数据
matchQuery&multiMatchQuery全文检索
//查询 match
//2. 设置查询条件
request.source().query(
QueryBuilders.matchQuery("all", "外滩如家") //match
);
//查询 multi_match
//2. 设置查询条件
request.source().query(
QueryBuilders.multiMatchQuery("外滩如家", "brand", "business", "name")
);
termQuery 精确查询
词条查询
//词条查询 term
//2. 设置查询条件
request.source().query(
QueryBuilders.termQuery("city", "北京")
);
rangeQuery 范围查询
//2. 构建条件,并添加到查询请求
request.source().query(
rangeQuery("price")
.gte(1000)
.lte(2000));
地理查询
geoBoundingBoxQuery 矩形范围
//2. 设置查询条件
request.source().query(
QueryBuilders.geoBoundingBoxQuery("location")
.setCorners(new GeoPoint(40.08, 116.47), new GeoPoint(39.9, 116.405))
);
geoDistanceQuery 圆形范围
//2. 设置查询条件
request.source().query(
QueryBuilders.geoDistanceQuery("location")
.point(39.9, 116.405)//设置圆心坐标
.distance("15km")//设置半径
);
functionScoreQuery 算法函数
//2. 设置查询条件
request.source().query(
QueryBuilders.functionScoreQuery(
QueryBuilders.matchQuery("all", "北京"),//设置查询条件
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ //设置算分函数
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("brand", "凯悦"),
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
).boostMode(CombineFunction.MULTIPLY) //设置算法策略
);
boolQuery 复合查询
//2. 设置查询条件
request.source().query(
QueryBuilders.boolQuery().must(
QueryBuilders.termQuery("name", "如家")
).mustNot(
QueryBuilders.rangeQuery("price").gte(400)
).filter(
QueryBuilders.geoDistanceQuery("location")
1 .point(39.9, 116.405)//设置圆心坐标
.distance("10km")//设置半径
)
);
排序 分页
//2. 设置查询条件
request.source().query(QueryBuilders.matchAllQuery());
request.source().sort("price", SortOrder.ASC);//排序,按照价格正序排
request.source().from(0).size(10);//分页,从0查,查10个;
高亮
高亮的结果与查询的文档结果默认是分离的,并不在一起,因此解析高亮的代码需要额外处理:
//高亮
@Test
public void testHighLight() throws IOException {
//1. 构建查询请求
SearchRequest request = new SearchRequest("hotel");
//2. 设置查询条件
request.source().query(QueryBuilders.matchQuery("all", "如家"));
//3. 设置高亮效果
request.source().highlighter(
new HighlightBuilder()
.field("name")
.preTags("<em>")
.postTags("</em>").
requireFieldMatch(false)
);
//4. 执行请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//5. 解析结果
SearchHit[] searchHits = response.getHits().getHits();
for (SearchHit searchHit : searchHits) {
//没有效果的对象
HotelDoc hotelDoc = JSON.parseObject(searchHit.getSourceAsString(), HotelDoc.class);
//解析效果,替换上面对象中的title的值
String name = (searchHit.getHighlightFields().get("name").getFragments())[0].toString();
hotelDoc.setName(name);
System.out.println(hotelDoc);
}
}
案例:酒店搜索
下面,我们通过一个旅游网站的搜索案例来实战演练下之前学习的知识,我们实现四部分功能:
- 酒店分页搜索
- 酒店结果过滤
- 我周边的酒店
- 酒店竞价排名
启动我们提供的hotel-demo项目,其默认端口是8089,访问http://localhost:8089,就能看到项目页面了
请求的信息如下:
- 请求方式:POST
- 请求路径:/hotel/list
- 请求参数:JSON对象,包含4个字段:
- key:搜索关键字
- page:页码
- size:每页大小
- sortBy:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。我们在com.itheima.hotel.pojo
包下创建这两个实体类:
package com.itheima.hotel.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
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;
}
package com.itheima.hotel.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult {
private Long total;
private List<HotelDoc> hotels;
}
添加启动类
实现搜索业务,肯定离不开RestHighLevelClient,我们需要把它注册到Spring中作为一个Bean。
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.136.134:9200")
));
}
HotelController
package com.itheima.hotel.controller;
import com.itheima.hotel.pojo.PageResult;
import com.itheima.hotel.pojo.RequestParams;
import com.itheima.hotel.service.HotelService;
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;
import java.io.IOException;
@RestController
@RequestMapping("/hotel")
public class HotelController {
@Autowired
private HotelService hotelService;
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams requestParams) throws IOException {
return hotelService.search(requestParams);
}
}
HotelService
package com.itheima.hotel.service;
import com.itheima.hotel.pojo.PageResult;
import com.itheima.hotel.pojo.RequestParams;
import java.io.IOException;
public interface HotelService {
PageResult search(RequestParams requestParams) throws IOException;
}
HotelServiceImpl
package com.itheima.hotel.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.itheima.hotel.pojo.HotelDoc;
import com.itheima.hotel.pojo.PageResult;
import com.itheima.hotel.pojo.RequestParams;
import com.itheima.hotel.service.HotelService;
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.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public class HotelServiceImpl implements HotelService {
@Autowired
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams requestParams) throws IOException {
//1. 构建查询请求
SearchRequest request = new SearchRequest("hotel");
//2. 设置查询条件
//2-1创建复合查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//2-2 获取搜索关键字, 设置为查询条件
String key = requestParams.getKey();
if (StringUtils.isEmpty(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
request.source().query(boolQuery);
//3. 设置分页
Integer pageNum = requestParams.getPage();
Integer pageSize = requestParams.getSize();
request.source().from((pageNum - 1) * pageSize).size(pageSize);
//4. 设置排序
//5. 设置高亮
request.source().highlighter(
new HighlightBuilder()
.field("name")
.preTags("<em>").postTags("</em>")
.requireFieldMatch(false)
);
//6. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//7. 处理结果
return handleResponse(response);
}
//处理结果
private PageResult handleResponse(SearchResponse response) {
//准备结果集合
List<HotelDoc> hotels = new ArrayList<>();
SearchHits searchHits = response.getHits();
//获取总记录数
long total = searchHits.getTotalHits().value;
//获取文档数组
SearchHit[] hits = searchHits.getHits();
//遍历文档, 获取结果, 处理高亮, 并且添加到返回结果结合
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 处理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (CollectionUtil.isNotEmpty(highlightFields)) {
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
}
hotels.add(hotelDoc);
}
//组装返回结果
return new PageResult(total, hotels);
}
}
酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
需求分析:
在页面搜索框下面,会有一些过滤项:
包含的过滤条件有:
- brand:品牌值
- city:城市
- minPrice~maxPrice:价格范围
- starName:星级
修改搜索业务
修改业务逻辑,在搜索条件之外,添加一些过滤条件
在之前的业务中,只有match查询,根据关键字搜索,现在要添加条件过滤,包括:
- 城市过滤:是keyword类型,用term查询
- 星级过滤:是keyword类型,用term查询
- 品牌过滤:是keyword类型,用term查询
- 价格过滤:是数值类型,用range查询
多个查询条件组合,肯定是boolean查询来组合:
- 关键字搜索放到must中,参与算分
- 其它过滤条件放到filter中,不参与算分
@Override
public PageResult list(RequestParams requestParams) throws IOException {
//1. 构建查询请求
SearchRequest request = new SearchRequest("hotel");
//2. 设置查询条件
//2-1创建复合查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//2-2 获取搜索关键字, 设置为查询条件
String key = requestParams.getKey();
if (StringUtils.isEmpty(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
//2-3 获取城市、星级、品牌、价格,使用过滤语法筛选
// 城市
if (StrUtil.isNotEmpty(requestParams.getCity())) {
boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
}
// 星级
if (StrUtil.isNotEmpty(requestParams.getStarName())) {
boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
}
// 品牌
if (StrUtil.isNotEmpty(requestParams.getBrand())) {
boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
}
// 价格
if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice()));
}
request.source().query(boolQuery);
//3. 设置分页
Integer pageNum = requestParams.getPage();
Integer pageSize = requestParams.getSize();
request.source().from((pageNum - 1) * pageSize).size(pageSize);
//4. 设置排序
//5. 设置高亮
request.source().highlighter(
new HighlightBuilder()
.field("name")
.preTags("<em>").postTags("</em>")
.requireFieldMatch(false)
);
//6. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//7. 处理结果
return handleResponse(response);
}
我周边的酒店
需求:让检索到的酒店根据我所处的地理位置进行排序,优先显示附近的酒店
需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置:
并且,在前端会发起查询请求,将你的坐标发送到服务端:
注意: Chrome和FireFox浏览器有时候不会发生查询请求,所有此功能使用Windows自带的Edge浏览器就行测试
添加具体排序
注意:距离的排序跟普通字段的排序在api上有些区别
修改search方法
@Override
public PageResult list(RequestParams requestParams) throws IOException {
//1. 构建查询请求
SearchRequest request = new SearchRequest("hotel");
//2. 设置查询条件
//2-1创建复合查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//2-2 获取搜索关键字, 设置为查询条件
String key = requestParams.getKey();
if (StringUtils.isEmpty(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
//2-3 获取城市、星级、品牌、价格,使用过滤语法筛选
// 城市
if (StrUtil.isNotEmpty(requestParams.getCity())) {
boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
}
// 星级
if (StrUtil.isNotEmpty(requestParams.getStarName())) {
boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
}
// 品牌
if (StrUtil.isNotEmpty(requestParams.getBrand())) {
boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
}
// 价格
if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice()));
}
request.source().query(boolQuery);
//3. 设置分页
Integer pageNum = requestParams.getPage();
Integer pageSize = requestParams.getSize();
request.source().from((pageNum - 1) * pageSize).size(pageSize);
//4. 设置排序
String location = requestParams.getLocation();
if (StrUtil.isNotEmpty(location)) {
request.source().sort(
SortBuilders.geoDistanceSort("location", new GeoPoint(location))//设置核心坐标位置
.order(SortOrder.ASC) //设置排序方式
.unit(DistanceUnit.KILOMETERS)// 单位
);
}
//5. 设置高亮
request.source().highlighter(
new HighlightBuilder()
.field("name")
.preTags("<em>").postTags("</em>")
.requireFieldMatch(false)
);
//6. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//7. 处理结果
return handleResponse(response);
}
排序距离显示
按照距离排序后,还需要显示具体的距离值:
修改handleResponse方法
//处理结果
private PageResult handleResponse(SearchResponse response) {
//准备结果集合
List<HotelDoc> hotels = new ArrayList<>();
SearchHits searchHits = response.getHits();
//获取总记录数
long total = searchHits.getTotalHits().value;
//获取文档数组
SearchHit[] hits = searchHits.getHits();
//遍历文档, 获取结果, 处理高亮, 并且添加到返回结果结合
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// 处理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (CollectionUtil.isNotEmpty(highlightFields)) {
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
}
// 处理距离显示
Object[] sortValues = hit.getSortValues();
if (ArrayUtil.isNotEmpty(sortValues)) {
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue);
}
hotels.add(hotelDoc);
}
//组装返回结果
return new PageResult(total, hotels);
}
酒店竞价排名
需求:让指定的酒店(广告)在搜索结果中排名置顶
要让指定酒店在搜索结果中排名置顶,效果如图:
我们给需要置顶的酒店文档添加一个标记。然后利用function_score给带有标记的文档增加权重。
实现步骤分析:
1. 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
2. 修改search方法,添加function_score功能,给isAD值为true的酒店增加权重
添加广告标记
接下来,我们挑几个酒店,添加isAD字段,设置为true:
POST /hotel/_update/2056126831
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/1989806195
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/2056105938
{
"doc": {
"isAD": true
}
}
添加算分函数
@Override
public PageResult list(RequestParams requestParams) throws IOException {
//1. 构建查询请求
SearchRequest request = new SearchRequest("hotel");
//2. 设置查询条件
//2-1创建复合查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//2-2 获取搜索关键字, 设置为查询条件
String key = requestParams.getKey();
if (StringUtils.isEmpty(key)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
//2-3 获取城市、星级、品牌、价格,使用过滤语法筛选
// 城市
if (StrUtil.isNotEmpty(requestParams.getCity())) {
boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
}
// 星级
if (StrUtil.isNotEmpty(requestParams.getStarName())) {
boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
}
// 品牌
if (StrUtil.isNotEmpty(requestParams.getBrand())) {
boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
}
// 价格
if (requestParams.getMinPrice() != null && requestParams.getMaxPrice() != null) {
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(requestParams.getMinPrice()).lte(requestParams.getMaxPrice()));
}
//2-4 使用算分函数 提供广告酒店的排名
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(boolQuery,
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("isAD", true),
ScoreFunctionBuilders.weightFactorFunction(5))
}
);
request.source().query(functionScoreQuery);
//3. 设置分页
Integer pageNum = requestParams.getPage();
Integer pageSize = requestParams.getSize();
request.source().from((pageNum - 1) * pageSize).size(pageSize);
//4. 设置排序
String location = requestParams.getLocation();
if (StrUtil.isNotEmpty(location)) {
request.source().sort(
SortBuilders.geoDistanceSort("location", new GeoPoint(location))//设置核心坐标位置
.order(SortOrder.ASC) //设置排序方式
.unit(DistanceUnit.KILOMETERS)// 单位
);
}
//5. 设置高亮
request.source().highlighter(
new HighlightBuilder().field("name").requireFieldMatch(false)
);
//6. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//7. 处理结果
return handleResponse(response);
}