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
,該引數對映到tags
的key
欄位,查詢方式為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/