SpringCloud(8)— 使用ElasticSearch(RestClient)
一 认识RestClient
ES 官方提供了各种语言的客户端用来操作ES,这些客户端的本质就是组创DSL语句,通过 Http 请求发送给ES
官方文档地址:Elasticsearch Clients | Elastic
提示1:ES 中支持两种地理坐标数据类型
- geo_point:ES中的 mapping 类型中利用经度和纬度确定一个点
- geo_shape:由多个 geo_point 组成的复杂几何图形
提示2:字段拷贝可以使用 copy_to 属性将当前字段的属性值拷贝到指定的属性上,方便以后搜索,且被指定的字段在查询数据时对外不可见
例如:以下示例中的 all 字段,在后边查询数据时不会返回
{
"all":{
"type":"text",
"analyzer":"ik_max_word"
},
"brand":{
"type":"keyword",
"copy_to":"all"
}
}
二 RestClient操作索引库
1.初始化RestClient
在项目中引入 RestClient 的 mavne 依赖坐标,版本需与 ElasticSearch 的版本一致。这里使用的是 7.12.1
<properties>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
</dependencies>
由于 SpringBoot 项目中已经对 elasticsearch 进行管理,所以需要手动定义相同的参数值去覆盖默认的版本号,从而使用我们想使用的版本
编写测试代码,测试 RestHighLevelClient 是否初始化成功
private RestHighLevelClient restHighLevelClient;
//初始化 RestHighLevelClient 对象
@BeforeEach
void setUp(){
this.restHighLevelClient=new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.119.101:9200")
));
}
//销毁 RestHighLevelClient 对象
@AfterEach
void tearDown() throws IOException {
this.restHighLevelClient.close();
}
@Test
void testInit(){
System.out.println(restHighLevelClient);
}
或者可以使用 Ioc 思想,创建一个 Bean 到内存中,然后使用它即可
@Bean
public RestHighLevelClient restHighLevelClient(){
return new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.119.101:9200")
));
}
2.创建索引库
编写一个测试方法,用来测试创建索引库是否成功
@Test
void testCreateIndex() throws IOException {
//1.创建一个 CreateIndexRequest 对象,指定 indexName(索引库名称)
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 这是一段很恶心的 DSL 语句的拼接,为了方便可以将这一块代码提了出来,这里为了方便演示不做优化处理
String mapping ="{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" },\n" +
" \"id\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"copy_to\": \"all\", \n" +
" \"analyzer\": \"ik_max_word\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"double\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"isAD\":{\n" +
" \"type\": \"boolean\"\n" +
" }\n" +
" }\n" +
" }\n" +
"} ";
//2.定义 DSL
request.source(mapping, XContentType.JSON);
//3.创建索引库
restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
}
运行完成后,可以去浏览器通过 GET 方法访问,如果返回对应的JSON格式信息,则说明创建索引库成功。
# 浏览器访问
http://192.168.119.101:9200/hotel
# kibana 中的 dev tools 中测试
GET /hotel
CreateIndexRequest 对象中的 indices() 对象里,包含了操作索引库的所有方法
3.删除索引库
@Test
void testDeleteIndex() throws IOException {
// 定义 DeleteIndexRequest 对象
DeleteIndexRequest request=new DeleteIndexRequest("hotel");
// 执行删除操作
restHighLevelClient.indices().delete(request,RequestOptions.DEFAULT);
}
4.判断指定索引库是否存在
@Test
void testDeleteExist() throws IOException {
// 定义 GetIndexRequest 对象
GetIndexRequest request=new GetIndexRequest("hotel");
// 发送请求
restHighLevelClient.indices().exists(request,RequestOptions.DEFAULT);
}
三 RestClient操作文档
1.添加文档
编写测试方法,从数据库查询到指定数据,并且格式化为 Json,然后将数据存储到 特定的索引库中
Hotel 实体对象示例:
@Data
public class Hotel implements Serializable {
private final static Long serialVersionUID=1L;
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String city;
private String starName;
private String brand;
private String business;
private String latitude;
private String longitude;
private String pic;
/**
* 是否广告,1-是,0-否
*/
@TableField("isAD")
private Integer isAD;
}
HotelDoc 文档实体对象:
注意:文档的实体应该与创建时的 mapping 字段完全一致
@Data
public class HotelDoc implements Serializable {
public HotelDoc() {
}
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();
// 使用字符串格式的 geo_point 时,要求“纬度在前,经度在后”
this.location = hotel.getLatitude() + "," + hotel.getLongitude();
this.pic = hotel.getPic();
//处理广告字段(isAD)格式
if (hotel.getIsAD() == 1) {
this.isAD = true;
} else {
this.isAD = false;
}
}
private final static Long serialVersionUID = 1L;
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String city;
private String starName;
private String brand;
private String business;
private String pic;
private String location;
/**
* 广告
*/
public Boolean isAD;
}
添加一条文档到索引库中:
@Test
void testAddDocument() throws IOException {
Long id = 36934L;
Hotel hotel = hotelService.getById(id);
if (Objects.nonNull(hotel)) {
// 1.创建 IndexRequest 对象,指定文档要存入的【索引库】及【文档Id】,此处的【文档Id】使用数据库的【主键Id】
IndexRequest request = new IndexRequest("hotel").id(id.toString());
// 2.准备 JSON 数据
HotelDoc hotelDoc = new HotelDoc(hotel);
String jsonString = JSON.toJSONString(hotelDoc);
request.source(jsonString, XContentType.JSON);
// 3.发送请求
restHighLevelClient.index(request, RequestOptions.DEFAULT);
return;
}
System.out.println("id=" + id + "的信息不存在");
}
踩坑底:对于 mapping 类型为 geo_point 的经纬度,使用字符串格式时,需要“纬度在前经度在后
运行成功后,数据将被存入到 ES 中
2.查看文档
编写查看文档的示例代码,重点API 【GetRequest】
@Test
void testGetDocument() throws IOException {
Long id = 36934L;
// 1.创建 GetRequest 对象,指定【索引库】和【文档ID】
GetRequest request = new GetRequest("hotel", id.toString());
// 2.发送请求,读取到数据
GetResponse documentFields = restHighLevelClient.get(request, RequestOptions.DEFAULT);
String sourceStr = documentFields.getSourceAsString();
//3. 使用fastJson转为实体对象
HotelDoc hotelDoc = JSON.parseObject(sourceStr, HotelDoc.class);
System.out.println(hotelDoc);
}
3.修改文档
编写修改文档的示例代码,重点API 【UpdateRequest】
@Test
void testUpdateDocument() throws IOException {
Long id=36934L;
// 1.创建 UpdateRequest 对象,指定【索引库】和 【文档Id】
UpdateRequest request=new UpdateRequest("hotel",id.toString());
// 2.准备要更新的参数和值,每两个参数为一对 key value
request.doc(
"price",200,
"name","8天假日酒店"
);
// 3.发送请求
restHighLevelClient.update(request,RequestOptions.DEFAULT);
}
4.删除文档
编写删除文档的示例代码,重点API 【DeleteRequest】
@Test
void testDeleteDocument() throws IOException {
Long id=36934L;
// 1.创建 DeleteRequest 对象,指定【索引库】和 【文档Id】
DeleteRequest deleteRequest=new DeleteRequest("hotel",id.toString());
// 2.发送请求
restHighLevelClient.delete(deleteRequest,RequestOptions.DEFAULT);
}
5.批量导入文档
编写批量导入测试方法,重点API 【BulkRequest】
@Test
void tesBulkDocument() throws IOException {
// 1.定义 BulkRequest 对象
BulkRequest request=new BulkRequest();
List<Hotel> list = hotelService.list();
// 2.查询所有数据并且转换为 List<HotelDoc>
List<HotelDoc> listDoc=list.stream().map(t->{
HotelDoc doc=new HotelDoc(t);
return doc;
}).collect(Collectors.toList());
// 3.遍历 List<HotelDoc> 对象,创建 IndexRequest 并且通过 add() 添加到 BulkRequest 对象中
for (HotelDoc doc: listDoc) {
IndexRequest indexRequest=new IndexRequest("hotel").id(doc.getId().toString());
indexRequest.source(JSON.toJSONString(doc),XContentType.JSON);
request.add(indexRequest);
}
// 4.发送 ES 请求
restHighLevelClient.bulk(request,RequestOptions.DEFAULT);
}
在 kibana 控制台通过查询命令可以查询到已经导入数据
#1.hotel为索引库名称
GET /hotel/_search
四 RestClient查询
RestClient 所有解析的归根结底是逐层解析 Json 结构
1.快速入门(match_all)
通过 match_all 演示 RestClient 的基本API
编写以下测试代码,实现 match_all 的使用
@Test
void testMatchAll() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request=new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(QueryBuilders.matchAllQuery())
.from(0)
.size(3);
//3.发送请求,得到响应结果
SearchResponse response=restHighLevelClient.search(request, RequestOptions.DEFAULT);
//4. 通过 SearchResponse 对象的 getHits 拿到数据结果集,然后进行处理
SearchHits hits = response.getHits();
//5.获取数据总条数
Long totalHits = hits.getTotalHits().value;
System.out.println(totalHits);
//6.处理结果
for (SearchHit hit : hits) {
HotelDoc doc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
System.out.println(doc.toString());
}
}
RestClient 与 DSL 语法对比:
- 使用 QueryBuilders 实现各种查询方式
- 通过 SearchRequest 对象的 source() 对象实现链式编程,从而设置分页,排序,高亮,复合查询等操作
- 通过 RestHighLevelClient 对象的 search() 方法发送查询请求,实现查询
- 通过 SearchResponse 对象接受查询结果并进行处理
- 通过 SearchHits 对象拿到结果集和数据总行数
主要通过解析 DSL 语法返回结果集中的 hits 对象,获取到总条数及数据内容,从而对数据进行处理
2.全文检索
全文检索 与 match_all 的API基本一致,差别在于 match 有了查询条件,即 DSL 语句中的 query 部分。
1.match(特定字段)
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(QueryBuilders.matchQuery("all", "如家"));
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
2.multi_match(多字段)
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(QueryBuilders.multiMatchQuery("如家","name","brand"))
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
match 和 multi_match 的区别主要在于 QueryBuilders 类的调用方法不同
match 中 QueryBuilders 使用了 matchQuery()
multi_match 中 QueryBuilders 使用了 multiMatchQuery()
3.term(精准查询)
与 前面的相比,唯一的区别是使用了 termQuery()
@Test
void testMatch() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source().query(QueryBuilders.termQuery("brand","如家"));
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
3.boolean query(组合)
boolean query 查询 与 前面的查询略有不同,需要先创建 BoolQueryBuilder 对象,然后通过 BoolQueryBuilder 对象添加过滤条件
@Test
void testBooleanQuery() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.创建 BoolQueryBuilder 对象,通过链式编程方式添加查询条件
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
queryBuilder
.should(QueryBuilders.matchQuery("city", "上海"))
.must(QueryBuilders.termQuery("brand", "如家"))
.filter(QueryBuilders.rangeQuery("price").gte(200).lte(300));
//3.组织 DSL 参数,填充 BoolQueryBuilder对象到 query中
request.source()
.query(queryBuilder);
//4.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
或者可以直接一步到位
@Test
void testBooleanQuery() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(new BoolQueryBuilder()
.should(QueryBuilders.matchQuery("city", "上海"))
.must(QueryBuilders.termQuery("brand", "如家"))
.filter(QueryBuilders.rangeQuery("price").gte(200).lte(300))
);
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
4.排序和分页
DSL 语句中,排序和分页 在 query 同级,RestClient 中与 DSL 一样,也是在 query() 之后
@Test
void testPageAndOrder() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(new BoolQueryBuilder()
.should(QueryBuilders.matchQuery("city", "上海"))
.must(QueryBuilders.termQuery("brand", "如家"))
.filter(QueryBuilders.rangeQuery("price").gte(200).lte(300))
)
.from(0)
.size(3)
.sort("score", SortOrder.DESC)
.sort("price",SortOrder.ASC);
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
- from:开始位置,默认为0
- size:获取的数据数量,默认为10
- sort:使用 sort() 方法来进行排序,传入 字段名 和 排序方式
5.highlight(高亮显示)
高亮API包括 请求DSL构建 和 结果解析 两部分
测试代码实现如下:
@Test
void testHighLight() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(QueryBuilders.matchQuery("all", "如家"))
.highlighter(
//高亮字段是否和查询字段匹配
new HighlightBuilder().field("name").requireFieldMatch(false).preTags("<em>").postTags("</em>")
);
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
//4. 通过 SearchResponse 对象的 getHits 拿到数据结果集,然后进行处理
SearchHits hits = response.getHits();
//5.获取数据总条数
Long totalHits = hits.getTotalHits().value;
System.out.println(totalHits);
for (SearchHit hit : hits) {
HotelDoc doc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
// 获取高亮区域的值
Map<String, HighlightField> fields = hit.getHighlightFields();
if (fields != null) {
// 获取指定字段的高亮内容对象
HighlightField highlightField = fields.get("name");
String highLightName = highlightField.getFragments()[0].toString();
// 替换原有的值
doc.setName(highLightName);
}
System.out.println(doc);
}
}
6.距离排序
按照距离排序,需要提供一组经纬度作为中心点,然后按照距离远近进行排序
距离排序中使用到一个新的对象,即 SortBuilders,以下是测试示例代码:
@Test
void testDistanceSort() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
//2.组织 DSL 参数
request.source()
.query(QueryBuilders.matchQuery("all", "如家"))
.sort(
SortBuilders.geoDistanceSort("location", new GeoPoint("31.251433,121.47522"))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
7.相关性算分
使用 RestClient 实现相关性算分时代码较为复杂,先看对照示例:
实现方式是基于 FunctionScoreQueryBuilder 对象
以下是测试代码,用于过滤 isAD=true 时,增加权重分值为10,且与原始分值相乘
@Test
void testFunctionScore() throws IOException {
//1.创建 SearchRequest 对象,指定索引库名称
SearchRequest request = new SearchRequest("hotel");
// 原始查询,可以是一般查询,也可以是复合查询 BooleanQuery
QueryBuilder queryBuilder = QueryBuilders.matchQuery("all", "如家");
// 构建 FunctionScoreQueryBuilder 对象
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
//方式查询
queryBuilder,
//functionScore 数组
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中一个 function score
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
//过滤条件
QueryBuilders.termQuery("isAD", true),
//算分函数,这里使用加权重
ScoreFunctionBuilders.weightFactorFunction(20)
)
}
)// 加权模式
.boostMode(CombineFunction.MULTIPLY);
;
//2.组织 DSL 参数
request.source().query(functionScoreQueryBuilder)
;
//3.发送请求,得到响应结果
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
注意:后边的 “isAD” 字段 是为了做相关性算分增加的新字段,前边的 索引库 和 实体类 中没有,需要手动增加。
2022-12-21,修改 “创建索引库”时未设置分词器的问题,Hotel 和 HotelDoc 实体类中添加 “isAD” 字段
本结知识点梳理完成,完结撒花。