美文网首页springbootelasticsearchjs css html
Elasticsearch集成SpringBoot(一)

Elasticsearch集成SpringBoot(一)

作者: 只有光头才强 | 来源:发表于2022-05-30 14:14 被阅读0次
    前言:

    之前的项目都有用到ES,但不是自己搭建和使用,包括ES语法和数据存储结构都不知道,趁着有时间来学习下ES的基本使用,很早就知道ES版本兼容问题有坑,唯有自己踩坑才印象深刻;
    公司服务器太多人用,动不动就搞出问题,所以我就用本地环境搭建Elasticsearch+Kibana+Spring-boot-starter-data-elasticsearch来集成,这样学习成本是比较低的,SpringBootData已经帮我们集成好了只需开箱即用,后面在优化代码通过自定义注解提供通用ES查询,现在先把代码跑起来。

    ES版本选择:
    ES对应Spring版本.png
    先检查自己SpringBoot版本,我是用的SpringBoot版本是2.3.12.RELEASE,所以ES安装版本用的elasticsearch-7.6.2/kibana-7.6.2-windows-x86_64(ES和kibana版本必须一致)
    ES对JDK的支持可参考:https://www.elastic.co/cn/support/matrix#matrix_jvm
    windows环境的搭建:

    参考:https://blog.csdn.net/pan_junbiao/article/details/114309373

    ES组件基本介绍:
    ES对比数据库类型.png
    集成到SpringBoot:
    • 引入依赖
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
            </dependency>
    
    • 配置文件
    spring:
      data:
        elasticsearch: #ElasticsearchProperties
          cluster-name: elasticsearch #默认即为elasticsearch
          cluster-nodes: 127.0.0.1:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode
    
    • 定义实体类
    
    
    /**
     * indexName 索引库名,个人建议以项目名称命名
     * type 类型,个人建议以实体类名称命名 注意:ES7.0之后将放弃type
     * shards 默认分区数
     * replicas 每个分区默认的备份数
     * indexStoreType 索引文件存储类型
     * refreshInterval 刷新间隔
     */
    @Document(indexName="oms",shards=5,replicas=1,indexStoreType="fs",refreshInterval="-1")
    @Data
    public class ElasticsearchOrderVo implements Serializable {
    
        private static final long serialVersionUID = 551589397625941750L;
    
        @Id
        private String id;
    
        /**
         * orderId
         */
        private Long orderId;
    
        /**
         * 订单编号
         */
        private String orderSn;
    
        /**
         * 收货人
         */
        private String receiverName;
    
        /**
         * 省市区
         */
        private String receiverAreaName;
    
        /**
         * 详细地址
         */
        private String receiverAddress;
    
        /**
         * 手机
         */
        private String receiverPhone;
    
        /**
         * 总金额
         */
        private Long totalAmount;
    
        /**
         * 实际支付金额
         */
        private Long payAmount;
    
        /**
         * 运费金额
         */
        private Long freightAmount;
    
        /**
         * 积分抵扣金额
         */
        private Long integrationAmount;
    
        /**
         * 优惠券抵扣金额
         */
        private Long couponAmount;
    
        /**
         * 创建时间
         */
        private Date createTime;
    
        /**
         * 过期时间
         */
        private Date expireTime;
    
        /**
         * 可获取的积分
         */
        private Long integration;
    
        /**
         * 备注
         */
        private String memo;
    
        /**
         * 订单状态,1:待付款,2:待发货,3:待收货,4:已完成
         */
        private Integer status;
    
        /**
         * 会员ID
         */
        private Long memberId;
    
        /**
         * 发票类型:0->不开发票;1->电子发票;2->纸质发票
         */
        private Integer billType;
    
        /**
         * 发票抬头
         */
        private String billHeader;
    
        /**
         * 发票内容
         */
        private String billContent;
    
        /**
         * 收票人电话
         */
        private String billReceiverPhone;
    
        /**
         * 收票人邮箱
         */
        private String billReceiverEmail;
    
        /**
         * 订单类型:0->正常订单;1->秒杀订单
         */
        private Integer orderType;
    }
    
    
    • 定义Repository接口
    @Component
    public interface ElasticsearchOrderRepository extends ElasticsearchRepository<ElasticsearchOrderVo, String> {
        //泛型<T,ID> T是文档内容,ID是文档ID类型
    }
    
    • 定义Service
    public interface ElasticsearchOrderService {
    
        /**
         * 保存
         * @param elasticsearchOrderVoList
         * @return
         */
        Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList);
    }
    
    
    @Slf4j
    @Service
    public class ElasticsearchOrderServiceImpl implements ElasticsearchOrderService {
    
        @Autowired
        private ElasticsearchOrderRepository elasticsearchOrderRepository;
    
        @Override
        public Iterable<ElasticsearchOrderVo>  saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList) {
            return elasticsearchOrderRepository.saveAll(elasticsearchOrderVoList);
        }
    }
    
    • 定义Controller
        public CommonResp es() {
            List<ElasticsearchOrderVo> elasticsearchOrderVoList = new ArrayList<>();
            for (int i = 1; i < 6; i++) {
                ElasticsearchOrderVo elasticsearchOrderVo = new ElasticsearchOrderVo();
                elasticsearchOrderVo.setId(UUID.randomUUID().toString());
                elasticsearchOrderVo.setOrderId(Long.valueOf(i));
                elasticsearchOrderVo.setOrderSn("Order_"+i);
                elasticsearchOrderVo.setReceiverName("小明"+i);
                elasticsearchOrderVo.setReceiverPhone("1201212122"+i);
                elasticsearchOrderVo.setReceiverAreaName("广东省深圳市南山区");
                elasticsearchOrderVo.setReceiverAddress("四方精创资讯大厦"+i+"楼");
                elasticsearchOrderVo.setBillReceiverEmail("12456789@qq.com");
                elasticsearchOrderVo.setOrderType(1);
                elasticsearchOrderVo.setCouponAmount(100L);
                elasticsearchOrderVo.setFreightAmount(8L);
                elasticsearchOrderVo.setIntegrationAmount(1L);
                elasticsearchOrderVo.setPayAmount(200L);
                elasticsearchOrderVo.setStatus(4);
                elasticsearchOrderVo.setBillHeader("深圳市 南山区 软基"+i + "层");
                elasticsearchOrderVoList.add(elasticsearchOrderVo);
            }
            return CommonResp.ok(elasticsearchOrderService.saveAll(elasticsearchOrderVoList));
        }
    
    • 浏览器请求:


      ES文档插入响应结果.png
    • 通过Kibana进去查询下数据,看下数据是否有被插入进去


      Kibana查询结果.png

    这里可以看到数据是有写入ES,但好像不是按顺序写进去的;
    took:执行搜索耗时,单位毫秒
    time_out:搜索是否超时
    _shards:多少分片被搜索,成功多少,失败多少
    hits:搜索结果展示
    hits.total:匹配条件的总当总数
    hits.max_score:最大匹配得分
    hits._score:返回文档的匹配得分(得分越高,匹配程度越高,越靠前)
    _index:索引名称
    _type:这里我默认没有指定type,因为ES7之后就放弃type了,在没有指定type的情况系统默认设置type=_doc
    _id:这个ID代码用UID生成的,也可以不指定,ES会默认生成一个ID
    _source:这个就是我们用到的字段了

    • 这里说明下:_idex_type_id可以直接定位到指定文档,比如:


      指定ID查询文章.png
    • 返回指定字段:


      返回指定字段.png
    • 按照条件模糊匹配:名字等于小明5


      名字等于小明5.png
    • 按照条件精准匹配:名字等于小明5


      精准匹配.png
    官方文档参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html

    SpringBoot实现ES分页

    ES分页介绍:
    • from+size
      优点:支持随机翻页
      缺点:受制于 max_result_window 设置,不能无限制翻页。存在深度翻页问题,越往后翻页越慢。
      适用场景:小型数据集或者大数据集返回 Top N(N <= 10000)结果集的业务场景,搜索引擎(谷歌、bing、百度、360、sogou等)支持随机跳转分页的业务场景
    • Scroll 遍历查询
      优点:支持全量遍历。ps:单次遍历的 size 值也不能超过 max_result_window 大小。
      缺点:响应时间非实时。保留上下文需要足够的堆内存空间。
      适用场景:全量或数据量很大时遍历结果数据,而非分页查询。
      官方文档强调:不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT+search_after
    • search_after
      优点:不严格受制于 max_result_window,可以无限制往后翻页。ps:不严格含义:单次请求值不能超过 max_result_window;但总翻页结果集可以超过。
      缺点:只支持向后翻页,不支持随机翻页。
      适用场景:不支持随机翻页,更适合手机端应用的场景。
    from+size+自定义注解代码实现:
    • 自定义注解
    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EsEquals {
    
        /**
         * filed name
         */
        String name() default "";
    }
    
    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EsIn {
        /**
         * filed name
         */
        String name() default "";
    }
    
    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EsLike {
        /**
         * filed name
         */
        String name() default "";
    
        boolean leftLike() default false;
    
        boolean rightLike() default false;
    }
    
    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EsRange {
    
        /**
         * filed name
         */
        String name() default "";
    
        /**
         * <
         */
        boolean lt() default false;
    
        /**
         * >
         */
        boolean gt() default false;
    
        /**
         * 包含上界
         */
        boolean includeUpper() default false;
    
        /**
         * 包含下界
         */
        boolean includeLower() default false;
    }
    
    
    • 定义抽象类:AbstractSearchQueryEngine
    public abstract class AbstractSearchQueryEngine<T,R> {
    
        @Autowired
        protected ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    
        /**
         * from+size 分页查询
         * @param requestPara
         * @param clazz
         * @param pageable
         * @return
         */
        public abstract SearchHits<R> findPage(T requestPara, Class<R> clazz, Pageable pageable);
    
    
        //todo search_after 深度分页
    
        //todo Scroll分页...
    
    }
    
    • 定义实现类
    @Component
    public class SimpleSearchQueryEngine extends AbstractSearchQueryEngine{
    
        @Override
        public SearchHits findPage(Object requestPara, Class clazz, Pageable pageable) {
            Query query = EsQueryParse.convertQuery(requestPara);
            //组装分页
            query.setPageable(pageable);
            return elasticsearchRestTemplate.search(query, clazz);
        }
    }
    
    • 自定义注解转换工具类
    
    /**
     * 条件构造器转换类
     * QueryBuilder.matchAllQuery(); 匹配所有
     * QueryBuilder.ermQuery精准匹配(); 大小写敏感且不支持
     * QueryBuilder.matchPhraseQuery(); 对中文精确匹配
     * QueryBuilder.matchQuery();单个匹配, field不支持通配符, 前缀具高级特性
     * QueryBuilder.multiMatchQuery("text", "field1", "field2"..); 匹配多个字段
     *
     *参考ES官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-query-builders.html
     */
    public class EsQueryParse {
        public static <T> Query convertQuery(T t) {
            try {
                NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
                BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
                Class<?> clazz = t.getClass();
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    Object value = ClassUtils.getPublicMethod(clazz, "get" + captureName(field.getName())).invoke(t);
                    if (value == null) {
                        continue;
                    }
                    if (field.isAnnotationPresent(EsLike.class)) {
                        WildcardQueryBuilder query = getLikeQuery(field, (String) value);
                        boolQueryBuilder.must(query);
                    }
                    if (field.isAnnotationPresent(EsEquals.class)) {
                        MatchQueryBuilder query = getEqualsQuery(field, value);
                        boolQueryBuilder.must(query);
                    }
                    if (field.isAnnotationPresent(EsRange.class)) {
                        RangeQueryBuilder rangeQueryBuilder = getRangeQuery(field, value);
                        boolQueryBuilder.must(rangeQueryBuilder);
                    }
                    if (field.isAnnotationPresent(EsIn.class)) {
                        TermsQueryBuilder query = getInQuery(field, (List<?>) value);
                        boolQueryBuilder.must(query);
                    }
                }
                return queryBuilder.withQuery(boolQueryBuilder).build();
            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                e.printStackTrace();
            }
            return Query.findAll();
        }
    
        /**
         * 一次匹配多个值
         * @param field
         * @param value
         * @return
         */
        private static TermsQueryBuilder getInQuery(Field field, List<?> value) {
            EsIn esIn = field.getAnnotation(EsIn.class);
            String filedName = StringUtils.isBlank(esIn.name()) ? field.getName() : esIn.name();
            return QueryBuilders.termsQuery(filedName, value);
        }
    
        /**
         * 大小范围查询
         * @param field
         * @param value
         * @return
         */
        private static RangeQueryBuilder getRangeQuery(Field field, Object value) {
            EsRange esRange = field.getAnnotation(EsRange.class);
            String filedName = StringUtils.isBlank(esRange.name()) ? field.getName() : esRange.name();
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(filedName)
                    .includeLower(esRange.includeLower())
                    .includeUpper(esRange.includeUpper());
            if (esRange.lt()) {
                rangeQueryBuilder.lt(value);
            }
            if (esRange.gt()) {
                rangeQueryBuilder.gt(value);
            }
            return rangeQueryBuilder;
        }
    
        /**
         * 分词短语匹配:QueryBuilders。matchQuery 会将搜索词分词,再与目标查询字段进行匹配,若分词中的任意一个词与目标字段匹配上,则可查询到。
         * 精准匹配短语: QueryBuilders.matchPhraseQuery()
         * 完全匹配: QueryBuilders.termQuery()
         * @param field
         * @param value
         * @return
         */
        private static MatchQueryBuilder getEqualsQuery(Field field, Object value) {
            EsEquals esEquals = field.getAnnotation(EsEquals.class);
            String filedName = StringUtils.isBlank(esEquals.name()) ? field.getName() : esEquals.name();
            return QueryBuilders.matchQuery(filedName, value);
        }
    
        /**
         * 模糊查询,?匹配单个字符,*匹配多个字符
         * @param field
         * @param likeValue
         * @return
         */
        private static WildcardQueryBuilder getLikeQuery(Field field, String likeValue) {
            EsLike esLike = field.getAnnotation(EsLike.class);
            String filedName = StringUtils.isBlank(esLike.name()) ? field.getName() : esLike.name();
            if (esLike.leftLike()) {
                likeValue = "%" + likeValue;
            }
            if (esLike.rightLike()) {
                likeValue = likeValue + "%";
            }
            return QueryBuilders.wildcardQuery(filedName, likeValue);
        }
    
        /**
         * 首字母大写
         * @param name
         * @return
         */
        public static String captureName(String name) {
            char[] cs = name.toCharArray();
            cs[0] -= 32;
            return String.valueOf(cs);
        }
    
    }
    
    • Service方法
        /**
         * 单个保存
         * @param elasticsearchOrderVo
         * @return
         */
        ElasticsearchOrderVo save(ElasticsearchOrderVo elasticsearchOrderVo);
    
        /**
         * 批量保存
         * @param elasticsearchOrderVoList
         * @return
         */
        Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList);
    
    
        /**
         * 查询
         * @param elasticsearchOrderQueryVo
         * @return
         */
        Page findElasticsearchOrderVoList(ElasticsearchOrderQueryVo elasticsearchOrderQueryVo);
    
    • Service实现
    
    @Slf4j
    @Service
    public class ElasticsearchOrderServiceImpl implements ElasticsearchOrderService {
    
        @Autowired
        private SimpleSearchQueryEngine simpleSearchQueryEngine;
    
        @Autowired
        private ElasticsearchOrderRepository elasticsearchOrderRepository;
    
        @Override
        public ElasticsearchOrderVo save(ElasticsearchOrderVo elasticsearchOrderVo) {
            return elasticsearchOrderRepository.save(elasticsearchOrderVo);
        }
    
        @Override
        public Iterable<ElasticsearchOrderVo>  saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList) {
            return elasticsearchOrderRepository.saveAll(elasticsearchOrderVoList);
        }
    
        @Override
        public Page findElasticsearchOrderVoList(ElasticsearchOrderQueryVo elasticsearchOrderQueryVo) {
            SearchHits<ElasticsearchOrderVo> searchHits =  simpleSearchQueryEngine.findPage(elasticsearchOrderQueryVo,
                    ElasticsearchOrderVo.class,
                    PageRequest.of(elasticsearchOrderQueryVo.getPage(),elasticsearchOrderQueryVo.getSize(),Sort.by(Sort.Order.desc("orderId"),Sort.Order.desc("createTime"))));
            Page page = new Page(elasticsearchOrderQueryVo.getPage(),elasticsearchOrderQueryVo.getSize(),searchHits.getTotalHits());
            page.setRecords(searchHits.get().collect(Collectors.toList()));
            return page;
        }
    }
    
    
    • Controller
        @PostMapping("/page")
        public CommonResp findElasticsearchOrderVoList(@RequestBody ElasticsearchOrderQueryVo elasticsearchOrderQueryVo){
            return iomsOrderItemService.findElasticsearchOrderVoList(elasticsearchOrderQueryVo);
        }
    
    • RequestVO(ElasticsearchOrderQueryVo)
     /**
         * 分页
         */
        private Integer page;
    
        /**
         * 数量
         */
        private Integer size;
    
        /**
         * orderId
         */
        private Long orderId;
    
        /**
         * 订单编号
         */
        private String orderSn;
    
        /**
         * 收货人
         */
        @EsEquals
        private String receiverName;
    
        /**
         * 省市区
         */
        private String receiverAreaName;
    
        /**
         * 详细地址
         */
        private String receiverAddress;
    
        /**
         * 手机
         */
        private String receiverPhone;
    
    
        /**
         * 实际支付金额
         */
        @EsRange(lt = true)
        private Long payAmount;
    
    
        /**
         * 创建时间
         */
        private Date createTime;
    
    
    • POSTMAN相应结果:


      form+size响应结果.png

    相关文章

      网友评论

        本文标题:Elasticsearch集成SpringBoot(一)

        本文链接:https://www.haomeiwen.com/subject/gfsmprtx.html