背景 & 痛点
通过 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 项目地址: https://github.com/lifengdi/search
如果觉得有帮助的话, 请帮忙点赞, 点星小小的支持一下~
谢谢~~
本文链接: https://www.lifengdi.com/archives/article/919
posted on 2019-09-16 09:31 李锋镝 阅读 (...) 评论 (...) 编辑 收藏
来源: https://www.cnblogs.com/lifengdi/p/11514463.html