• SpringBoot使用注解的方式构建Elasticsearch查询语句,实现多条件的复杂查询


    背景&痛点

    通过ES进行查询,如果需要新增查询条件,则每次都需要进行硬编码,然后实现对应的查询功能。这样不仅开发工作量大,而且如果有多个不同的索引对象需要进行同样的查询,则需要开发多次,代码复用性不高。

    想要解决这个问题,那么就需要一种能够模块化、配置化的解决方案。

    解决方案

    思路一:配置参数

    通过配置参数的方式来配置参数映射、查询方式等,代码读取配置文件,根据配置文件构建查询语句。

    优点:可配置化,新增查询字段基本不需要改动代码,除非增加新的查询方式。

    缺点:配置文件太多、太复杂,配置文件配置错误将会导致整个查询不可用。

    思路二:注解方式

    和方案一类似,通过注解的方式来配置参数映射等,然后读取注解,根据注解构建查询语句。

    优点:可配置化,代码清晰、明确,可读性高。

    缺点:每次新增查询字段都需要改动代码(在指定字段增加注解)

    目前只有这两种可以说大同小异的解决思路,不过不喜欢配置文件太多,所以我就选择了第二种思路。

    代码实现(Elasticsearch版本6.7.2)

    首先需要创建一个查询方式的枚举类,来区分有哪些查询方式,目前只实现了一些常用的查询类型。

    源码如下:

    package com.lifengdi.search.enums;
    
    /**
     * @author 李锋镝
     * @date Create at 19:17 2019/8/27
     */
    public enum QueryTypeEnum {
    
        /**
         * 等于
         */
        EQUAL,
    
        /**
         * 忽略大小写相等
         */
        EQUAL_IGNORE_CASE,
    
        /**
         * 范围
         */
        RANGE,
    
        /**
         * in
         */
        IN,
    
        IGNORE,
    
        /**
         * 搜索
         */
        FULLTEXT,
    
        /**
         * 匹配 和q搜索区分开
         */
        MATCH,
    
        /**
         * 模糊查询
         */
        FUZZY,
    
        /**
         * and
         */
        AND,
    
        /**
         * 多个查询字段匹配上一个即符合条件
         */
        SHOULD,
    
        /**
         * 前缀查询
         */
        PREFIX,
    
        ;
    }
    

    然后开始自定义注解,通过注解来定义字段的查询方式、映射字段、嵌套查询的path以及其他的一些参数;通过@Repeatable注解来声明这是一个重复注解类。
    源码如下:

    package com.lifengdi.search.annotation;
    
    import com.lifengdi.search.enums.QueryTypeEnum;
    
    import java.lang.annotation.*;
    
    /**
     * 定义查询字段的查询方式
     * @author 李锋镝
     * @date Create at 19:07 2019/8/27
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE})
    @Repeatable(DefinitionQueryRepeatable.class)
    public @interface DefinitionQuery {
    
        /**
         * 查询参数
         *
         * @return 查询字段
         */
        String key() default "";
    
        /**
         * 查询类型 see{@link QueryTypeEnum}
         *
         * @return QueryTypeEnum
         */
        QueryTypeEnum type() default QueryTypeEnum.EQUAL;
    
        /**
         * 范围查询 from后缀
         *
         * @return from后缀
         */
        String fromSuffix() default "From";
    
        /**
         * 范围查询 to后缀
         *
         * @return to后缀
         */
        String toSuffix() default "To";
    
        /**
         * 多个字段分隔符
         *
         * @return 分隔符
         */
        String separator() default ",";
    
        /**
         * 指定对象的哪个字段将应用于查询映射
         * 例如:
         * 同一个文档下有多个User对象,对象名分别为createdUser、updatedUser,该User对象的属性有name等字段,
         * 如果要根据查询createdUser的name来进行查询,
         * 则可以这样定义DefinitionQuery:queryField = cName, mapped = createdUser.name
         *
         * @return 映射的实体的字段路径
         */
        String mapped() default "";
    
        /**
         * 嵌套查询的path
         *
         * @return path
         */
        String nestedPath() default "";
    
    }
    
    

    同时定义@DefinitionQueryRepeatable注解,声明这是上边注解的容器注解类,源码如下:

    package com.lifengdi.search.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author 李锋镝
     * @date Create at 19:11 2019/8/27
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE})
    public @interface DefinitionQueryRepeatable {
        DefinitionQuery[] value();
    }
    
    

    如何使用注解?

    • 在索引文档中需要查询的字段、对象或者类上面使用即可。

    源码如下:

    package com.lifengdi.document;
    
    import com.lifengdi.document.store.*;
    import com.lifengdi.search.annotation.DefinitionQuery;
    import com.lifengdi.search.enums.QueryTypeEnum;
    import lombok.Data;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.elasticsearch.annotations.Document;
    import org.springframework.data.elasticsearch.annotations.Field;
    import org.springframework.data.elasticsearch.annotations.FieldType;
    
    import java.util.List;
    
    /**
     * 门店Document
     *
     * @author 李锋镝
     * @date Create at 19:31 2019/8/22
     */
    @Document(indexName = "store", type = "base")
    @Data
    @DefinitionQuery(key = "page", type = QueryTypeEnum.IGNORE)
    @DefinitionQuery(key = "size", type = QueryTypeEnum.IGNORE)
    @DefinitionQuery(key = "q", type = QueryTypeEnum.FULLTEXT)
    public class StoreDocument {
    
        @Id
        @DefinitionQuery(type = QueryTypeEnum.IN)
        @DefinitionQuery(key = "id", type = QueryTypeEnum.IN)
        @Field(type = FieldType.Keyword)
        private String id;
    
        /**
         * 基础信息
         */
        @Field(type = FieldType.Object)
        private StoreBaseInfo baseInfo;
    
        /**
         * 标签
         */
        @Field(type = FieldType.Nested)
        @DefinitionQuery(key = "tagCode", mapped = "tags.key", type = QueryTypeEnum.IN)
        @DefinitionQuery(key = "tagValue", mapped = "tags.value", type = QueryTypeEnum.AND)
        @DefinitionQuery(key = "_tagValue", mapped = "tags.value", type = QueryTypeEnum.IN)
        private List<StoreTags> tags;
    
    }
    
    package com.lifengdi.document.store;
    
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    import com.lifengdi.search.annotation.DefinitionQuery;
    import com.lifengdi.search.enums.QueryTypeEnum;
    import com.lifengdi.serializer.JodaDateTimeDeserializer;
    import com.lifengdi.serializer.JodaDateTimeSerializer;
    import lombok.Data;
    import org.joda.time.DateTime;
    import org.springframework.data.elasticsearch.annotations.Field;
    import org.springframework.data.elasticsearch.annotations.FieldType;
    
    /**
     * 门店基础信息
     * 
     */
    @Data
    public class StoreBaseInfo {
    
        /**
         * 门店id
         */
        @Field(type = FieldType.Keyword)
        private String storeId;
    
        /**
         * 门店名称
         */
        @Field(type = FieldType.Text, analyzer = "ik_smart")
        @DefinitionQuery(type = QueryTypeEnum.FUZZY)
        @DefinitionQuery(key = "name", type = QueryTypeEnum.SHOULD)
        private String storeName;
    
        /**
         * 门店简称
         */
        @Field(type = FieldType.Text, analyzer = "ik_smart")
        private String shortName;
    
        /**
         * 门店简介
         */
        @Field(type = FieldType.Text, analyzer = "ik_smart")
        private String profile;
    
        /**
         * 门店属性
         */
        @Field(type = FieldType.Integer)
        private Integer property;
    
        /**
         * 门店类型
         */
        @Field(type = FieldType.Integer)
        private Integer type;
    
        /**
         * 详细地址
         */
        @Field(type = FieldType.Text, analyzer = "ik_smart")
        private String address;
    
        /**
         * 所在城市
         */
        @Field(type = FieldType.Keyword)
        @DefinitionQuery(type = QueryTypeEnum.IN)
        private String cityCode;
    
        /**
         * 城市名称
         */
        @Field(type = FieldType.Keyword)
        private String cityName;
    
        /**
         * 所在省份
         */
        @Field(type = FieldType.Keyword)
        private String provinceCode;
    
        /**
         * 省份名称
         */
        @Field(type = FieldType.Keyword)
        private String provinceName;
    
        /**
         * 所在地区
         */
        @Field(type = FieldType.Keyword)
        private String regionCode;
    
        /**
         * 地区名称
         */
        @Field(type = FieldType.Keyword)
        private String regionName;
    
        /**
         * 所属市场id
         */
        @Field(type = FieldType.Long)
        @DefinitionQuery(type = QueryTypeEnum.IN)
        private Integer marketId;
    
        /**
         * 所属市场key
         */
        @Field(type = FieldType.Keyword)
        @DefinitionQuery(type = QueryTypeEnum.IN)
        private String marketKey;
    
        /**
         * 所属市场名称
         */
        @Field(type = FieldType.Keyword)
        private String marketName;
    
        /**
         * 摊位号
         */
        @Field(type = FieldType.Text)
        private String marketStall;
    
        /**
         * 门店状态
         */
        @Field(type = FieldType.Keyword)
        @DefinitionQuery(key = "storeStatus", type = QueryTypeEnum.IN)
        @DefinitionQuery(key = "_storeStatus", type = QueryTypeEnum.IN)
        private String status;
    
        /**
         * 删除标示
         */
        @Field(type = FieldType.Integer)
        @DefinitionQuery(key = "deleted")
        private Integer deleted;
    
        /**
         * 创建时间
         */
        @Field(type = FieldType.Date)
        @JsonDeserialize(using = JodaDateTimeDeserializer.class)
        @JsonSerialize(using = JodaDateTimeSerializer.class)
        @DefinitionQuery(type = QueryTypeEnum.RANGE)
        public DateTime createdTime;
    
        /**
         * 创建人id
         */
        @Field(type = FieldType.Keyword)
        @DefinitionQuery
        private String createdUserId;
    
        /**
         * 创建人名称
         */
        @Field(type = FieldType.Keyword)
        private String createdUserName;
    
        /**
         * 修改时间
         */
        @Field(type = FieldType.Date)
        @JsonDeserialize(using = JodaDateTimeDeserializer.class)
        @JsonSerialize(using = JodaDateTimeSerializer.class)
        private DateTime updatedTime;
    
        /**
         * 修改人ID
         */
        @Field(type = FieldType.Keyword)
        private String updatedUserId;
    
        /**
         * 修改人姓名
         */
        @Field(type = FieldType.Keyword)
        private String updatedUserName;
    
        /**
         * 业务类型
         */
        @Field(type = FieldType.Long)
        private Long businessType;
    
        /**
         * storeNo
         */
        @Field(type = FieldType.Keyword)
        @DefinitionQuery(type = QueryTypeEnum.SHOULD)
        private String storeNo;
    }
    
    package com.lifengdi.document.store;
    
    import lombok.Data;
    import org.springframework.data.elasticsearch.annotations.Field;
    import org.springframework.data.elasticsearch.annotations.FieldType;
    
    /**
     * @author 李锋镝
     * @date Create at 18:15 2019/2/18
     */
    @Data
    public class StoreTags {
        @Field(type = FieldType.Keyword)
        private String key;
    
        @Field(type = FieldType.Keyword)
        private String value;
    
        private String showName;
    }
    
    

    解释一下上面的源码:

    @DefinitionQuery(key = "tagCode", mapped = "tags.key", type = QueryTypeEnum.IN)
    

    这行代码的意思是指定一个查询参数tagCode,该参数映射到tagskey字段,查询方式为IN,调用接口入参查询的时候只需要入参tagCode={tagCode}即可。

    请求体:

    curl -X POST 
      http://localhost:8080/search/store/search 
      -H 'Content-Type: application/json' 
      -d '{
    	"tagCode": "1"
    }'
    

    构建的ES查询语句:

    {
        "query": {
            "bool": {
                "must": [
                    {
                        "nested": {
                            "query": {
                                "bool": {
                                    "must": [
                                        {
                                            "terms": {
                                                "tags.key": [
                                                    "1"
                                                ],
                                                "boost": 1
                                            }
                                        }
                                    ],
                                    "adjust_pure_negative": true,
                                    "boost": 1
                                }
                            },
                            "path": "tags",
                            "ignore_unmapped": false,
                            "score_mode": "none",
                            "boost": 1
                        }
                    }
                ],
                "adjust_pure_negative": true,
                "boost": 1
            }
        }
    }
    

    继续说源码

    使用了注解,就需要将注解中的参数提取出来,并生成映射数据,目前实现的是将所有的字段全都封装到Map中,查询的时候遍历取值。
    源码如下:

    package com.lifengdi.search.mapping;
    
    import com.lifengdi.SearchApplication;
    import com.lifengdi.model.FieldDefinition;
    import com.lifengdi.model.Key;
    import com.lifengdi.search.annotation.DefinitionQuery;
    import com.lifengdi.search.annotation.DefinitionQueryRepeatable;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.data.elasticsearch.annotations.FieldType;
    
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * @author 李锋镝
     * @date Create at 09:15 2019/8/28
     */
    public class KeyMapping {
    
        // 启动类所在包
        private static final String BOOTSTRAP_PATH = SearchApplication.class.getPackage().getName();
    
        /**
         * 字段映射
         * @param clazz Class
         * @return Map
         */
        public static Map<Key, FieldDefinition> mapping(Class clazz) {
            Map<Key, FieldDefinition> mappings = mapping(clazz.getDeclaredFields(), "");
            mappings.putAll(typeMapping(clazz));
            return mappings;
        }
    
        /**
         * 字段映射
         *
         * @param fields      字段
         * @param parentField 父级字段名
         * @return Map
         */
        public static Map<Key, FieldDefinition> mapping(Field[] fields, String parentField) {
            Map<Key, FieldDefinition> mappings = new HashMap<>();
            for (Field field : fields) {
                org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = field.getAnnotation
                        (org.springframework.data.elasticsearch.annotations.Field.class);
                String nestedPath = null;
                if (Objects.nonNull(fieldAnnotation) && FieldType.Nested.equals(fieldAnnotation.type())) {
                    nestedPath = parentField + field.getName();
                }
                DefinitionQuery[] definitionQueries = field.getAnnotationsByType(DefinitionQuery.class);
                // 如果属性非BOOTSTRAP_PATH包下的类,说明属性为基础字段 即跳出循环,否则递归调用mapping
                if (!field.getType().getName().startsWith(BOOTSTRAP_PATH)) {
                    for (DefinitionQuery definitionQuery : definitionQueries) {
                        buildMapping(parentField, mappings, field, nestedPath, definitionQuery);
                    }
                } else {
                    for (DefinitionQuery definitionQuery : definitionQueries) {
                        if (StringUtils.isNotBlank(definitionQuery.mapped())) {
                            buildMapping(parentField, mappings, field, nestedPath, definitionQuery);
                        }
                    }
                    mappings.putAll(mapping(field.getType().getDeclaredFields(), parentField + field.getName() + "."));
                }
            }
            return mappings;
        }
    
        /**
         * 构建mapping
         * @param parentField 父级字段名
         * @param mappings mapping
         * @param field 字段
         * @param nestedPath 默认嵌套路径
         * @param definitionQuery 字段定义
         */
        private static void buildMapping(String parentField, Map<Key, FieldDefinition> mappings, Field field,
                                         String nestedPath, DefinitionQuery definitionQuery) {
            FieldDefinition fieldDefinition;
            nestedPath = StringUtils.isNotBlank(definitionQuery.nestedPath()) ? definitionQuery.nestedPath() : nestedPath;
            String key = StringUtils.isBlank(definitionQuery.key()) ? field.getName() : definitionQuery.key();
            String filedName = StringUtils.isBlank(definitionQuery.mapped()) ? field.getName() : definitionQuery.mapped();
            switch (definitionQuery.type()) {
                case RANGE:
                    buildRange(parentField, mappings, definitionQuery, key, filedName);
                    break;
                default:
                    fieldDefinition = FieldDefinition.builder()
                            .key(key)
                            .queryField(parentField + filedName)
                            .queryType(definitionQuery.type())
                            .separator(definitionQuery.separator())
                            .nestedPath(nestedPath)
                            .build();
                    mappings.put(new Key(key), fieldDefinition);
                    break;
            }
        }
    
        /**
         * 构建范围查询
         * @param parentField 父级字段名
         * @param mappings mapping
         * @param definitionQuery 字段定义
         * @param key 入参查询字段
         * @param filedName 索引文档中字段名
         */
        private static void buildRange(String parentField, Map<Key, FieldDefinition> mappings, DefinitionQuery definitionQuery,
                                  String key, String filedName) {
            FieldDefinition fieldDefinition;
            String queryField = parentField + filedName;
            String rangeKeyFrom = key + definitionQuery.fromSuffix();
            String rangeKeyTo = key + definitionQuery.toSuffix();
    
            fieldDefinition = FieldDefinition.builder()
                    .key(rangeKeyFrom)
                    .queryField(queryField)
                    .queryType(definitionQuery.type())
                    .fromSuffix(definitionQuery.fromSuffix())
                    .toSuffix(definitionQuery.toSuffix())
                    .build();
            mappings.put(new Key(rangeKeyFrom), fieldDefinition);
    
            fieldDefinition = FieldDefinition.builder()
                    .key(rangeKeyTo)
                    .queryField(queryField)
                    .queryType(definitionQuery.type())
                    .fromSuffix(definitionQuery.fromSuffix())
                    .toSuffix(definitionQuery.toSuffix())
                    .build();
            mappings.put(new Key(rangeKeyTo), fieldDefinition);
        }
    
        /**
         * 对象映射
         * @param clazz document
         * @return Map
         */
        public static Map<Key, FieldDefinition> typeMapping(Class clazz) {
            DefinitionQueryRepeatable repeatable = (DefinitionQueryRepeatable) clazz.getAnnotation(DefinitionQueryRepeatable.class);
            Map<Key, FieldDefinition> mappings = new HashMap<>();
            for (DefinitionQuery definitionQuery : repeatable.value()) {
                String key = definitionQuery.key();
                switch (definitionQuery.type()) {
                    case RANGE:
                        buildRange("", mappings, definitionQuery, key, definitionQuery.mapped());
                        break;
                    default:
                        FieldDefinition fieldDefinition = FieldDefinition.builder()
                                .key(key)
                                .queryField(key)
                                .queryType(definitionQuery.type())
                                .separator(definitionQuery.separator())
                                .nestedPath(definitionQuery.nestedPath())
                                .build();
                        mappings.put(new Key(key), fieldDefinition);
                        break;
                }
    
            }
            return mappings;
        }
    }
    

    定义Key对象,解决重复字段在Map中会覆盖的问题:

    package com.lifengdi.model;
    
    /**
     * @author 李锋镝
     * @date Create at 09:25 2019/8/28
     */
    public class Key {
    
        private String key;
    
        public Key(String key) {
            this.key = key;
        }
    
        @Override
        public String toString() {
            return key;
        }
    
        public String getKey() {
            return key;
        }
    }
    
    

    接下来重头戏来了,根据查询类型的枚举值,来封装对应的ES查询语句,如果需要新增查询类型,则新增枚举,然后新增对应的实现代码;同时也增加了对排序的支持,不过排序字段需要传完整的路径,暂时还未实现通过mapping映射来进行对应的排序。

    源码如下:

    package com.lifengdi.search;
    
    import com.lifengdi.model.FieldDefinition;
    import com.lifengdi.model.Key;
    import com.lifengdi.search.enums.QueryTypeEnum;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.lucene.search.join.ScoreMode;
    import org.elasticsearch.action.search.SearchResponse;
    import org.elasticsearch.index.query.*;
    import org.elasticsearch.search.sort.SortBuilders;
    import org.elasticsearch.search.sort.SortOrder;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
    import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
    import org.springframework.data.elasticsearch.core.query.SearchQuery;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    import javax.annotation.Resource;
    import java.util.*;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    import static com.lifengdi.global.Global.*;
    
    /**
     * @author 李锋镝
     * @date Create at 16:49 2019/8/27
     */
    @Service
    public class SearchService {
    
        @Resource
        private ElasticsearchTemplate elasticsearchTemplate;
    
        /**
         * 通用查询
         * @param params 查询入参
         * @param indexName 索引名称
         * @param type 索引类型
         * @param defaultSort 默认排序
         * @param keyMappings 字段映射
         * @param keyMappingsMap 索引对应字段映射
         * @return Page
         */
        protected Page<Map> commonSearch(Map<String, String> params, String indexName, String type, String defaultSort,
                                 Map<Key, FieldDefinition> keyMappings,
                                 Map<String, Map<Key, FieldDefinition>> keyMappingsMap) {
            SearchQuery searchQuery = buildSearchQuery(params, indexName, type, defaultSort, keyMappings, keyMappingsMap);
            return elasticsearchTemplate.queryForPage(searchQuery, Map.class);
        }
    
        /**
         * 数量通用查询
         * @param params 查询入参
         * @param indexName 索引名称
         * @param type 索引类型
         * @param defaultSort 默认排序
         * @param keyMappings 字段映射
         * @param keyMappingsMap 索引对应字段映射
         * @return Page
         */
        protected long count(Map<String, String> params, String indexName, String type, String defaultSort,
                          Map<Key, FieldDefinition> keyMappings,
                          Map<String, Map<Key, FieldDefinition>> keyMappingsMap) {
            SearchQuery searchQuery = buildSearchQuery(params, indexName, type, defaultSort, keyMappings, keyMappingsMap);
    
            return elasticsearchTemplate.count(searchQuery);
        }
    
        /**
         * 根据ID获取索引
         * @param id ID
         * @param indexName 索引名
         * @param type 索引类型
         * @return 索引
         */
        protected Map get(String id, String indexName, String type) {
            return elasticsearchTemplate.getClient()
                    .prepareGet(indexName, type, id)
                    .execute()
                    .actionGet()
                    .getSourceAsMap();
        }
    
        /**
         * 根据定义的查询字段封装查询语句
         * @param params 查询入参
         * @param indexName 索引名称
         * @param type 索引类型
         * @param defaultSort 默认排序
         * @param keyMappings 字段映射
         * @param keyMappingsMap 索引对应字段映射
         * @return SearchQuery
         */
        private SearchQuery buildSearchQuery(Map<String, String> params, String indexName, String type, String defaultSort,
                                             Map<Key, FieldDefinition> keyMappings,
                                             Map<String, Map<Key, FieldDefinition>> keyMappingsMap) {
            NativeSearchQueryBuilder searchQueryBuilder = buildSearchField(params, indexName, type, keyMappings, keyMappingsMap);
    
            String sortFiled = params.getOrDefault(SORT, defaultSort);
            if (StringUtils.isNotBlank(sortFiled)) {
                String[] sorts = sortFiled.split(SPLIT_FLAG_COMMA);
                handleQuerySort(searchQueryBuilder, sorts);
            }
    
            return searchQueryBuilder.build();
        }
    
        /**
         * 根据定义的查询字段封装查询语句
         * @param params 查询入参
         * @param indexName 索引名称
         * @param type 索引类型
         * @param keyMappings 字段映射
         * @param keyMappingsMap 索引对应字段映射
         * @return NativeSearchQueryBuilder
         */
        private NativeSearchQueryBuilder buildSearchField(Map<String, String> params, String indexName, String type,
                                                            Map<Key, FieldDefinition> keyMappings,
                                                            Map<String, Map<Key, FieldDefinition>> keyMappingsMap) {
    
            int page = Integer.parseInt(params.getOrDefault(PAGE, "0"));
            int size = Integer.parseInt(params.getOrDefault(SIZE, "10"));
    
            AtomicBoolean matchSearch = new AtomicBoolean(false);
    
            String q = params.get(Q);
            String missingFields = params.get(MISSING);
            String existsFields = params.get(EXISTS);
    
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            BoolQueryBuilder boolFilterBuilder = QueryBuilders.boolQuery();
    
            Map<String, BoolQueryBuilder> nestedMustMap = new HashMap<>();
            Map<String, BoolQueryBuilder> nestedMustNotMap = new HashMap<>();
            List<String> fullTextFieldList = new ArrayList<>();
    
            // 查询条件构建器
            NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder()
                    .withIndices(params.getOrDefault(INDEX_NAME, indexName))
                    .withTypes(params.getOrDefault(INDEX_TYPE, type))
                    .withPageable(PageRequest.of(page, size));
    
            String fields = params.get(FIELDS);
            if (Objects.nonNull(fields)) {
                searchQueryBuilder.withFields(fields.split(SPLIT_FLAG_COMMA));
            }
    
            keyMappingsMap.getOrDefault(params.getOrDefault(INDEX_NAME, indexName), keyMappings)
                    .entrySet()
                    .stream()
                    .filter(m -> m.getValue().getQueryType() == QueryTypeEnum.FULLTEXT
                            || m.getValue().getQueryType() != QueryTypeEnum.IGNORE
                            && params.get(m.getKey().toString()) != null)
                    .forEach(m -> {
                        String k = m.getKey().toString();
                        FieldDefinition v = m.getValue();
                        String queryValue = params.get(k);
                        QueryTypeEnum queryType = v.getQueryType();
                        String queryName = v.getQueryField();
                        String nestedPath = v.getNestedPath();
                        BoolQueryBuilder nestedMustBoolQuery = null;
                        BoolQueryBuilder nestedMustNotBoolQuery = null;
                        boolean nested = false;
                        if (StringUtils.isNotBlank(nestedPath)) {
                            nested = true;
                            if (nestedMustMap.containsKey(nestedPath)) {
                                nestedMustBoolQuery = nestedMustMap.get(nestedPath);
                            } else {
                                nestedMustBoolQuery = QueryBuilders.boolQuery();
                            }
                            if (nestedMustNotMap.containsKey(nestedPath)) {
                                nestedMustNotBoolQuery = nestedMustNotMap.get(nestedPath);
                            } else {
                                nestedMustNotBoolQuery = QueryBuilders.boolQuery();
                            }
                        }
                        switch (queryType) {
                            case RANGE:
                                RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder(queryName);
                                if (k.endsWith(v.getFromSuffix())) {
                                    rangeQueryBuilder.from(queryValue);
                                } else {
                                    rangeQueryBuilder.to(queryValue);
                                }
                                boolFilterBuilder.must(rangeQueryBuilder);
                                break;
                            case FUZZY:
                                if (nested) {
                                    if (k.startsWith(NON_FLAG)) {
                                        nestedMustBoolQuery.mustNot(QueryBuilders.wildcardQuery(queryName, queryValue));
                                    } else {
                                        nestedMustBoolQuery.filter(QueryBuilders.wildcardQuery(queryName,
                                                StringUtils.wrapIfMissing(queryValue, WILDCARD)));
                                    }
                                } else {
                                    if (k.startsWith(NON_FLAG)) {
                                        boolFilterBuilder.mustNot(QueryBuilders.wildcardQuery(queryName, queryValue));
                                    } else {
                                        boolFilterBuilder.filter(QueryBuilders.wildcardQuery(queryName,
                                                StringUtils.wrapIfMissing(queryValue, WILDCARD)));
                                    }
                                }
                                break;
                            case PREFIX:
                                boolFilterBuilder.filter(QueryBuilders.prefixQuery(queryName, queryValue));
                                break;
                            case AND:
                                if (nested) {
                                    for (String and : queryValue.split(v.getSeparator())) {
                                        nestedMustBoolQuery.must(QueryBuilders.termQuery(queryName, and));
                                    }
                                } else {
                                    for (String and : queryValue.split(v.getSeparator())) {
                                        boolFilterBuilder.must(QueryBuilders.termQuery(queryName, and));
                                    }
                                }
                                break;
                            case IN:
                                String inQuerySeparator = v.getSeparator();
                                if (nested) {
                                    buildIn(k, queryValue, queryName, nestedMustBoolQuery, inQuerySeparator, nestedMustNotBoolQuery);
                                } else {
                                    buildIn(k, queryValue, queryName, boolFilterBuilder, inQuerySeparator);
                                }
                                break;
                            case SHOULD:
                                boolFilterBuilder.should(QueryBuilders.wildcardQuery(queryName,
                                        StringUtils.wrapIfMissing(queryValue, WILDCARD)));
                                break;
                            case FULLTEXT:
                                if (!Q.equalsIgnoreCase(queryName)) {
                                    fullTextFieldList.add(queryName);
                                }
                                break;
                            case MATCH:
                                boolQueryBuilder.must(QueryBuilders.matchQuery(queryName, queryValue));
                                matchSearch.set(true);
                                break;
                            case EQUAL_IGNORE_CASE:
                                boolFilterBuilder.must(QueryBuilders.termQuery(queryName, queryValue.toLowerCase()));
                                break;
                            default:
                                boolFilterBuilder.must(QueryBuilders.termQuery(queryName, queryValue));
                                break;
                        }
                        if (nested) {
                            if (nestedMustBoolQuery.hasClauses()) {
                                nestedMustMap.put(nestedPath, nestedMustBoolQuery);
                            }
                            if (nestedMustNotBoolQuery.hasClauses()) {
                                nestedMustNotMap.put(nestedPath, nestedMustNotBoolQuery);
                            }
                        }
                    });
            if (StringUtils.isNotBlank(q)) {
                MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(q);
                fullTextFieldList.forEach(multiMatchQueryBuilder::field);
                boolQueryBuilder.should(multiMatchQueryBuilder);
            }
            if (StringUtils.isNotBlank(q) || matchSearch.get()) {
                searchQueryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
            }
            if (StringUtils.isNotBlank(missingFields)) {
                for (String miss : missingFields.split(SPLIT_FLAG_COMMA)) {
                    boolFilterBuilder.mustNot(QueryBuilders.existsQuery(miss));
                }
            }
            if (StringUtils.isNotBlank(existsFields)) {
                for (String exists : existsFields.split(SPLIT_FLAG_COMMA)) {
                    boolFilterBuilder.must(QueryBuilders.existsQuery(exists));
                }
            }
    
            if (!CollectionUtils.isEmpty(nestedMustMap)) {
                for (String key : nestedMustMap.keySet()) {
                    if (StringUtils.isBlank(key)) {
                        continue;
                    }
                    boolFilterBuilder.must(QueryBuilders.nestedQuery(key, nestedMustMap.get(key), ScoreMode.None));
                }
            }
            if (!CollectionUtils.isEmpty(nestedMustNotMap)) {
                for (String key : nestedMustNotMap.keySet()) {
                    if (StringUtils.isBlank(key)) {
                        continue;
                    }
                    boolFilterBuilder.mustNot(QueryBuilders.nestedQuery(key, nestedMustNotMap.get(key), ScoreMode.None));
                }
            }
    
            searchQueryBuilder.withFilter(boolFilterBuilder);
            searchQueryBuilder.withQuery(boolQueryBuilder);
    
            return searchQueryBuilder;
        }
    
        private void buildIn(String k, String queryValue, String queryName, BoolQueryBuilder boolQuery, String separator) {
            buildIn(k, queryValue, queryName, boolQuery, separator, null);
        }
    
        private void buildIn(String k, String queryValue, String queryName, BoolQueryBuilder boolQuery, String separator,
                             BoolQueryBuilder nestedMustNotBoolQuery) {
            if (queryValue.contains(separator)) {
                if (k.startsWith(NON_FLAG)) {
                    if (Objects.nonNull(nestedMustNotBoolQuery)) {
                        nestedMustNotBoolQuery.must(QueryBuilders.termsQuery(queryName, Arrays.asList(queryValue.split(separator))));
                    } else {
                        boolQuery.mustNot(QueryBuilders.termsQuery(queryName, Arrays.asList(queryValue.split(separator))));
                    }
                } else {
                    boolQuery.must(QueryBuilders.termsQuery(queryName, Arrays.asList(queryValue.split(separator))));
                }
            } else {
                if (k.startsWith(NON_FLAG)) {
                    if (Objects.nonNull(nestedMustNotBoolQuery)) {
                        nestedMustNotBoolQuery.must(QueryBuilders.termsQuery(queryName, queryValue));
                    } else {
                        boolQuery.mustNot(QueryBuilders.termsQuery(queryName, queryValue));
                    }
                } else {
                    boolQuery.must(QueryBuilders.termsQuery(queryName, queryValue));
                }
            }
        }
    
        /**
         * 处理排序
         *
         * @param sorts 排序字段
         */
        private void handleQuerySort(NativeSearchQueryBuilder searchQueryBuilder, String[] sorts) {
            for (String sort : sorts) {
                sortBuilder(searchQueryBuilder, sort);
            }
        }
    
        private void sortBuilder(NativeSearchQueryBuilder searchQueryBuilder, String sort) {
            switch (sort.charAt(0)) {
                case '-': // 字段前有-: 倒序排序
                    searchQueryBuilder.withSort(SortBuilders.fieldSort(sort.substring(1)).order(SortOrder.DESC));
                    break;
                case '+': // 字段前有+: 正序排序
                    searchQueryBuilder.withSort(SortBuilders.fieldSort(sort.substring(1)).order(SortOrder.ASC));
                    break;
                default:
                    searchQueryBuilder.withSort(SortBuilders.fieldSort(sort.trim()).order(SortOrder.ASC));
                    break;
            }
        }
    
        /**
         * 获取一个符合查询条件的数据
         * @param filterBuilder 查询条件
         * @param indexName 索引名
         * @param type 索引类型
         * @return Map
         */
        protected Map<String, Object> getOne(TermQueryBuilder filterBuilder, String indexName, String type) {
            final SearchResponse searchResponse = elasticsearchTemplate.getClient()
                    .prepareSearch(indexName)
                    .setTypes(type)
                    .setPostFilter(filterBuilder)
                    .setSize(1)
                    .get();
            final long total = searchResponse.getHits().getTotalHits();
            if (total > 0) {
                return searchResponse.getHits().getAt(0).getSourceAsMap();
            }
            return null;
        }
    
    }
    

    好了关键的代码就这么些,具体源码可以在我的github上查看。

    Git项目地址:search

    如果觉得有帮助的话,请帮忙点赞、点星小小的支持一下~
    谢谢~~

    本文链接:https://www.lifengdi.com/archives/article/919

  • 相关阅读:
    webpack 3.X学习之CSS处理
    webpack 3.X学习之图片处理
    webpack 3.X学习之基本配置
    webpack 3.X学习之JS压缩与打包HTML文件
    webpack 3.X学习之初始构建
    【复习】VueJS之内部指令
    前端学习记录之Javascript-DOM
    javascript常用的Math对象的方法
    nodejs+mongoose+websocket搭建xxx聊天室
    Markdown常用语法
  • 原文地址:https://www.cnblogs.com/lifengdi/p/11514463.html
Copyright © 2020-2023  润新知