【Elasticsearch】7. Spring Boot整合ES

·  阅读 631

ES在项目中的使用思路

以为ES在处理事务(数据一致性)方面比Database要若很多,所以在项目实战中,ES需要和Database配合使用。

ES中存储的数据相当于Database中对应数据的简略版,只可用于搜索结果展示,真正获取详细的信息还得去Database中获取。

如果不使用ES生态,使用原生ES,那么项目中对于数据的操作如下:

20210818004009.png

数据一致性

在ES和Database数据的同步时,存在两种数据一致性:

  • 数据强一致性
  • 数据最终一致性

数据强一致性表示,Database中的数据和ES中的数据保证实时一致,也就是说Database数据变更后立即同步到ES,数据的同步存在实时性

数据最终一致性表示,Database中的数据和ES中的数据在过了某个特定的时间段之后保证一致,也就是说Database数据变更后隔一段时间再同步到ES,数据的同步存在延时性

1. 数据一致性的实现

数据强一致性的具体实现,可以在Database更新数据时,同时调用Java代码更新ES中的数据,这样做会导致效率低而且不易维护。在企业级解决方案中,会将ES分成一个独立的服务,并配合消息队列实现ES数据的同步更新。

数据最终一致性的具体实现,可以设置定时任务,设置在每天某个并发量低的时刻,参考Database同步ES中的数据。

2. 数据一致性的选择

选择选择强一致性还是最终一致性得看具体的业务,如果该业务的数据实时更新很重要,比如商品价格的调整,那么需要使用强一致性。如果数据的实时更新不那么重要,比如一个商品的日访问量,那么就使用最终一致性。

Spring Boot整合ES

1. Spring Data

Spring Boot整合ES需要另外一个Spring开源的项目,Spring Data。

Spring Data官网:spring.io/projects/sp…

Spring Data项目的目的是为了简化构建基于Spring框架应用的数据访问计数,包括非关系数据库、Map-Reduce 框架、云数据服务等等;另外也包含对关系数据库的访问支持。

20210818115102.png

常见的子项目有:

  • Spring Data JDBC:对JDBC的Spring Data存储库支持。
  • Spring Data JPA:对JPA的Spring Data存储库支持。
  • Spring Data MongoDB:对MongoDB的基于Spring对象文档的存储库支持。
  • Spring Data Redis:从Spring应用程序轻松配置和访问Redis。
  • Spring Data Elasticsearch:从Spring应用程序轻松配置和访问Elasticsearch。

Spring Boot整合ES实际上是Spring Boot整合了Spring Data,通过Spring Data来操作Elasticsearch。

2. 基础环境搭建

  1. Spring Boot版本:2.2.5.RELEASE

  2. 在pom.xml中引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    复制代码
  3. 创建config包

  4. 创建RestClient配置类

    @Configuration
    public class RestClientConfig extends AbstractElasticsearchConfiguration {
    
        @Override
        @Bean
        public RestHighLevelClient elasticsearchClient() {
    
            // 定义ES客户端对象:ip + port
            final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                    .connectedTo("localhost:9200")
                    .build();
    
            return RestClients.create(clientConfiguration).rest();
        }
    
    }
    复制代码

3. 操作ES两种方式

在Spring data 2.x ~ 3.x 时,推荐使用ElasticTemplate来操作ES,ElasticTemplate底层调用的是TransportClient,使用的是ES的TCP端口9300,但是TransportClient在ES 6.x ~ 7.x 时就已经不推荐使用,在 8.x 已经废弃。所以在最新版的Spring Data 4.x中,已经弃用ElasticTemplate,推荐使用RestHighLevelClient(高等级REST客户端)和ElasticsearchRepository接口来操作ES,使用的是ES的Web端口9200,类似于Kibana。

  • RestHighLevelClient:用来实现ES的复杂检索。
  • ElasticsearchRepository:用来实现ES的常规操作。

通常只用RestHighLevelClient完成高亮检索,剩下的都可以用ElasticsearchRepository完成。

RestHighLevelClient操作ES

1. 注入

@Autowired
private RestHighLevelClient restHighLevelClient;
复制代码

2. 新增Document

@Test
public void saveDocument() throws IOException {
    // 构建索引请求 传入参数为:index名,type名,自定义该Document的_id
    IndexRequest indexRequest = new IndexRequest("postilhub", "user", "1");

    // 传入参数为:新增Document的数据,数据类型
    indexRequest.source("{\"id\":\"1\",\"username\":\"小明\",\"age\":19}", XContentType.JSON);

    // 执行新增 RequestOptions.DEFAULT为枚举类型,默认即可
    IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);

    // 查看操作是否成功
    System.out.println(indexResponse.status());
}
复制代码

