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

分享到:
打赏 赞(1)
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

   

说点什么

avatar
  订阅  
提醒