背景&痛点

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

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

  1. MySQL查询语句执行过程及性能优化-查询过程及优化方法(JOIN/ORDER BY)

    在上一篇文章MySQL查询语句执行过程及性能优化-基本概念和EXPLAIN语句简介中介绍了EXPLAIN语句,并举了一个慢查询例子:

  2. mysql查询语句,通过limit来限制查询的行数。

    mysql查询语句,通过limit来限制查询的行数. 例如: select name from usertb where age > 20 limit 0, 1; //限制从第一条开始,显示1条 ...

  3. 【2017-03-10】Tsql语句基础、条件,高级查询

    一.语句基础 1.创建数据库:create database 数据库名(不能汉字,不能数字.符号开头) 2.删除数据库:drop database 数据库名 3.选用数据库:use 数据库名 4.创建 ...

  4. php学习之sqlite查询语句之多条件查询

    一.PHP+Mysql多条件-多值查询示例代码: index.html代码:<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitio ...

  5. elasticsearch查询语句

    1,安装es 安装java环境 # java --versionjava version "1.8.0_65" Java(TM) SE Runtime Environment (b ...

  6. Orcle 查询语句

    首先,以超级管理员的身份登录oracle       sqlplus sys/bjsxt as sysdba   --然后,解除对scott用户的锁       alter user scott ac ...

  7. ThinkPHP(3)SQL查询语句

    ThinkPHP中对查询语句,包含了基本的查询方式.表达方式.快速查询.区间查询.组合查询.SQL查询.动态查询和子查询. 一.查询方式 ThinkPHP提供了三种基本的查询方式:字符串条件查询.索引 ...

  8. (转)经典SQL查询语句大全

    (转)经典SQL查询语句大全 一.基础1.说明:创建数据库CREATE DATABASE database-name2.说明:删除数据库drop database dbname3.说明:备份sql s ...

  9. 经典SQL查询语句大全

    一.基础1.说明:创建数据库CREATE DATABASE database-name2.说明:删除数据库drop database dbname3.说明:备份sql server--- 创建 备份数 ...

随机推荐

  1. 集成方法 Ensemble

    一.bagging 用于基础模型复杂.容易过拟合的情况,用来减小 variance(比如决策树).基础模型之间没有太多联系(相对于boosting),训练可以并行.但用 bagging 并不能有助于把 ...

  2. android——SQLite数据库存储(创建)

    Android 专门提供了SQLiteOpenHelper帮助类,借助这个类就可以非常简单的对数据库进行创建和升级. 首先SQLiteOpenHelper是一个抽象类,在使用的时候需要创建一个自己的帮 ...

  3. python第三课--函数

    函数的作用 编程大师Martin Fowler先生曾经说过:“代码有很多种坏味道,重复是最坏的一种!”,要写出高质量的代码首先要解决的就是重复代码的问题.例如3次求阶乘: m = int(input( ...

  4. Yii2 登录报错

    当用数据库登录系统报如下错误时 PHP Recoverable Error – yii\base\ErrorException Argument 1 passed to yii\web\User::l ...

  5. PHP危险函数总结学习

    1.PHP中代码执行的危险函数 call_user_func() 第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数. 传入call_user_func()的参数不能为引用传递 ...

  6. 初学html总结

    2019-08-17 17:58:49 html:超文本标记语言,用于网页结构的搭建 html语言构成:由标签.属性.属性值构成 标签:" < "后面第一个单词 属性:标签后 ...

  7. linux常用命令示例汇总

    1.ping -c 3 -i 0.1 -W 1 -t 3 100.100.242.181 -c发包数目,-c 3三个 -i,发包间隔,-i 0.1,每隔0.1秒发一个包 -W,发包超时时间,-W 1, ...

  8. rabbitmq生产者的消息确认

    通过Publisher Confirms and Returns机制,生产者可以判断消息是否发送到了exchange及queue,而通过消费者确认机制,Rabbitmq可以决定是否重发消息给消费者,以 ...

  9. .net打杂工程师的面试感想和总结

    上个月26号辞职了,今天开始第一场面试,随便写写感想,后面还会继续分享一些感想 前言 这个时候找工作是不是找死? 开门见山吧,95年的,之前做过两份工作,第一家公司在做了2年2个月,在北京,也就是去年 ...

  10. NDK Cmake

    CMake与NDK搭配使用时,可以配置的部分变量: 1. `ANDROID_PLATFORM`:指定Android的目标版本,对应`$NDK/platforms/`目录下的版本.通常情况下是`defa ...