3. 删除Document

@Test
public void deleteDocument() throws IOException {
    // 构建删除请求 传入参数为:index名,type名,该Document的_id
    DeleteRequest deleteRequest = new DeleteRequest("postilhub", "user", "1");

    // 执行删除 RequestOptions.DEFAULT为枚举类型,默认即可
    DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);

    // 查看操作是否成功
    System.out.println(deleteResponse.status());
}
复制代码

4. 更新Document

@Test
public void updateDocument() throws IOException {
    // 传入参数为:index名,type名,Document的_id
    UpdateRequest updateRequest = new UpdateRequest("postilhub", "user", "1");

    // 传入参数为:更新后的Document数据,数据类型
    updateRequest.doc("{\"id\":\"1\",\"username\":\"小花\",\"age\":19}", XContentType.JSON);

    // 执行更新 RequestOptions.DEFAULT为枚举类型,默认即可
    UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);

    // 查看操作是否成功
    System.out.println(updateResponse.status());
}
复制代码

注意:该更新保留原始数据。

5. 查询所有Document

@Test
public void queryAllDocuments() throws IOException {
    // 搜索条件构造器 设置搜索条件为:matchAll
    SearchSourceBuilder builder = new SearchSourceBuilder();
    builder.query(QueryBuilders.matchAllQuery());

    // 构建搜索请求 传入参数为:index名
    SearchRequest searchRequest = new SearchRequest("postilhub");
    // 传入参数为:type名
    searchRequest.types("user").source(builder);

    // 执行检索
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // 获取TotalHits和MaxScore
    System.out.println("检索出的文档总数:" + searchResponse.getHits().getTotalHits());
    System.out.println("检索出的文档最大得分:" + searchResponse.getHits().getMaxScore());
    // 检索出的所有文档
    for (SearchHit hit : searchResponse.getHits().getHits()) {
        System.out.println(hit.getSourceAsMap());
    }
}
复制代码

6. 批量混合操作(插入+删除+更新)

@Test
public void bulk() throws IOException {
    BulkRequest bulkRequest = new BulkRequest();

    // 添加Document
    IndexRequest indexRequest = new IndexRequest("postilhub", "user", "3");
    indexRequest.source("{\"id\":3,\"username\":\"老王\",\"age\":72}", XContentType.JSON);
    // 装载indexRequest
    bulkRequest.add(indexRequest);

    // 删除Document
    DeleteRequest deleteRequest = new DeleteRequest("postilhub", "user", "2");
    // 装载deleteRequest
    bulkRequest.add(deleteRequest);

    // 更新Document
    UpdateRequest updateRequest = new UpdateRequest("postilhub", "user", "1");
    updateRequest.doc("{\"id\":\"1\",\"username\":\"老八\",\"age\":29}", XContentType.JSON);
    // 装载updateRequest
    bulkRequest.add(updateRequest);

    // 执行批量操作
    BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

    // 分别查看每一个操作是否成功
    for (BulkItemResponse item : bulkResponse.getItems()) {
        System.out.println(item.status());
    }
}
复制代码

7. 自定义查询Document

可以实现前文《ES高级检索》中所有的查询,支持链式调用组合多个查询模式。

配置不同的查询条件和查询模式,仅仅需要修改SearchSourceBuilder即可,其他地方不变。

