当前位置 博文首页 > 小熊的进阶之路:Elasticsearch从0到千万级数据查询实践(非转载

    小熊的进阶之路:Elasticsearch从0到千万级数据查询实践(非转载

    作者:小熊的进阶之路 时间:2021-01-30 19:41

    1.es简介

      1.1 起源

      https://www.elastic.co/cn/what-is/elasticsearch,es的起源,是因为程序员Shay Banon在使用Apache Lucene发现不太好用,然后手动改造升级的过程中发展起来的。(程序员就是需要有这种动力~)实际上es也是一个java应用,跑在jvm里面的

      1.2 与关系型数据库的区别

    关系型数据库 schema(库) 每一行的数据 字段columns
    elasticsearch index(索引) document 字段fields

      1.3 为什么这么快

      索引方式的区别,es主要是利用倒排索引(inverted index),这个翻译可能会让初次接触的人产生误解,误以为是倒着排序?其实不是这样,一般关系型数据库索引是把某个字段建立起一张索引表,传入这个字段的某个值,再去索引中判断是否有这个值,从而找到这个值所在数据(id)的位置。而倒排索引则是把这个值所在的文档id记录下来,当输入这个值的时候,直接查询出这个值所匹配的文档id,再取出id。所以我们在建立es索引的时候,有分词的概念,相当于可以把filed字段内容拆分,然后索引记录下来。例如“我爱中国”,可以拆分成“我”,“爱”,“中国”,“我爱中国”这五个词,同时记录下来这几个关键词的对应文档数据id是1,当我们查询“我”,“中国”时,都能查出这条数据。而如果使用关系型数据库去查包含“中国”这个关键字的数据的时候,则需要like前后通配全表扫描,无法快速找到关键词所在的数据行。  

      1.4 下载安装

      https://www.elastic.co/cn/start 在这个地址里面下载最新版本,目前是7.10.2(拖了一个月写完,我下载的时候是7.9.3- -!)

      

      Windows版是一个压缩包文件,解压后(进入bin点开bat)即可使用。Linux版由于是直接在k8s里拉的镜像,这里就不做赘述。  

      文件结构

       

      启动完成之后访问:http://127.0.0.1:9200/,看见如下页面:You Know, for Search,就算启动成功啦。  

      

     

     

      1.5 安装可视化软件

      像数据库一样,可视化界面有Navicat,SQLyog,MySql自带的Workbench。es也是需要一个可视化ui界面来方便我们操作的。这里选择的也是官方的的kibana:

      https://www.elastic.co/cn/downloads/kibana :

      

       请注意需要选择与es匹配的版本,如果版本不匹配,则会提示你:

      

      或者是其他类似版本不匹配的错误。

      安装完成后就可以打开kibana玩耍啦,由于我本地没有数据,拿的是7.6.2版本搭建的elk中kibana界面:

       如果需要连接环境上的es,则可以在这里配置用户名和密码:

      

      这个工具的搜索很方便,不需要指定查哪个字段的哪个值,直接在输入框搜索想要查询的字段即可。如果想看他对应的查询语句,点开F12打开控制台即可研究:  

      

       es的查询条件还是比较复杂的,但是在业务查询当中,一些比较简单的查询就可以满足大多数的通用分页查询了,除非是要开发报表查询,会复杂一些。

      1.6 机器要求

      本地跑demo的话还是很容易的,这两个应用默认占用内存都不大,有需求可以自行调小一点:  

      

     2.Java中使用Elasticsearch

      2.1 使用spring-data提供的封装

        2.1.1 maven依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
            </dependency>

        2.1.2 yml参数

      

        2.1.3 代码中映射索引实体

      

       其中“omsElasticsearchSettings”这一段的意思是像mybatis那样解析表达式,找到omsElasticsearchSettings这个bean的getSuffix方法获取前后缀。这样就可以实现动态的根据环境生成映射对应的索引

     1 @Configuration
     2 @AllArgsConstructor
     3 public class ElasticsearchConfig {
     5     private final Environment env;
     7     @Bean
     8     public ElasticsearchSettings omsElasticsearchSettings(){
     9         return new ElasticsearchSettings().setSuffix(env.getActiveProfiles()[0]);
    10     }
    12 }
    13 
    16 @Data
    17 @Accessors(chain = true)
    18 public class ElasticsearchSettings {
    20     public String suffix;
    22 }

      2.1.4 索引mapping生成

      在启动项目的时候,SpringData会检测配置中的es里是否存在对应索引,如不存在,则会根据@Document实体中配置的@Field字段来生成mapping文件: 

        

       生成的Mapping Demo如下:

    PUT om_package_dev/?pretty
    {
       "settings": {
                "number_of_shards" :1,
                "number_of_replicas" : 1
      },
      "mappings": {
          "properties": {
            "_class": {
              "type": "text",
              "fields": {
                "keyword": {
                  "type": "keyword",
                  "ignore_above": 256
                }
              }
            },
            "actualFreightCost": {
              "type": "double"
            },
            "actualPackageCost": {
              "type": "double"
            }
        }
      }
    }

      2.1.5 增删改

      建立一个@Repository像mybatis一样来做增删改查的映射封装:

      

       底层是SpringData提供封装的统一方法:

      

      保存数据的时候直接调用即可:

      

      一般来说订单这些重要数据不会删除,要删除也是逻辑删除,所以删除接口基本不调用。直接更新逻辑删除值就好。更新也是调用这个:save/saveAll

       2.1.6 查

      查是Es的重头戏,我们打开org.elasticsearch.index.query.AbstractQueryBuilder查看实现类可以发现,继承这个抽象类的各种查询类有四五十个之多,不得不让人感叹es的查询强大,(与反人类,学习成本太高了)。

      好消息是,如果业务场景不复杂,仅仅是想在分页查询上提高速度,那么只需要掌握一下几个类的用法即可:

      

      我们封装了两个查询枚举,一个用来定义该实体是es查询条件实体@interface QueryEntity:

      

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface QueryEntity {
    
        String[] dbOrders() default {};
    
        String[] esOrders() default {};
    
        String dbLogicField() default "";
    
        String esLogicField() default "";
    }

      另外一个是用来定义字段,即使用es的哪个条件去查询@interface QueryField:

       

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface QueryField {
    
        String esField() default "";
    
        String dbField() default "";
    
        boolean like() default false;
    
        boolean range() default false;
    
        boolean require() default false;
    
        boolean match() default false;
    
        boolean commaSupported() default false;
    
        boolean isBigDecimal() default false;
    
        Class<?> searchTypeEnum() default void.class;
    
        Class<?> sortTypeEnum() default void.class;
    }

      对应到实体上的用法demo就是:

      

       这样可以支持区间查询,字段类型,对应es字段,从设计上规避了根据每个字段,调用每个拼接语句的上百个if/else噩梦。通过一个通用的查询工具类,来封装拼接这些查询条件QueryUtils:

    @Slf4j
    public class QueryUtils {
    
        private static ConcurrentHashMap<Class<?>, HashMap<String, Field>> classFieldMap = new ConcurrentHashMap<>();
    
        /**
         * 构建查询
         *
         * @param obj
         * @return 若为 null 说明该查询必定不会返回结果,无需查询 ES
         */
        public static BoolQueryBuilder boolQuery(Object obj) {
            if (obj == null) {
                return null;
            }
            BoolQueryBuilder root = QueryBuilders.boolQuery();
            if (!classFieldMap.containsKey(obj.getClass())) {
                HashMap<String, Field> filedNameMap = new HashMap<>(obj.getClass().getDeclaredFields().length);
                for (Field field : obj.getClass().getDeclaredFields()) {
                    filedNameMap.put(field.getName(), field);
                }
                classFieldMap.put(obj.getClass(), filedNameMap);
            }
            HashMap<String, Field> filedNameMap = classFieldMap.get(obj.getClass());
            QueryEntity entitySetting = obj.getClass().getAnnotation(QueryEntity.class);
            for (Field field : filedNameMap.values()) {
                QueryField fieldSetting;
                if ((fieldSetting = field.getAnnotation(QueryField.class)) == null) {
                    continue;
                }
                Object value = ReflectionUtil.getValue(field, obj);
                if (isNullOrEmpty(value)) {
                    if (!fieldSetting.require()) {
                        continue;
                    }
                    return null;
                }
                String fieldName = getEsQueryFieldName(field, fieldSetting);
    
                if (fieldSetting.range()) {
                    BoolQueryBuilder bool = QueryBuilders.boolQuery();
                    String[] arr = (String[]) value;
                    RangeQueryBuilder range = QueryBuilders.rangeQuery(fieldName);
                    if (arr.length != 2 || (StringUtils.isEmpty(arr[0]) && StringUtils.isEmpty(arr[1]))) {
                        continue;
                    }
                    if (!StringUtils.isEmpty(arr[0]) && StringUtils.isEmpty(arr[1])) {
                        bool.must(range.from(
                                fieldSetting.isBigDecimal() ? new BigDecimal(arr[0]) : DateUtil.parseAndGetTimestamp(arr[0])));
                    } else if (StringUtils.isEmpty(arr[0]) && !StringUtils.isEmpty(arr[1])) {
                        bool.must(range.to(fieldSetting.isBigDecimal() ? new BigDecimal(arr[1]) : DateUtil.parseAndGetTimestamp(arr[1])));
                    } else {
                        bool.must(range.from(fieldSetting.isBigDecimal() ? new BigDecimal(arr[0]) : DateUtil.parseAndGetTimestamp(arr[0])).
                                to(fieldSetting.isBigDecimal() ? new BigDecimal(arr[1]) : DateUtil.parseAndGetTimestamp(arr[1])));
                    }
                    root.must(bool);
                } else if (field.getType() == List.class) {
                    assert value instanceof List<?>;
                    List<?> list = (List<?>) value;
                    if (CollectionUtils.isEmpty(list)) {
                        if (fieldSetting.require()) {
                            return null;
                        }
                        continue;
                    }
                    if (list.get(0) instanceof StoreListBO) {
                        BoolQueryBuilder bool1 = QueryBuilders.boolQuery();
                        for (Object store : list) {
                            StoreListBO bo = (StoreListBO) store;
                            BoolQueryBuilder bool2 = QueryBuilders.boolQuery();
                            if (!bo.getFlagAll()) {
                                bool2.must(QueryBuilders.termQuery("platformCode", bo.getPlatformCode()));
                                bool2.must(QueryBuilders.termsQuery("storeCode", bo.getStoreCodeList()));
                            }
                            bool1.should(bool2);
                        }
                        root.must(bool1);
                    } else {
                        root.must(QueryBuilders.termsQuery(fieldName, (List<?>) value));
                    }
                } else if (fieldSetting.like()) {
                    root.must(QueryBuilders.wildcardQuery(fieldName, String.format("*%s*", value)));
                } else if (fieldSetting.commaSupported()) {
                    root.must(QueryBuilders.termsQuery(fieldName, StringUtility.splitCommaString((String) value)));
                } else if (fieldSetting.match()) {
                    if (fieldSetting.commaSupported()) {
                        root.must(QueryBuilders.multiMatchQuery(fieldName, StringUtility.splitCommaString((String) value)));
                    }