通过ES进行查询,如果需要新增查询条件,则每次都需要进行硬编码,然后实现对应的查询功能。这样不仅开发工作量大,而且如果有多个不同的索引对象需要进行同样的查询,则需要开发多次,代码复用性不高。
想要解决这个问题,那么就需要一种能够模块化、配置化的解决方案。
通过配置参数的方式来配置参数映射、查询方式等,代码读取配置文件,根据配置文件构建查询语句。
优点:可配置化,新增查询字段基本不需要改动代码,除非增加新的查询方式。
缺点:配置文件太多、太复杂,配置文件配置错误将会导致整个查询不可用。
和方案一类似,通过注解的方式来配置参数映射等,然后读取注解,根据注解构建查询语句。
优点:可配置化,代码清晰、明确,可读性高。
缺点:每次新增查询字段都需要改动代码(在指定字段增加注解)
目前只有这两种可以说大同小异的解决思路,不过不喜欢配置文件太多,所以我就选择了第二种思路。
首先需要创建一个查询方式的枚举类,来区分有哪些查询方式,目前只实现了一些常用的查询类型。
源码如下:
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/919