李锋镝的博客

  • 首页
  • 时间轴
  • 留言
  • 插件
  • 左邻右舍
  • 关于我
    • 关于我
    • 另一个网站
  • 知识库
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 原创
  3. 正文

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

2019年9月12日 20191点热度 3人点赞 14条评论

背景&痛点

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

相关文章

  • SpringBoot整合Elasticsearch游标查询(scroll)
  • SpringBoot整合Elasticsearch详细步骤以及代码示例(附源码)
  • SpringBoot常用注解
  • CompletableFuture使用详解
  • SpringBoot 中内置的 49 个常用工具类
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: ElasticSearch JAVA SpringBoot
最后更新:2019年9月26日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • ZS

    您好,must和should做组合查询,should条件会失效,查询结果只满足must。百度了一些办法并没起作用

    2022年4月7日
    回复
    • 李锋镝

      @ZS must和should并行查询,确实会只有must生效,除非是must下嵌套should。你可以试试多个must下分别嵌套should来进行查询。

      2022年4月7日
      回复
      • ZS

        @李锋镝 之前已经试过把should嵌套在must里做查询了,并没有起作用,可能是我代码写的有问题

        2022年4月7日
        回复
        • ZS

          @ZS 已解决 :douyin.99:

          2022年4月7日
          回复
        • ZS

          @ZS 代码没问题,我的查询参数忘了改了,所以没查到数据 :douyin.19:

          2022年4月7日
          回复
  • ZS

    您好,大佬,我想问一下search项目es的范围查询需要什么样的参数

    2022年3月8日
    回复
    • 李锋镝

      @ZS

      /**
           * 范围查询 from后缀
           *
           * @return from后缀
           */
          String fromSuffix() default "From";
      
          /**
           * 范围查询 to后缀
           *
           * @return to后缀
           */
          String toSuffix() default "To";
      

      需要使用范围查询的字段,用Range注解标明。然后范围查询的参数名根据需要在后面分别加上From或者To的后缀。

      2022年3月8日
      回复
      • ZS

        @李锋镝 好的,谢谢大佬

        2022年3月8日
        回复
      • ZS

        @李锋镝

        /**
         * 搜索
         * @param params 查询参数
         * @return 搜索结果
         */
        @PostMapping("/search")
        public ResponseResult search(@RequestBody Map<String,String> params) {
            return ResponseResult.success(houseSearchService.search(params));
        }
        

        多条件查询传参格式是啥样的
        比如:AND,RANGE,IN这三个查询类型的字段去做多条件查询
        这样的的传参结构是啥样的,有没有例子呀

        2022年3月8日
        回复
      • ZS

        @李锋镝 传参结构问题已解决

        2022年3月8日
        回复
  • lin

    有学习价值,但是不敢随便引入项目,除非完全摸透测透 :bugaoxing:

    2021年11月22日
    回复
  • 贺龙

    我最新也在学习这个优秀的技术,虽然我们项目暂时没有用到。从你这又学习到了很多知识!

    2021年5月25日
    回复
    • 李锋镝

      @贺龙 不得不说,ES越来越好用了

      2021年5月25日
      回复
  • 2323

    群2

    2021年3月13日
    回复
  • 1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
    回复 ZS 取消回复

    但使龙城飞将在,不教胡马度阴山。

    最新 热点 随机
    最新 热点 随机
    SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports 应用型负载均衡(ALB)和网络型负载均衡(NLB)区别 什么是Helm? TransmittableThreadLocal介绍与使用 ReentrantLock深度解析 RedisTemplate和Redisson的区别
    玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?什么是Helm?2024年11月1号 农历十月初一别再背线程池的七大参数了,现在面试官都这么问URL地址末尾加不加“/”有什么区别
    九种常用的UML图总结 Hibernate、MyBatis的简介、区别以及优化 OOM后,JVM一定会退出吗?为什么? 用动画解释 TCP 三次握手过程 RabbitMQ Exchange 居住证签注...
    标签聚合
    SQL K8s 教程 IDEA docker JVM 设计模式 日常 多线程 Spring Redis MySQL 面试 数据库 架构 SpringBoot 文学 JAVA 分布式 ElasticSearch
    友情链接
    • i架构
    • 临窗旋墨
    • 博友圈
    • 博客录
    • 博客星球
    • 哥斯拉
    • 志文工作室
    • 搬砖日记
    • 旋律的博客
    • 旧时繁华
    • 林羽凡
    • 知向前端
    • 蜗牛工作室
    • 集博栈
    • 韩小韩博客
    • 風の声音

    COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

    Theme Kratos Made By Dylan

    津ICP备2024022503号-3