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