1. 程式人生 > >SpringBoot使用註解的方式構建Elasticsearch查詢語句,實現多條件的複雜查詢

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/