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 语法对比:

restcloud etl 入门_elasticsearch

  • 使用 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() 方法来进行排序,传入 字段名 和 排序方式

restcloud etl 入门_Test_02

5.highlight(高亮显示)

高亮API包括 请求DSL构建 和 结果解析 两部分

restcloud etl 入门_elasticsearch_03

测试代码实现如下:

@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 实现相关性算分时代码较为复杂,先看对照示例:

restcloud etl 入门_spring cloud_04

实现方式是基于 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” 字段

本结知识点梳理完成,完结撒花。