Elasticsearch集成SpringBoot(一)

前言:

之前的项目都有用到ES,但不是自己搭建和使用,包括ES语法和数据存储结构都不知道,趁着有时间来学习下ES的基本使用,很早就知道ES版本兼容问题有坑,唯有自己踩坑才印象深刻;
公司服务器太多人用,动不动就搞出问题,所以我就用本地环境搭建Elasticsearch+Kibana+Spring-boot-starter-data-elasticsearch来集成,这样学习成本是比较低的,SpringBootData已经帮我们集成好了只需开箱即用,后面在优化代码通过自定义注解提供通用ES查询,现在先把代码跑起来。

ES版本选择:

ES对应Spring版本.png

先检查自己SpringBoot版本,我是用的SpringBoot版本是2.3.12.RELEASE,所以ES安装版本用的elasticsearch-7.6.2/kibana-7.6.2-windows-x86_64(ES和kibana版本必须一致)
ES对JDK的支持可参考:https://www.elastic.co/cn/support/matrix#matrix_jvm

windows环境的搭建:

参考:https://blog.csdn.net/pan_junbiao/article/details/114309373

ES组件基本介绍:
ES对比数据库类型.png
集成到SpringBoot:
  • 引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
  • 配置文件
spring:
  data:
    elasticsearch: #ElasticsearchProperties
      cluster-name: elasticsearch #默认即为elasticsearch
      cluster-nodes: 127.0.0.1:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode
  • 定义实体类


/**
 * indexName 索引库名,个人建议以项目名称命名
 * type 类型,个人建议以实体类名称命名 注意:ES7.0之后将放弃type
 * shards 默认分区数
 * replicas 每个分区默认的备份数
 * indexStoreType 索引文件存储类型
 * refreshInterval 刷新间隔
 */
@Document(indexName="oms",shards=5,replicas=1,indexStoreType="fs",refreshInterval="-1")
@Data
public class ElasticsearchOrderVo implements Serializable {

    private static final long serialVersionUID = 551589397625941750L;

    @Id
    private String id;

    /**
     * orderId
     */
    private Long orderId;

    /**
     * 订单编号
     */
    private String orderSn;

    /**
     * 收货人
     */
    private String receiverName;

    /**
     * 省市区
     */
    private String receiverAreaName;

    /**
     * 详细地址
     */
    private String receiverAddress;

    /**
     * 手机
     */
    private String receiverPhone;

    /**
     * 总金额
     */
    private Long totalAmount;

    /**
     * 实际支付金额
     */
    private Long payAmount;

    /**
     * 运费金额
     */
    private Long freightAmount;

    /**
     * 积分抵扣金额
     */
    private Long integrationAmount;

    /**
     * 优惠券抵扣金额
     */
    private Long couponAmount;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 过期时间
     */
    private Date expireTime;

    /**
     * 可获取的积分
     */
    private Long integration;

    /**
     * 备注
     */
    private String memo;

    /**
     * 订单状态,1:待付款,2:待发货,3:待收货,4:已完成
     */
    private Integer status;

    /**
     * 会员ID
     */
    private Long memberId;

    /**
     * 发票类型:0->不开发票;1->电子发票;2->纸质发票
     */
    private Integer billType;

    /**
     * 发票抬头
     */
    private String billHeader;

    /**
     * 发票内容
     */
    private String billContent;

    /**
     * 收票人电话
     */
    private String billReceiverPhone;

    /**
     * 收票人邮箱
     */
    private String billReceiverEmail;

    /**
     * 订单类型:0->正常订单;1->秒杀订单
     */
    private Integer orderType;
}

  • 定义Repository接口
@Component
public interface ElasticsearchOrderRepository extends ElasticsearchRepository<ElasticsearchOrderVo, String> {
    //泛型<T,ID> T是文档内容,ID是文档ID类型
}
  • 定义Service
public interface ElasticsearchOrderService {

    /**
     * 保存
     * @param elasticsearchOrderVoList
     * @return
     */
    Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList);
}


@Slf4j
@Service
public class ElasticsearchOrderServiceImpl implements ElasticsearchOrderService {

    @Autowired
    private ElasticsearchOrderRepository elasticsearchOrderRepository;

