• SpringBoot3集成ElasticSearch


    标签:ElasticSearch8.Kibana8;

    一、简介

    Elasticsearch是一个分布式、RESTful风格的搜索和数据分析引擎,适用于各种数据类型,数字、文本、地理位置、结构化数据、非结构化数据;

    在实际的工作中,历经过Elasticsearch从6.07.0的版本升级,而这次SpringBoot3和ES8.0的集成,虽然脚本的语法变化很小,但是Java客户端的API语法变化很大;

    二、环境搭建

    1、下载安装包

    需要注意的是,这些安装包的版本要选择对应的,不然容易出问题;

    软件包:elasticsearch-8.8.2-darwin-x86_64.tar.gz
    分词器工具:elasticsearch-analysis-ik-8.8.2.zip
    可视化工具:kibana-8.8.2-darwin-x86_64.tar.gz
    

    2、服务启动

    不论是ES还是Kibana,在首次启动后,会初始化很多配置文件,可以根据自己的需要做相关的配置调整,比如常见的端口调整,资源占用,安全校验等;

    1、启动ES
    elasticsearch-8.8.2/bin/elasticsearch
    
    本地访问:localhost:9200
    
    2、启动Kibana
    kibana-8.8.2/bin/kibana
    
    本地访问:http://localhost:5601
    
    # 3、查看安装的插件
    http://localhost:9200/_cat/plugins  ->  analysis-ik 8.8.2
    

    三、工程搭建

    1、工程结构

    2、依赖管理

    starter-elasticsearch组件中,实际上依赖的是elasticsearch-java组件的8.7.1版本;

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-elasticsearchartifactId>
        <version>${spring-boot.version}version>
    dependency>
    

    3、配置文件

    在上面环境搭建的过程中,已经禁用了用户和密码的登录验证,配置ES服务地址即可;

    spring:
      # ElasticSearch配置
      elasticsearch:
        uris: localhost:9200
    

    四、基础用法

    1、实体类

    通过DocumentField注解描述ES索引结构的实体类,注意这里JsonIgnoreProperties注解,解决索引中字段和实体类非一一对应的而引起的JSON解析问题;

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Document(indexName = "contents_index", createIndex = false)
    public class ContentsIndex implements Serializable {
    
        private static final long serialVersionUID=1L;
    
        @Field(type= FieldType.Integer)
        private Integer id;
    
        @Field(type= FieldType.Keyword)
        private String title;
    
        @Field(type= FieldType.Keyword)
        private String intro;
    
        @Field(type= FieldType.Text)
        private String content;
    
        @Field(type= FieldType.Integer)
        private Integer createId;
    
        @Field(type= FieldType.Keyword)
        private String createName;
    
        @Field(type= FieldType.Date,format = DateFormat.date_hour_minute_second)
        private Date createTime;
    }
    

    2、初始化索引

    基于ElasticsearchTemplate类和上述实体类,实现索引结构的初始化,并且将tb_contents表中的数据同步到索引中,最后通过ID查询一条测试数据;

    @Service
    public class ContentsIndexService {
        private static final Logger log = LoggerFactory.getLogger(ContentsIndexService.class);
    
        @Resource
        private ContentsService contentsService ;
        @Resource
        private ElasticsearchTemplate template ;
    
        /**
         * 初始化索引结构和数据
         */
        public void initIndex (){
            // 处理索引结构
            IndexOperations indexOps = template.indexOps(ContentsIndex.class);
            if (indexOps.exists()){
                boolean delFlag = indexOps.delete();
                log.info("contents_index exists,delete:{}",delFlag);
                indexOps.createMapping(ContentsIndex.class);
            } else {
                log.info("contents_index not exists");
                indexOps.createMapping(ContentsIndex.class);
            }
            // 同步数据库表记录
            List contentsList = contentsService.queryAll();
            if (contentsList.size() > 0){
                List contentsIndexList = new ArrayList<>() ;
                contentsList.forEach(contents -> {
                    ContentsIndex contentsIndex = new ContentsIndex() ;
                    BeanUtils.copyProperties(contents,contentsIndex);
                    contentsIndexList.add(contentsIndex);
                });
                template.save(contentsIndexList);
            }
            // ID查询
            ContentsIndex contentsIndex = template.get("10",ContentsIndex.class);
            log.info("contents-index-10:{}",contentsIndex);
        }
    }
    

    3、仓储接口

    继承ElasticsearchRepository接口,可以对ES这种特定类型的存储库进行通用增删改查操作;在测试类中对该接口的方法进行测试;

    // 1、接口定义
    public interface ContentsIndexRepository extends ElasticsearchRepository {
    }
    
    // 2、接口测试
    public class ContentsIndexRepositoryTest {
        @Autowired
        private ContentsIndexRepository contentsIndexRepository;
    
        @Test
        public void testAdd (){
            // 单个新增
            contentsIndexRepository.save(buildOne());
            // 批量新增
            contentsIndexRepository.saveAll(buildList()) ;
        }
    
        @Test
        public void testUpdate (){
            // 根据ID查询后再更新
            Optional contentsOpt = contentsIndexRepository.findById(14L);
            if (contentsOpt.isPresent()){
                ContentsIndex contentsId = contentsOpt.get();
                System.out.println("id=14:"+contentsId);
                contentsId.setContent("update-content");
                contentsId.setCreateTime(new Date());
                contentsIndexRepository.save(contentsId);
            }
        }
    
        @Test
        public void testQuery (){
            // 单个ID查询
            Optional contentsOpt = contentsIndexRepository.findById(1L);
            if (contentsOpt.isPresent()){
                ContentsIndex contentsId1 = contentsOpt.get();
                System.out.println("id=1:"+contentsId1);
            }
            // 批量ID查询
            Iterator contentsIterator = contentsIndexRepository
                                            .findAllById(Arrays.asList(10L,12L)).iterator();
            while (contentsIterator.hasNext()){
                ContentsIndex contentsIndex = contentsIterator.next();
                System.out.println("id="+contentsIndex.getId()+":"+contentsIndex);
            }
        }
    
        @Test
        public void testDelete (){
            contentsIndexRepository.deleteById(15L);
            contentsIndexRepository.deleteById(16L);
        }
    }
    

    4、查询语法

    无论是ElasticsearchTemplate类还是ElasticsearchRepository接口,都是对ES常用的简单功能进行封装,在实际使用时,复杂的查询语法还是依赖ElasticsearchClient和原生的API封装;

    这里主要演示七个查询方法,主要涉及:ID查询,字段匹配,组合与范围查询,分页与排序,分组统计,最大值查询和模糊匹配;更多的查询API还是要多看文档中的案例才行;

    public class ElasticsearchClientTest {
    
        @Autowired
        private ElasticsearchClient client ;
    
        @Test
        public void testSearch1 () throws IOException {
            // ID查询
            GetResponse resp = client.get(
                    getReq ->getReq.index("contents_index").id("7"), ContentsIndex.class);
            if (resp.found()){
                ContentsIndex contentsIndex = resp.source() ;
                System.out.println("contentsIndex-7:"+contentsIndex);
            }
        }
    
        @Test
        public void testSearch2 () throws IOException {
            // 指定字段匹配
            SearchResponse resp = client.search(searchReq -> searchReq.index("contents_index")
                            .query(query -> query.match(field -> field
                            .field("createName").query("张三"))),ContentsIndex.class);
            printResp(resp);
        }
    
        @Test
        public void testSearch3 () throws IOException {
            // 组合查询:姓名和时间范围
            Query byName = MatchQuery.of(field -> field.field("createName").query("王五"))._toQuery();
            Query byTime = RangeQuery.of(field -> field.field("createTime")
                            .gte(JsonData.of("2023-07-10T00:00:00"))
                            .lte(JsonData.of("2023-07-12T00:00:00")))._toQuery();
            SearchResponse resp = client.search(searchReq -> searchReq.index("contents_index")
                            .query(query -> query.bool(boolQuery -> boolQuery.must(byName).must(byTime))),ContentsIndex.class);
            printResp(resp);
        }
    
        @Test
        public void testSearch4 () throws IOException {
            // 排序和分页,在14条数据中,根据ID倒序排列,从第5条往后取4条数据
            SearchResponse resp = client.search(searchReq -> searchReq.index("contents_index")
                    .from(5).size(4)
                    .sort(sort -> sort.field(sortField -> sortField.field("id").order(SortOrder.Desc))),ContentsIndex.class);
            printResp(resp);
        }
    
        @Test
        public void testSearch5 () throws IOException {
            // 根据createId分组统计
            SearchResponse resp = client.search(searchReq -> searchReq.index("contents_index")
                    .aggregations("createIdGroup",agg -> agg.terms(term -> term.field("createId"))),ContentsIndex.class);
            Aggregate aggregate = resp.aggregations().get("createIdGroup");
            LongTermsAggregate termsAggregate = aggregate.lterms();
            Buckets buckets = termsAggregate.buckets();
            for (LongTermsBucket termsBucket : buckets.array()) {
                System.out.println(termsBucket.key() + " : " + termsBucket.docCount());
            }
        }
    
        @Test
        public void testSearch6 () throws IOException {
            // 查询最大的ID
            SearchResponse resp = client.search(searchReq -> searchReq.index("contents_index")
                    .aggregations("maxId",agg -> agg.max(field -> field.field("id"))),ContentsIndex.class);
            for (Map.Entry entry : resp.aggregations().entrySet()){
                System.out.println(entry.getKey()+":"+entry.getValue().max().value());
            }
        }
    
        @Test
        public void testSearch7 () throws IOException {
            // 模糊查询title字段,允许1个误差
            Query byContent = FuzzyQuery.of(field -> field.field("title").value("设计").fuzziness("1"))._toQuery();
            SearchResponse resp = client.search(
                    searchReq -> searchReq.index("contents_index").query(byContent),ContentsIndex.class);
            printResp(resp);
        }
    
        private void printResp (SearchResponse resp){
            TotalHits total = resp.hits().total();
            System.out.println("total:"+total);
            List> hits = resp.hits().hits();
            for (Hit hit: hits) {
                ContentsIndex contentsIndex = hit.source();
                System.out.println(hit.id()+":"+contentsIndex);
            }
        }
    }
    

    五、参考源码

    文档仓库:
    https://gitee.com/cicadasmile/butte-java-note
    
    源码仓库:
    https://gitee.com/cicadasmile/butte-spring-parent
    
  • 相关阅读:
    springboot多数据源配置-通过SqlSessionFactory指定的数据源来操作指定目录的XML文件的方式
    laravel中锁以及事务的简单使用
    一文详解手眼标定公式推导
    TypeScript: 判断两个数组的内容是否相等
    16 Linux 内核定时器
    奇舞周刊第 458 期 浅谈低代码平台远程组件加载方案
    电力移动应用及终端安全溯源管控技术研究与实践
    Educational Codeforces Round 130 (Rated for Div. 2) A--C
    vscode快捷键使用总结
    微信将电脑的聊天记录导入手机的方法(win 和 Mac)
  • 原文地址:https://www.cnblogs.com/cicada-smile/p/17632955.html