@Test
public void conditionalQueryDocuments() throws IOException, ParseException {
    // 搜索构造器 设置组合条件
    SearchSourceBuilder builder = new SearchSourceBuilder();
    builder.query(QueryBuilders.termQuery("username", "张三"))
        // 分页 从0号document开始 每页容量为10
        .from(0).size(10)
        // 按照年龄降序排序
        .sort("age", SortOrder.DESC)
        // 设置关键字高亮 指定关键词匹配为全部字段 不开启字段匹配(如果设置关键词匹配为所有某个字段,则开启字段匹配)
        .highlighter(new HighlightBuilder().field("*").requireFieldMatch(false).preTags("\"<span style='color:red'>\"").postTags("\"</span>\""))
        // 设置范围过滤 过滤条件为:年龄大于等于15小于等于30
        .postFilter(QueryBuilders.rangeQuery("age").gte(15).lte(30));

    // 构建搜索请求 传入参数为:index名
    SearchRequest searchRequest = new SearchRequest("postilhub");
    // 传入参数为:type名
    searchRequest.types("user").source(builder);

    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // 获取TotalHits和MaxScore
    System.out.println("检索出的文档总数:" + searchResponse.getHits().getTotalHits());
    System.out.println("检索出的文档最大得分:" + searchResponse.getHits().getMaxScore());

    // 检索出的所有文档并构建Bean
    List<User> userList = new ArrayList<>();
    for (SearchHit hit : searchResponse.getHits().getHits()) {
        Map<String, Object> sourceMap = hit.getSourceAsMap();

        // 将Document转成Bean
        User user = new User();
        user.setId(hit.getId());
        user.setUsername(sourceMap.get("username").toString());
        user.setAge(Integer.parseInt(sourceMap.get("age").toString()));
        user.setBirth(new SimpleDateFormat("yyyy-MM-dd").parse(sourceMap.get("birth").toString()));
        user.setIntro(sourceMap.get("intro").toString());

        // 关键字高亮替换
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        // 判断分词字段(username,intro)数据中是否包含关键词,包含则将原关键词替换成高亮关键词
        if (highlightFields.containsKey("username")) {
            user.setUsername(highlightFields.get("username").fragments()[0].toString());
        }
        if (highlightFields.containsKey("intro")) {
            user.setIntro(highlightFields.get("username").fragments()[0].toString());
        }

        userList.add(user);
    }

    // 打印所有Document构成的Bean
    userList.forEach(System.out::println);
}
复制代码

ElasticsearchRepository操作ES

上述案例描述了如何使用RestHighLevelClient来操作ES,我们会发现一个问题,如果我们需要通过RestHighLevelClient来新增和更新Document,那么我们需要必须将Bean转换成JSON格式。当然RestHighLevelClient这种方式更强大,更灵活,但是在处理一些简单检索时,就显得有些麻烦。

ElasticsearchRepository更具有面向对象的思想,配合注解可以将Bean自动JSON序列化,不需要再把Bean手动转换成JSON格式。所以在对ES进行一些常规操作时,推荐使用ElasticsearchRepository

1. 配置

  1. 配置需要存储进ES的Bean

    @Data
    @Document(indexName = "postilhub", type = "user")
    public class User {
    
        @Id
        @Field(type = FieldType.Keyword)
        private String id;
    
        @Field(type = FieldType.Text, analyzer = "ik_max_word")
        private String username;
    
        @Field(type = FieldType.Integer)
        private Integer age;
    
        @Field(type = FieldType.Date)
        @JsonFormat(pattern="yyyy-MM-dd", timezone = "GMT+8")
        private Date birth;
        
       	@Field(type = FieldType.Text, analyzer = "ik_max_word")
        private String intro;
        
    }
    复制代码
    • @Document:构建指定index和type,并能够自动将该类构建的Bean通过JSON序列化成Document存入ES的该index的type中。
    • @Field:构建指定mapping,给某些必要的字段设置分词器。
    • @id:创建Document的同时将Bean中的id赋值给 _id。

    注意:

    1. 在第一次向ES存储该Document时,ES就会自动去ES中创建该index,type和mapping,因此指定的index和type在原ES中不能已经存在。
    2. 在开发中,ES的Bean和业务中的Bean可以共用同一个类,ES中使用的Bean只是业务中使用的Bean的一部分,所以我们只需要在需要存入ES的字段上构建mapping即可(上文构建了id,username,age三个字段)。
  2. 创建repository包

  3. 创建该Bean对应的Repository并继承ElasticsearchRepository

    public interface UserRepository extends ElasticsearchRepository<User, String> {
        
    }
    复制代码

    注意:泛型中第一个参数是该Repository对应的是哪一个Bean,第二个参数为Bean中id的类型。

  4. 在使用处注入

    @Autowired
    private UserRepository userRepository;
    复制代码

2. 新增Document

@Test
public void saveDocument() {
    // 模拟新增的User
    User user = new User();
    user.setId("1");
    user.setUsername("张三");
    user.setAge(18);
	user.setBirth(new Date());
    user.setIntro("我是张三");
    
    userRepository.save(user);
}
复制代码

3. 删除Document

@Test
public void deleteDocument() {
    userRepository.deleteById("1");
}
复制代码

4. 更新Document

save方法不仅能完成新增,还能完成更新。

当Bean的id在ES中不存在时,为新增;当Bean的id在ES中存在时,为更新。