    @Override
    public Iterable<ElasticsearchOrderVo>  saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList) {
        return elasticsearchOrderRepository.saveAll(elasticsearchOrderVoList);
    }
}
  • 定义Controller
    public CommonResp es() {
        List<ElasticsearchOrderVo> elasticsearchOrderVoList = new ArrayList<>();
        for (int i = 1; i < 6; i++) {
            ElasticsearchOrderVo elasticsearchOrderVo = new ElasticsearchOrderVo();
            elasticsearchOrderVo.setId(UUID.randomUUID().toString());
            elasticsearchOrderVo.setOrderId(Long.valueOf(i));
            elasticsearchOrderVo.setOrderSn("Order_"+i);
            elasticsearchOrderVo.setReceiverName("小明"+i);
            elasticsearchOrderVo.setReceiverPhone("1201212122"+i);
            elasticsearchOrderVo.setReceiverAreaName("广东省深圳市南山区");
            elasticsearchOrderVo.setReceiverAddress("四方精创资讯大厦"+i+"楼");
            elasticsearchOrderVo.setBillReceiverEmail("12456789@qq.com");
            elasticsearchOrderVo.setOrderType(1);
            elasticsearchOrderVo.setCouponAmount(100L);
            elasticsearchOrderVo.setFreightAmount(8L);
            elasticsearchOrderVo.setIntegrationAmount(1L);
            elasticsearchOrderVo.setPayAmount(200L);
            elasticsearchOrderVo.setStatus(4);
            elasticsearchOrderVo.setBillHeader("深圳市 南山区 软基"+i + "层");
            elasticsearchOrderVoList.add(elasticsearchOrderVo);
        }
        return CommonResp.ok(elasticsearchOrderService.saveAll(elasticsearchOrderVoList));
    }
  • 浏览器请求:


    ES文档插入响应结果.png
  • 通过Kibana进去查询下数据,看下数据是否有被插入进去


    Kibana查询结果.png

这里可以看到数据是有写入ES,但好像不是按顺序写进去的;
took:执行搜索耗时,单位毫秒
time_out:搜索是否超时
_shards:多少分片被搜索,成功多少,失败多少
hits:搜索结果展示
hits.total:匹配条件的总当总数
hits.max_score:最大匹配得分
hits._score:返回文档的匹配得分(得分越高,匹配程度越高,越靠前)
_index:索引名称
_type:这里我默认没有指定type,因为ES7之后就放弃type了,在没有指定type的情况系统默认设置type=_doc
_id:这个ID代码用UID生成的,也可以不指定,ES会默认生成一个ID
_source:这个就是我们用到的字段了

  • 这里说明下:_idex_type_id可以直接定位到指定文档,比如:


    指定ID查询文章.png
  • 返回指定字段:


    返回指定字段.png
  • 按照条件模糊匹配:名字等于小明5


    名字等于小明5.png
  • 按照条件精准匹配:名字等于小明5


    精准匹配.png
官方文档参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html

SpringBoot实现ES分页

ES分页介绍:
  • from+size
    优点:支持随机翻页
    缺点:受制于 max_result_window 设置,不能无限制翻页。存在深度翻页问题,越往后翻页越慢。
    适用场景:小型数据集或者大数据集返回 Top N(N <= 10000)结果集的业务场景,搜索引擎(谷歌、bing、百度、360、sogou等)支持随机跳转分页的业务场景
  • Scroll 遍历查询
    优点:支持全量遍历。ps:单次遍历的 size 值也不能超过 max_result_window 大小。
    缺点:响应时间非实时。保留上下文需要足够的堆内存空间。
    适用场景:全量或数据量很大时遍历结果数据,而非分页查询。
    官方文档强调:不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT+search_after
  • search_after
    优点:不严格受制于 max_result_window,可以无限制往后翻页。ps:不严格含义:单次请求值不能超过 max_result_window;但总翻页结果集可以超过。
    缺点:只支持向后翻页,不支持随机翻页。
    适用场景:不支持随机翻页,更适合手机端应用的场景。
from+size+自定义注解代码实现:
  • 自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EsEquals {

    /**
     * filed name
     */
    String name() default "";
}

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EsIn {
    /**
     * filed name
     */
    String name() default "";
}

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EsLike {
    /**
     * filed name
     */
    String name() default "";

    boolean leftLike() default false;

    boolean rightLike() default false;
}

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EsRange {

    /**
     * filed name
     */
    String name() default "";

    /**
     * <
     */
    boolean lt() default false;

    /**
     * >
     */
    boolean gt() default false;

    /**
     * 包含上界
     */
    boolean includeUpper() default false;

    /**
     * 包含下界
     */
    boolean includeLower() default false;
}

  • 定义抽象类:AbstractSearchQueryEngine
