前言:
之前的项目都有用到ES,但不是自己搭建和利用,包罗ES语法和数据存储结构都不知道,趁着偶然间来学习下ES的根本利用,很早就知道ES版本兼容标题有坑,唯有自己踩坑才印象深刻;
公司服务器太多人用,动不动就搞出标题,以是我就用当地环境搭建Elasticsearch+Kibana+Spring-boot-starter-data-elasticsearch来集成,如许学习本钱是比力低的,SpringBootData已经帮我们集成好了只需开箱即用,反面在优化代码通过自定义注解提供通用ES查询,现在先把代码跑起来。
ES版本选择:
先查抄自己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组件根本先容:
集成到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")@Datapublic 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;}
@Componentpublic interface ElasticsearchOrderRepository extends ElasticsearchRepository<ElasticsearchOrderVo, String> { //泛型<T,ID> T是文档内容,ID是文档ID范例}
public interface ElasticsearchOrderService { /** * 保存 * @param elasticsearchOrderVoList * @return */ Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList);}@Slf4j@Servicepublic class ElasticsearchOrderServiceImpl implements ElasticsearchOrderService { @Autowired private ElasticsearchOrderRepository elasticsearchOrderRepository; @Override public Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList) { return elasticsearchOrderRepository.saveAll(elasticsearchOrderVoList); }}
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)); }
- 欣赏器哀求:
- 通过Kibana进去查询下数据,看下数据是否有被插入进去
这里可以看到数据是有写入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可以直接定位到指定文档,比如:
- 返回指定字段:
- 按照条件模糊匹配:名字便是小明5
- 按照条件精准匹配:名字便是小明5
官方文档参考: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分页...}
@Componentpublic 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); }}
/** * 单个保存 * @param elasticsearchOrderVo * @return */ ElasticsearchOrderVo save(ElasticsearchOrderVo elasticsearchOrderVo); /** * 批量保存 * @param elasticsearchOrderVoList * @return */ Iterable<ElasticsearchOrderVo> saveAll(List<ElasticsearchOrderVo> elasticsearchOrderVoList); /** * 查询 * @param elasticsearchOrderQueryVo * @return */ Page findElasticsearchOrderVoList(ElasticsearchOrderQueryVo elasticsearchOrderQueryVo);
@Slf4j@Servicepublic 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; }}
@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;
|