@Test
public void updateDocument() {
    // 模拟已经更新username后的User
    User user = new User();
    user.setId("1");
    user.setUsername("小明");
    user.setAge(18);
    user.setBirth(new Date());
    user.setIntro("我是张三");

    userRepository.save(user);
}
复制代码

5. 查询指定Document

@Test
public void queryDocument() {
    // 传入Document的id
    User user = userRepository.findById("1").get();

    System.out.println(user);
}
复制代码

6. 查询所有Document

@Test
public void queryAllDocuments() {
    Iterable<User> users = userRepository.findAll();

    users.forEach(user -> {
        System.out.println(user);
    });
}
复制代码

7. 查询所有Document并排序

@Test
public void queryAllDocumentsBySort() {
    // 按照age降序排序
    Iterable<User> users = userRepository.findAll(Sort.by(Sort.Order.desc("age")));

    users.forEach(user -> {
        System.out.println(user);
    });
}
复制代码

8. 分页查询Document

@Test
public void queryDocumentsByPage() {
    // 页号从0算起
    Page<User> userPage = userRepository.search(QueryBuilders.matchAllQuery(), PageRequest.of(0, 10));

    userPage.forEach(user -> {
        System.out.println(user);
    });
}
复制代码

9. 模糊查询Document

模糊查询规则参考前文。

@Test
public void queryDocumentsByFuzzy() {
    FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("intro", "李四");

    Iterable<User> users = userRepository.search(fuzzyQueryBuilder);

    users.forEach(user -> {
        System.out.println(user);
    });
}
复制代码

10. 自定义查询Document

因为ElasticsearchRepository只提供了基本的ES操作接口,所以如果我们要使用ElasticsearchRepository完成更灵活的操作,比如我要根据某个字段关键词进行查询。

因此ElasticsearchRepository提供了一种DIY操作接口的功能,我们只需要在Repository中按照规定对接口进行命名和设计,ElasticsearchRepository会根据我们命名的接口,自动判断其功能并将其实现。

例如:我们需要根据查询出所有username字段数据中包含"张三"的Document(假设username的类型为text)

  1. 在Repository中设计接口

    接口命名规则为:findBy + 字段名

    public interface UserRepository extends ElasticsearchRepository<User, String> {
        
        List<User> findByUsername(String username);
        
    }
    复制代码
  2. 调用接口完成操作

    @Test
    public void queryDocumentsByUsername() {
        List<User> users = userRepository.findByUsername("张三");
    
        users.forEach(user -> {
            System.out.println(user);
        });
    }
    复制代码

该接口的实现等价于QueryDSL:

GET /postilhub/user/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "username": {
              "value": "张三"
            }
          }
        }
      ]
    }
  }
}
复制代码

针对各个接口的设计规则,Spring Data Elasticsearch给开发人员提供了一张表格:

注意:表格中 ? 表示参数,参数类型需要和ES中匹配。返回值统一为:List<Bean>

命名关键词命名示例QueryDSL示例
IsfindByUsername{"query":{"bool":{"must":[{"term":{"username":{"value":"?"}}}]}}}
AndfindByUsernameAndAge{"query":{"bool":{"must":[{"term":{"username":{"value":"?"}}},{"term":{"age":{"value":?}}}]}}}
OrfindByUsernameOrAge{"query":{"bool":{"should":[{"term":{"username":{"value":"?"}}},{"term":{"age":{"value":?}}}]}}}
NotfindByUsernameNot{"query":{"bool":{"must_not":[{"term":{"username":{"value":"?"}}}]}}}
BetweenfindByAgeBetween{"query":{"bool":{"must":[{"range":{"age":{"gt":?,"lt":?}}}]}}}
LessThanEqualfindByAgeLessThanEqual{"query":{"bool":{"must":[{"range":{"age":{"gte":null,"lte":?}}}]}}}
GreaterThanEqualfindByAgeGreaterThanEqual{"query":{"bool":{"must":[{"range":{"age":{"gte":?,"lte":null}}}]}}}
LessThanfindByAgeLessThan{"query":{"bool":{"must":[{"range":{"age":{"gt":null,"lt":?}}}]}}}
GreaterThanfindByAgeGreaterThan{"query":{"bool":{"must":[{"range":{"age":{"gt":?,"lt":null}}}]}}}
LikefindByUsernameLike{"query":{"wildcard":{"username":{"value":"*?*"}}}}
StartingWithfindByUsernameStartingWith{"query":{"wildcard":{"username":{"value":"?*"}}}}
EndingWithfindByUsernameEndingWith{"query":{"wildcard":{"username":{"value":"*?"}}}}
分类:
后端