public abstract class AbstractSearchQueryEngine<T,R> {

    @Autowired
    protected ElasticsearchRestTemplate elasticsearchRestTemplate;


    /**
     * from+size 分页查询
     * @param requestPara
     * @param clazz
     * @param pageable
     * @return
     */
    public abstract SearchHits<R> findPage(T requestPara, Class<R> clazz, Pageable pageable);


    //todo search_after 深度分页

    //todo Scroll分页...

}
  • 定义实现类
@Component
public class SimpleSearchQueryEngine extends AbstractSearchQueryEngine{

    @Override
    public SearchHits findPage(Object requestPara, Class clazz, Pageable pageable) {
        Query query = EsQueryParse.convertQuery(requestPara);
        //组装分页
        query.setPageable(pageable);
        return elasticsearchRestTemplate.search(query, clazz);
    }
}
  • 自定义注解转换工具类

/**
 * 条件构造器转换类
 * QueryBuilder.matchAllQuery(); 匹配所有
 * QueryBuilder.ermQuery精准匹配(); 大小写敏感且不支持
 * QueryBuilder.matchPhraseQuery(); 对中文精确匹配
 * QueryBuilder.matchQuery();单个匹配, field不支持通配符, 前缀具高级特性
 * QueryBuilder.multiMatchQuery("text", "field1", "field2"..); 匹配多个字段
 *
 *参考ES官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-query-builders.html
 */
public class EsQueryParse {
    public static <T> Query convertQuery(T t) {
        try {
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            Class<?> clazz = t.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                Object value = ClassUtils.getPublicMethod(clazz, "get" + captureName(field.getName())).invoke(t);
                if (value == null) {
                    continue;
                }
                if (field.isAnnotationPresent(EsLike.class)) {
                    WildcardQueryBuilder query = getLikeQuery(field, (String) value);
                    boolQueryBuilder.must(query);
                }
                if (field.isAnnotationPresent(EsEquals.class)) {
                    MatchQueryBuilder query = getEqualsQuery(field, value);
                    boolQueryBuilder.must(query);
                }
                if (field.isAnnotationPresent(EsRange.class)) {
                    RangeQueryBuilder rangeQueryBuilder = getRangeQuery(field, value);
                    boolQueryBuilder.must(rangeQueryBuilder);
                }
                if (field.isAnnotationPresent(EsIn.class)) {
                    TermsQueryBuilder query = getInQuery(field, (List<?>) value);
                    boolQueryBuilder.must(query);
                }
            }
            return queryBuilder.withQuery(boolQueryBuilder).build();
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            e.printStackTrace();
        }
        return Query.findAll();
    }

    /**
     * 一次匹配多个值
     * @param field
     * @param value
     * @return
     */
    private static TermsQueryBuilder getInQuery(Field field, List<?> value) {
        EsIn esIn = field.getAnnotation(EsIn.class);
        String filedName = StringUtils.isBlank(esIn.name()) ? field.getName() : esIn.name();
        return QueryBuilders.termsQuery(filedName, value);
    }

    /**
     * 大小范围查询
     * @param field
     * @param value
     * @return
     */
    private static RangeQueryBuilder getRangeQuery(Field field, Object value) {
        EsRange esRange = field.getAnnotation(EsRange.class);
        String filedName = StringUtils.isBlank(esRange.name()) ? field.getName() : esRange.name();
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(filedName)
                .includeLower(esRange.includeLower())
                .includeUpper(esRange.includeUpper());
        if (esRange.lt()) {
            rangeQueryBuilder.lt(value);
        }
        if (esRange.gt()) {
            rangeQueryBuilder.gt(value);
        }
        return rangeQueryBuilder;
    }

    /**
     * 分词短语匹配:QueryBuilders。matchQuery 会将搜索词分词,再与目标查询字段进行匹配,若分词中的任意一个词与目标字段匹配上,则可查询到。
     * 精准匹配短语: QueryBuilders.matchPhraseQuery()
     * 完全匹配: QueryBuilders.termQuery()
     * @param field
     * @param value
     * @return
     */
    private static MatchQueryBuilder getEqualsQuery(Field field, Object value) {
        EsEquals esEquals = field.getAnnotation(EsEquals.class);
        String filedName = StringUtils.isBlank(esEquals.name()) ? field.getName() : esEquals.name();
        return QueryBuilders.matchQuery(filedName, value);
    }

    /**
     * 模糊查询,?匹配单个字符,*匹配多个字符
     * @param field
     * @param likeValue
     * @return
     */
    private static WildcardQueryBuilder getLikeQuery(Field field, String likeValue) {
        EsLike esLike = field.getAnnotation(EsLike.class);
        String filedName = StringUtils.isBlank(esLike.name()) ? field.getName() : esLike.name();
        if (esLike.leftLike()) {
            likeValue = "%" + likeValue;
        }
        if (esLike.rightLike()) {
            likeValue = likeValue + "%";
        }
        return QueryBuilders.wildcardQuery(filedName, likeValue);
    }

    /**
     * 首字母大写
     * @param name
     * @return
     */
    public static String captureName(String name) {
        char[] cs = name.toCharArray();
        cs[0] -= 32;
        return String.valueOf(cs);
    }

}
  • Service方法
    /**
     * 单个保存
     * @param elasticsearchOrderVo
     * @return
     */
    ElasticsearchOrderVo save(ElasticsearchOrderVo elasticsearchOrderVo);

    /**
     * 批量保存
     * @param elasticsearchOrderVoList
     * @return
     */
    Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList);


    /**
     * 查询
     * @param elasticsearchOrderQueryVo
     * @return
     */
    Page findElasticsearchOrderVoList(ElasticsearchOrderQueryVo elasticsearchOrderQueryVo);
  • Service实现

