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

  • 2019 年 10 月 3 日
  • 笔记

背景&痛点

通过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