@Slf4j
@Service
public class ElasticsearchOrderServiceImpl implements ElasticsearchOrderService {

    @Autowired
    private SimpleSearchQueryEngine simpleSearchQueryEngine;

    @Autowired
    private ElasticsearchOrderRepository elasticsearchOrderRepository;

    @Override
    public ElasticsearchOrderVo save(ElasticsearchOrderVo elasticsearchOrderVo) {
        return elasticsearchOrderRepository.save(elasticsearchOrderVo);
    }

    @Override
    public Iterable<ElasticsearchOrderVo>  saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList) {
        return elasticsearchOrderRepository.saveAll(elasticsearchOrderVoList);
    }

    @Override
    public Page findElasticsearchOrderVoList(ElasticsearchOrderQueryVo elasticsearchOrderQueryVo) {
        SearchHits<ElasticsearchOrderVo> searchHits =  simpleSearchQueryEngine.findPage(elasticsearchOrderQueryVo,
                ElasticsearchOrderVo.class,
                PageRequest.of(elasticsearchOrderQueryVo.getPage(),elasticsearchOrderQueryVo.getSize(),Sort.by(Sort.Order.desc("orderId"),Sort.Order.desc("createTime"))));
        Page page = new Page(elasticsearchOrderQueryVo.getPage(),elasticsearchOrderQueryVo.getSize(),searchHits.getTotalHits());
        page.setRecords(searchHits.get().collect(Collectors.toList()));
        return page;
    }
}

  • Controller
    @PostMapping("/page")
    public CommonResp findElasticsearchOrderVoList(@RequestBody ElasticsearchOrderQueryVo elasticsearchOrderQueryVo){
        return iomsOrderItemService.findElasticsearchOrderVoList(elasticsearchOrderQueryVo);
    }
  • RequestVO(ElasticsearchOrderQueryVo)
 /**
     * 分页
     */
    private Integer page;

    /**
     * 数量
     */
    private Integer size;

    /**
     * orderId
     */
    private Long orderId;

    /**
     * 订单编号
     */
    private String orderSn;

    /**
     * 收货人
     */
    @EsEquals
    private String receiverName;

    /**
     * 省市区
     */
    private String receiverAreaName;

    /**
     * 详细地址
     */
    private String receiverAddress;

    /**
     * 手机
     */
    private String receiverPhone;


    /**
     * 实际支付金额
     */
    @EsRange(lt = true)
    private Long payAmount;


    /**
     * 创建时间
     */
    private Date createTime;

  • POSTMAN相应结果:


    form+size响应结果.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容