Spring Boot 2.1.18 集成 Elasticsearch 6.6.2 实战指南
Spring Boot 2.1.18 集成 Elasticsearch 6.6.2 实战指南
-
- 前言:
- 一. JAVA客户端对比
- 二. 导入数据
-
- 2.1 分析创建索引
- 2.2 代码实现
- 三. ElasticSearch 查询
-
- 3.1 matchAll 查询
- 3.2 term查询
- 3.3 match查询
- 3.4 模糊查询
- 3.5 范围查询
- 3.6 字符串查询
- 3.7 布尔查询
- 3.8 分页与排序
- 3.9 聚合查询
- 3.10 高亮查询
- 四. 重建索引&索引别名
- 五. ElasticSearch 增删改文档
- 六. ElasticsearchRepository基本使用
- 七. Elasticsearch 集群搭建
-
- 7.1 集群及分布式介绍
- 7.2 相关概念
- 7.3 集群搭建
- 7.4 JavaAPI 操作集群
- 7.5 分片配置
- 7.6 路由原理
- 7.7 脑裂
- 八、关键注意事项
前言:
本文主要讲述的是springboot2.1.18和java 1.8情况下集成elasticsearch,由于Spring Boot 2.1.x默认支持的是Elasticsearch 6.4.x,而我们选择的是6.6.2版本,因此可能需要手动管理版本,确保版本兼容。
在上篇文章ElasticSearch的概念、安装、以及与spring boot简单整合中我是搭建HighLevel客户端集成到Springboot项目中,本文主要讲述的是用spring-data-elasticsearch的集成到SpringBoot项目中,也会重点讲一下ElasticsearchTemplate的各种查询方法以及可能出现的一些问题。
一. JAVA客户端对比
目标:理解不同客户端的区别,能够在项目中选择合适的客户端
transportclient:通过监听9300端口tcp进行数据传输,它可以触摸到es的API和结构,此客户端对ES的版本兼容性较差,并且它在高并发环境下会有性能问题。
restclient:restclient就是采用http协议进行交互,它相比transportclient最大的好处就是对ES版本兼容性较好。restclient也分为high-level和low-level两种,两者原理基本一致,区别最大的就是封装性。low-level各种操作都要你自己封装,并且java本身不支持json还需要引用第三方包。而high-level是针对elasticsearch的api进行高级封装,和elasticsearch的版本关联大一些。
spring-data-elasticsearch:spring官方提供的框架,使用起来非常方便,3.2.0 版本之前是基于transportclient封装的,在此之后是基于HighLevelRestClient进行封装的,因此建议使用3.2.0 及以后的版本。
spring-boot-starter-data-elasticsearch:springboot官方提供的客户端,内部使用spring-data-elasticsearch,springboot-2.2(对应spring-data-elasticsearch-3.2.0)
该如何选择客户端?
- 若使用的是springboot项目(spring-boot-starter-data-elasticsearch启动器),建议使用springboot-2.2以后版本
- 若只是一个普通spring项目,使用spring-data-elasticsearch-3.2.0以后版本。
- 若以上两种都不是,建议使用HighLevelRestClient,而非transportclient
二. 导入数据
2.1 分析创建索引
目标:理解如何分析数据并创建索引库
需求:将数据库中Goods表的数据导入到ElasticSearch中,数据需要自己造一点
创建脚本如下:
PUT goods{ \"mappings\": { \"_doc\": { \"properties\": { \"title\": { \"type\": \"text\", \"analyzer\": \"ik_smart\" }, \"price\": { \"type\": \"double\" }, \"num\": { \"type\": \"integer\" }, \"category\": { \"type\": \"keyword\" }, \"brand\": { \"type\": \"keyword\" } } } }}
DROP TABLE IF EXISTS `goods`;CREATE TABLE `goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \'商品id,同时也是商品编号\', `title` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT \'商品标题\', `price` decimal(20, 2) NOT NULL COMMENT \'商品价格,单位为:元\', `num` int(10) NOT NULL COMMENT \'库存数量\', `category` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT \'商品类别\', `brand` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT \'品牌名称\', PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 1369284 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = \'商品表\' ROW_FORMAT = Dynamic;
字段说明:
- title:商品标题
- price:商品价格
- num:商品库存
- category:商品类别
- brand:品牌名称
添加文档进行测试:
PUT goods/_doc/1{ \"title\": \"小米手机\", \"price\": 1000, \"num\": 10000, \"category\": \"手机\", \"brand\": \"小米\"}
2.2 代码实现
目标:使用ElasticsearchTemplate批量添加文档到goods索引库
1)创建maven工程
2)添加相关依赖包
<project xmlns=\"http://maven.apache.org/POM/2.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/2.0.0 http://maven.apache.org/xsd/maven-2.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.18.RELEASE</version> </parent> <groupId>cn.explame</groupId> <artifactId>springboot_es2</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies></project>
3)创建启动类
package cn.explame;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class ElasticApplication { public static void main(String[] args) { SpringApplication.run(ElasticApplication.class, args); }}
4)创建并编写application.yml文件
spring: data: # es连接信息 elasticsearch: cluster-name: elasticsearch cluster-nodes: 192.168.211.129:9300 # 数据库连接信息 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/es username: root password: 123456 # mybatis配置mybatis: # 指定xml文件位置 mapper-locations: - classpath:/mappers/*.xml type-aliases-package: cn.explame.pojo
5)编写实体类Goods
package cn.explame.pojo;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;/** * 商品表 * * @Author LK * @Date 2021/2/25 */@Document(indexName = \"goods\", type = \"_doc\")public class Goods { // 商品id @Id // 指定id,对应到ES中的_Id private Long id; // 商品标题 @Field(type = FieldType.Text, analyzer = \"ik_smart\") private String title; // 商品价格 @Field(type = FieldType.Double) private Double price; // 商品库存 @Field(type = FieldType.Integer) private Integer num; // 商品类别 @Field(type = FieldType.Keyword) private String category; // 品牌名称 @Field(type = FieldType.Keyword) private String brand; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public Integer getNum() { return num; } public void setNum(Integer num) { this.num = num; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } }
6)创建GoodsDao数据访问层
package cn.explame.dao;import cn.explame.pojo.Goods;import org.apache.ibatis.annotations.Mapper;import java.util.List;/** * 商品数据访问层 * * @Author LK * @Date 2021/2/25 */@Mapperpublic interface GoodsDao { public List<Goods> findAll();}
7)在resources目录下创建mappers文件夹,创建并编写GoodsMapper.xml文件
<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 2.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\"><mapper namespace=\"cn.explame.dao.GoodsDao\"> <select id=\"findAll\" resultType=\"Goods\"> select * from goods </select></mapper>
8)编写单元测试用例
package cn.explame.dao;import cn.explame.pojo.Goods;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;import org.springframework.data.elasticsearch.core.query.IndexQuery;import org.springframework.test.context.junit4.SpringRunner;import java.util.ArrayList;import java.util.List;/** * TODO * * @Author LK * @Date 2021/2/22 */@RunWith(SpringRunner.class)@SpringBootTestpublic class GoodsDaoTest { @Autowired private ElasticsearchTemplate template; @Autowired private GoodsDao goodsDao; @Test public void importData() throws Exception { // 1.查询数据库数据 List<Goods> goodsList = goodsDao.findAll(); // 如果不通过脚本操作,可以先创建索引库 // template.createIndex(Goods.class); // 如果不通过脚本操作,指定映射 // template.putMapping(Goods.class); // 2.循环创建请求对象 if (goodsList != null) List<IndexQuery> queries = new ArrayList<>(); for (Goods goods : goodsList) { // 2.1 创建请求对象 IndexQuery indexQuery = new IndexQuery(); indexQuery.setObject(goods); // 2.2 添加请求对象 queries.add(indexQuery); } // 2.执行操作,无返回值 template.bulkIndex(queries); } }}
运行结果:
注解说明
- @Document(indexName = “goods”, type = “_doc”) // 指定对应索引库以及类型
- @Id // 指定对应ES中_id的字段
- @Field(type = FieldType.Text, analyzer = “ik_smart”) // 指定字段的数据类型,以及分词器
- 添加文档前,先创建索引库、类型、映射信息
三. ElasticSearch 查询
3.1 matchAll 查询
目标:掌握matchAll查询的应用场景以及代码实现
应用场景:当查询列表的页面初始化时,没有任何查询条件
脚本操作
# GET 索引库名称/_search,默认展示10条数据GET goods/_search{ \"query\": { \"match_all\": {} }}
代码操作
1)改造Goods实体类,添加toString方法
package cn.explame.pojo;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;/** * 商品表 * * @Author LK * @Date 2021/2/25 */@Document(indexName = \"goods\", type = \"_doc\")public class Goods { // 商品id @Id // 指定id private Long id; // 商品标题 @Field(type = FieldType.Text, analyzer = \"ik_smart\") private String title; // 商品价格 @Field(type = FieldType.Double) private Double price; // 商品库存 @Field(type = FieldType.Integer) private Integer num; // 商品类别 @Field(type = FieldType.Keyword) private String category; // 品牌名称 @Field(type = FieldType.Keyword) private String brand; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public Integer getNum() { return num; } public void setNum(Integer num) { this.num = num; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } @Override public String toString() { return \"Goods{\" + \"id=\" + id + \", title=\'\" + title + \'\\\'\' + \", price=\" + price + \", num=\" + num + \", category=\'\" + category + \'\\\'\' + \", brand=\'\" + brand + \'\\\'\' + \'}\'; }}
2)编写测试用例
/** * 匹配全部查询 */@Testpublic void matchAllTest(){ // 构建查询条件 SearchQuery query = new NativeSearchQuery(QueryBuilders.matchAllQuery()); // 执行查询,获取运行结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
运行结果:
3.2 term查询
目标:掌握term查询的应用场景以及代码实现
应用场景:不想对搜索关键字进行分词,搜索的结果更加精确。
脚本操作
GET goods/_search{ \"query\": { \"term\": { \"title\": { \"value\": \"老人手机\" } } }}
执行搜索可以发现结果为空,为何?在前一天我们其实已经学过,term搜索是将搜索关键字的整个内容作为词条去倒排索引中进行词条的等值匹配。如果倒排索引中并没有分出\"老人手机\"这个词,就搜索不到。我们可以通过ES提供的接口看看某字符串按某分词器分出的效果:
代码操作
/** * term查询 */@Testpublic void termTest(){ // 构建查询条件 SearchQuery query = new NativeSearchQuery(QueryBuilders.termQuery(\"title\", \"老人手机\")); // 执行查询,获取运行结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
3.3 match查询
目标:掌握match查询的应用场景以及代码实现
应用场景:想对搜索关键字进行分词,搜索的结果更全面
特点
-
会对查询条件进行分词
-
然后将分词后的查询条件和词条进行等值匹配
-
默认取并集(OR)
脚本操作
GET goods/_search{ \"query\": { \"match\": { \"title\": \"老人手机\" } }}
运行结果:
若想要结果取交集(既包含手机,又包含老人),可以如下进行操作
代码操作
/** * match查询 */@Testpublic void matchTest(){ // 构建查询条件 SearchQuery query = new NativeSearchQuery(QueryBuilders.matchQuery(\"title\", \"老人手机\").operator(Operator.AND)); // 执行查询,获取运行结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
3.4 模糊查询
目标:掌握模糊查询的应用场景以及代码实现
应用场景:当使用match搜索仍然查询不到数据,可以尝试使用模糊查询,范围更广
样例:
GET goods/_search{ \"query\": { \"match\": { \"title\": \"华\" } }}
运行结果:
可以发现查询的结果中,那些title包含\"华为\"的数据查不出来,因为那些数据,没有分出\"华\"这一个字,而分出的就是\"华为\",这个时候我们若想把包含\"华为\"的数据都查出来,就可以使用模糊查询。
wildcard查询特点
-
会对查询条件进行分词
-
分出的词和索引库的词条进行模糊匹配,可以使用通配符 ?(任意单个字符) 和 * (0个或多个字符)
-
默认取结果并集
脚本操作
# 模糊匹配索引库中以华开头的词条,注意不要在华前面使用通配符,否则就和mysql数据库一样,索引失效了GET goods/_search{ \"query\": { \"wildcard\": { \"title\": { \"value\": \"华*\" } } }}
运行结果:
代码操作
/** * wildcard查询 */@Testpublic void wildcardTest(){ // 构建查询条件 SearchQuery query = new NativeSearchQuery(QueryBuilders.wildcardQuery(\"title\", \"华*\")); // 执行查询,获取查询结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
3.5 范围查询
目标:掌握范围查询的应用场景以及代码实现
应用场景:当想对数值类型的字段做区间的搜索,例如商品价格。
脚本操作
# 价格大于等于2000,小于等于3000# gte: >= lte:<= gt:> lt:<GET goods/_search{ \"query\": { \"range\": { \"price\": { \"gte\": 2000, \"lte\": 3000 } } }}
代码操作
/** * ranage查询 */@Testpublic void rangeTest(){ // 构建查询条件 SearchQuery query = new NativeSearchQuery(QueryBuilders.rangeQuery(\"price\").gte(2000).lte(3000)); // 执行查询,返回结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
3.6 字符串查询
目标:掌握字符串查询的应用场景以及代码实现
应用场景:当不知道搜索的内容存储在哪个字段时,可以使用字符串搜索
特点
- 会对查询条件进行分词
- 将分词后的查询条件和词条进行等值匹配
- 默认取并集(OR)
- 可以指定多个查询字段
脚本操作
1)不指定字段
GET goods/_search{ \"query\": { \"query_string\": { \"query\": \"华为手机\" } }}
2)指定字段
GET goods/_search{ \"query\": { \"query_string\": { \"fields\": [\"title\", \"brand\"], \"query\": \"华为手机\" } }}
运行结果:
代码操作
/** * 字符串查询 */@Testpublic void stringTest(){ // 构建查询条件 SearchQuery query = new NativeSearchQuery(QueryBuilders.queryStringQuery(\"华为手机\").field(\"title\").field(\"brand\")); // 执行查询,获取查询结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
3.7 布尔查询
目标:掌握布尔查询的应用场景以及代码实现
应用场景:当存在多个查询条件时
语法
must(and):条件必须成立must_not(not):条件必须不成立,必须和must或filter连接起来使用should(or):条件可以成立filter:条件必须成立,性能比must高(不会计算得分)
脚本操作
# 查询品牌为华为,并且title包含手机的数据GET goods/_search{ \"query\": { \"bool\": { \"must\": [ # must可以改为filter { \"term\": { \"brand\": { \"value\": \"华为\" } } }, { \"match\": { \"title\": \"手机\" } } ] } }}
运行结果:
如果想词条查询品牌为华为,或title包含手机的数据,即如下所示:
GET goods/_search{ \"query\": { \"bool\": { \"should\": [ { \"term\": { \"brand\": { \"value\": \"华为\" } } }, { \"match\": { \"title\": \"手机\" } } ] } } }
代码操作
/** * bool查询:词条查询品牌为华为,并且title包含手机的数据 */@Testpublic void boolTest(){ // 构建查询条件 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(QueryBuilders.termQuery(\"brand\", \"华为\")); boolQueryBuilder.must(QueryBuilders.matchQuery(\"title\", \"手机\")); SearchQuery query = new NativeSearchQuery(boolQueryBuilder).setPageable(PageRequest.of(0, 20)); // 执行查询,获取查询结果 List<Goods> goodsList = template.queryForList(query, Goods.class); for (Goods goods : goodsList) { System.out.println(goods); }}
3.8 分页与排序
目标:掌握分页与排序的API
脚本操作
# GET 索引库名称/_search,默认展示10条数据GET goods/_doc/_search{ \"query\": { \"match_all\": {} }, \"sort\": [ { \"price\": { \"order\": \"desc\" # 根据价格降序排序 } } ], \"from\": 0, # 从哪一条开始 \"size\": 20 # 显示多少条 }
代码操作
/** * 分页与排序 */@Testpublic void pageAndSort(){ // pageRequest.of,参数1-当前页(从0开始,0代表第一页),参数2-每页显示数量 SearchQuery query = new NativeSearchQuery(QueryBuilders.matchAllQuery()).setPageable(PageRequest.of(0, 20)); // 设置排序参数,根据价格降序排序 query.addSort(new Sort(Sort.Direction.DESC, \"price\")); // 执行查询,返回结果 AggregatedPage<Goods> page = template.queryForPage(query, Goods.class); List<Goods> goodsList = page.getContent(); // 获取文档数据 System.out.println(\"总页数 = \" + page.getTotalPages()); System.out.println(\"当前页 = \" + (page.getNumber() + 1)); System.out.println(\"总条数 = \" + page.getTotalElements()); System.out.println(\"每页显示条数 = \" + page.getNumberOfElements()); for (Goods goods : goodsList) { System.out.println(goods); }}
3.9 聚合查询
目标:掌握聚合查询的应用场景以及代码实现
- 指标聚合:相当于MySQL的聚合函数。max、min、avg、sum等
- 桶聚合:相当于MySQL的 group by 操作。(不要对text类型的数据进行分组,会失败)
脚本操作
1)指标聚合
# 查询价格最贵的华为GET goods/_search{ \"query\": { \"match\": { \"title\": \"华为\" } }, \"aggs\": { \"max_price\": { # max_price可以自定义名称 \"max\": { \"field\": \"price\" } } }}
运行结果:
2)桶聚合
# 查询title包含手机的品牌GET goods/_search{ \"query\": { \"match\": { \"title\": \"手机\" } }, \"aggs\": { \"NAME\": { \"terms\": { \"field\": \"brand\", \"size\": 200 } } }}
运行结果:
代码操作
1)指标聚合
/** * 聚合查询 */@Testpublic void aggregateTest(){ // 构造查询条件 NativeSearchQueryBuilder query = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchQuery(\"title\", \"手机\")); // 添加聚合条件 query.addAggregation(AggregationBuilders.max(\"max_price\").field(\"price\")); // 执行查询,获取查询结果 AggregatedPage<Goods> page = template.queryForPage(query.build(), Goods.class); // 获取聚合函数结果 Max maxPrice = (Max)page.getAggregation(\"max_price\"); System.out.println(max.getValue());}
运行结果:
2)桶聚合
/** * 聚合查询 */@Testpublic void aggregateTest2(){ // 构造查询条件 NativeSearchQueryBuilder query = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchQuery(\"title\", \"手机\")); // 添加聚合条件 query.addAggregation(AggregationBuilders.terms(\"goods_brand\").field(\"brand\")); // 执行查询,获取查询结果 AggregatedPage<Goods> page = template.queryForPage(query.build(), Goods.class); // 获取聚合函数结果 Terms terms = (Terms) page.getAggregation(\"goods_brand\"); List<? extends Terms.Bucket> buckets = terms.getBuckets(); for (Terms.Bucket bucket : buckets) { System.out.println(bucket.getKey() + \",\" + bucket.getDocCount()); }}
运行结果:
3.10 高亮查询
目标:掌握高亮查询的代码实现
分析:高亮显示如何实现?
高亮三要素:
- 高亮字段
- 前缀
- 后缀
脚本操作
GET goods/_search{ \"query\": { \"match\": { \"title\": \"华为\" } }, \"highlight\": { \"fields\": { \"title\": { \"pre_tags\": \"\", \"post_tags\": \"\" } } }}
运行结果:
代码操作
/** * 高亮查询 */@Testpublic void highLightTest() { // 构造查询条件 NativeSearchQueryBuilder query = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchQuery(\"title\", \"华为\")).withPageable(PageRequest.of(0, 10)); // 设置高亮前缀后缀 String preTag = \"\"; String postTag = \"\"; // 设置高亮对象 query.withHighlightFields(new HighlightBuilder.Field(\"title\").preTags(preTag).postTags(postTag)); // 执行查询,获取查询结果 AggregatedPage<Goods> page = template.queryForPage(query.build(), Goods.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) { List<Goods> goodsList = new ArrayList<>(); // 通过响应对象获取返回数据 SearchHits hits = searchResponse.getHits(); try { // 遍历文档数据 for (SearchHit hit : hits) { String sourceJsonString = hit.getSourceAsString(); // 将文档json转为对象 Goods goods = new ObjectMapper().readValue(sourceJsonString, Goods.class); // 获取title高亮显示封装后的数据 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if (highlightFields != null) { HighlightField title = highlightFields.get(\"title\"); if (title != null) { String titleAfterHighlight = title.getFragments()[0].toString(); // 重新设置title为高亮显示封装后的数据 goods.setTitle(titleAfterHighlight); } } goodsList.add(goods); } if (goodsList.size() > 0) { return (AggregatedPage<T>) new AggregatedPageImpl<Goods>(goodsList, pageable, hits.getTotalHits()); } } catch (Exception e) { } return null; } }); // 总页数 System.out.println(\"总页数 = \" + page.getTotalPages()); // 当前页 System.out.println(\"当前页 = \" + (page.getNumber() + 1)); // 每页显示数 System.out.println(\"每页显示数 = \" + page.getNumberOfElements()); // 总条数 System.out.println(\"总条数 = \" + page.getTotalElements()); // 文档数据 List<Goods> goodsList = page.getContent(); for (Goods goods : goodsList) { System.out.println(goods); }}
运行结果:
四. 重建索引&索引别名
目标:掌握重建索引&索引别名的应用场景以及脚本操作
应用场景:随着业务需求的变更,结构可能发生改变。ES的索引一旦创建,只允许添加字段,不允许改变字段(因为改变字段,需要重建倒排索引,影响内部缓存结构,性能太低),那么此时,就需要重建一个新的索引,并将原有索引的数据导入到新索引中。
操作步骤
1)创建一个索引库student_index_v1,并指定映射信息
# 创建一个索引库student_index_v1PUT student_index_v1{ \"mappings\": { \"_doc\":{ \"properties\":{ \"birthday\":{ \"type\": \"date\" } } } }}
2)往student_index_v1索引库添加数据
# 往student_index_v1索引库添加数据PUT student_index_v1/_doc/1{ \"birthday\": \"1990-01-01\"}
3)此时,由于业务需求变更,需要往birthday字段添加一个1990年01月01日的数据
PUT student_index_v1/_doc/1{ \"birthday\": \"1990年01月01日\"}
毫无疑问添加失败,因为birthday的类型是date,不支持这种数据格式,那我们就需要修改birthday字段的数据类型为text或keyword,但是前面也提过,ES是不准我们修改字段的,因此就需要用到重建索引
4)再创建一个索引库student_index_v2,并指定映射信息
# 创建student_index_v2索引库PUT student_index_v2{ \"mappings\": { \"_doc\":{ \"properties\":{ \"birthday\":{ \"type\": \"text\" } } } }}
5)将student_index_v1中的数据导入到student_index_v2中
# 重建索引,将student_index_v1中的数据导入到student_index_v2中POST _reindex{ \"source\": { \"index\": \"student_index_v1\" }, \"dest\": { \"index\": \"student_index_v2\" }}
6)查看student_index_v2可以发现数据已经导过来了,而且此时也可以添加新数据比如1990年01月01日,操作如下:
# 查看student_index_v2数据GET student_index_v2/_search# 插入1990年01月01日到student_index_v2PUT student_index_v2/_doc/2{ \"birthday\": \"1990年01月01日\"}# 再查看student_index_v2数据GET student_index_v2/_search
7)现在仍然存在一个问题,比方说我们之前的java代码中已经写死了索引库名称,如果重建了索引,新数据还往旧的索引库里插入肯定是不行的,这个时候就需要用到另一个操作:索引别名
# 先删除旧的索引库DELETE student_index_v1 # 给student_index_v2起个别名student_index_v1POST student_index_v2/_alias/student_index_v1
这个时候,操作student_index_v2索引库,可以用student_index_v2,也可以用student_index_v1
五. ElasticSearch 增删改文档
目标:掌握template增删改文档的应用场景及代码实现
应用场景:当数据库的数据发生了增删改,需要同步数据至索引库。
代码实现
1)新增文档/修改文档
/** * 添加/修改文档 */@Testpublic void addOrUpdateDocTest(){ // 创建文档数据 Goods goods = new Goods(); // 如果已经存在该id,即修改文档 goods.setId(99999l); goods.setTitle(\"娃娃私人订制111\"); goods.setBrand(\"日本牌111\"); goods.setCategory(\"玩具类111\"); goods.setPrice(100d); goods.setNum(9999); IndexQuery query = new IndexQuery(); query.setObject(goods); String id = template.index(query); System.out.println(id);}
# 通过脚本操作是否添加数据GET goods/_search{ \"query\": { \"match\": { \"title\": \"娃娃\" } }}
2)删除文档
/** * 删除文档 */@Testpublic void deleteDoc(){ // 根据id删除 String id = template.delete(Goods.class, \"99999\"); // 根据条件删除 //DeleteQuery query = new DeleteQuery(); //query.setQuery(QueryBuilders.matchAllQuery()); //template.delete(query, Goods.class);}
六. ElasticsearchRepository基本使用
目标:掌握ElasticsearchRepository基本使用
ElasticsearchRepository 是spring-data框架提供的一个接口,封装了ES的一些增删查改的基本API,使用起来比较方便。
使用步骤
1)创建GoodsRepository接口,让其继承ElasticsearchRepository
/** * TODO * * @Author LK * @Date 2021/3/2 */public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> { }
2)在调用GoodsRepository时,可以发现里面多了一些CRUD的方法
// 添加、修改文档<S extends T> S save(S var1);// 添加、修改文档<S extends T> S index(S entity);// 根据条件查询,返回集合Iterable<T> search(QueryBuilder query);// 根据条件查询,返回分页对象Page<T> search(QueryBuilder query, Pageable pageable);// 根据条件查询,返回分页对象Page<T> search(SearchQuery searchQuery);// 根据id删除void deleteById(ID var1);// 根据条件删除void delete(T var1);// 批量删除指定文档void deleteAll(Iterable<? extends T> var1);// 删除所有void deleteAll();
七. Elasticsearch 集群搭建
7.1 集群及分布式介绍
目标:理解什么是集群、分布式
- 集群:多个人做一样的事。
- 分布式:多个人做不一样的事。
说明:在一个系统中,往往分布式和集群是并存的。
7.2 相关概念
目标:理解ES集群中的一些相关概念
- 节点(node) :集群中的一个 Elasticearch 服务实例。在Elasticsearch中,节点的类型主要分为如下几种:
- master eligible节点:有资格参加选举成为Master的节点,默认为true(可以通过node.master: false设置)。
- data节点:保存数据的节点,默认为true(可以通过node.data: false设置)。
- Coordinating 节点:客户端节点。负责接收客户端请求,将请求发送到合适的节点,最终把结果汇集到一起返回,默认为true。
- 集群(cluster):一组拥有相同集群名称的节点,集群名称默认是elasticsearch。
- 索引(index) :es存储数据的地方,相当于关系数据库中的database。
- 分片(shard):索引库可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引库的不同分片可以拆分到放到不同的节点中,分片的好处有如下两点。
- 提高查询性能(多个节点并行查询)
- 提高数据安全性(鸡蛋不要放在一个篮子里)
- 主分片(Primary shard):相对于副本分片的定义。
- 副本分片(Replica shard):即对主分片数据的备份,每个主分片可以有一个或者多个副本,数据和主分片一样,副本的好处有如下两点:
- 数据备份,防止数据丢失
- 一定程度提高查询的并发能力(同一份完整的索引库的数据,分成了两份,都可以查询)
说明:主分片和副本分片永远不会分配在同一个节点上
7.3 集群搭建
目标:能够参考文档搭建ES集群
请参考资料\\ElasticSearch集群搭建.md
7.4 JavaAPI 操作集群
目标:掌握如何使用javaApi操作集群
1)spring-boot-data-elasticsearch,修改yml配置即可
spring: data: # es连接信息 elasticsearch: cluster-name: explame-es # 修改集群名称 cluster-nodes: 192.168.211.129:9301,192.168.211.129:9302,192.168.211.129:9303 # 指定多个节点的地址
2)HighLevelRestApi
@Beanpublic RestHighLevelClient restHighLevelClient() { RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder( new HttpHost( \"192.168.211.129\", 9201, \"http\" ), new HttpHost( \"192.168.211.129\", 9202, \"http\" ), new HttpHost( \"192.168.211.129\", 9203, \"http\" ) )); return restHighLevelClient;}
7.5 分片配置
目标:掌握如何使用脚本设置索引分片数,以及常用分片及节点设置
-
在创建索引时,如果不指定分片配置,ES6默认主分片5,副本分片1,而ES7默认主分片1,副本分片1。
-
在创建索引时,可以通过settings设置分片
\"settings\": {\"number_of_shards\": 3, # 分片数\"number_of_replicas\": 1 # 副本数}
-
分片与自平衡:当节点挂掉后,挂掉的节点分片会自平衡到其他节点中
-
在Elasticsearch 中,每个查询在每个分片的单个线程中执行,但是可以并行处理多个分片。
-
分片数量一旦确定好,不能修改。
查看分片分布情况步骤如下:
常用配置:
1、每个分片推荐大小10-30GB
2、分片数量推荐 = 节点数量 * 1~3倍
思考:比如有1000GB数据,应该有多少个分片?多少个节点?
分片数:1000 / 20 = 50
节点数:50 / 2 = 25
7.6 路由原理
目标:理解ES中路由的原理
- 文档存入对应的分片,ES计算分片编号的过程,称为路由。
- Elasticsearch 是怎么知道一个文档应该存放到哪个分片中呢?
- 查询时,根据文档id查询文档, Elasticsearch 又该去哪个分片中查询数据呢?
- 路由算法 :shard_index(分片编号) = hash(文档id) % number_of_primary_shards(主分片个数)
假设有三个节点,三个主分片,三个副本分片
现在有个 id=5 文档要进行存储,会先会id进行hash运算得到一个数字17,17对3(分片数量)取模运算:17 % 3 = 2
最终决定存储在编号为2的分片上,即放到ES-node-3上,并且在ES-node-2节点上的副本分片上进行数据备份。
当要查询 id = 5 的文档,同样也要先进行hash计算,计算分片位置,路由到对应的分片进行数据查询。
说明:任何一个节点收到查询请求后,如果是一些词条搜索,也会根据倒排索引找到对应的id集合,再分别计算每个id的hash值,所存储的分片位置,再转发请求到分片所在的节点,最终汇总查询结果。
7.7 脑裂
目标:理解何为脑裂以及如何防止脑裂
何为脑裂?
- 一个正常es集群中只有一个主节点(Master),主节点负责管理整个集群。如创建或删除索引,并决定哪些分片分配给哪些节点。此外还跟踪哪些节点是集群的一部分。
- 脑裂就是一个集群出现多个主节点从而使集群分裂,使得集群处于异常状态。简单来说就是一个集群里只能有一个老大来指挥工作,如果有多个老大,就乱套了。
脑裂原因
-
网络原因:网络延迟
一般es集群会在内网部署,也可能在外网部署,比如阿里云。
内网一般不会出现此问题,外网的网络出现问题的可能性大些。 -
节点负载
主节点的角色既为master又为data。数据访问量较大时,可能会导致Master节点停止响应(假死状态)。 -
JVM内存回收
当Master节点设置的JVM内存较小时,引发JVM的大规模内存回收,造成ES进程失去响应
避免脑裂
脑裂产生的原因:
- 网络原因:网络延迟较高
- 节点负载:主节点的角色既为master又为data
- JVM内存回收:JVM内存设置太小
避免脑裂:
-
网络原因:discovery.zen.ping.timeout 超时时间配置大一点。默认是3S
-
节点负载:角色分离策略
-
主节点配置:
node.master: true # 是否有资格参加选举成为masternode.data: false # 是否存储数据
-
数据节点配置:
node.master: false # 是否有资格参加选举成为masternode.data: true # 是否存储数据
-
-
JVM内存回收:修改 config/jvm.options 文件的 -Xms 和 -Xmx 为服务器的内存一半。
-
还可以在选举层面解决脑裂问题(即不让第二个老大产生):
# 声明获得大于几票,主节点才有效,请设置为(master eligble nodes / 2) + 1discovery.zen.minimum_master_nodes: 2
比如上面存在8个节点(假如都是master eligble节点),那需要设置discovery.zen.minimum_master_nodes: 5,代表至少5票投某个节点,才有效。如果某个时刻两个机房网络中断了,右边的机房里四个节点揭竿而起从新选举,也不够票数。
八、关键注意事项
-
端口区别:
REST API端口:9200(HTTP协议) 传输层端口:9300(TCP协议,TransportClient使用)
-
版本一致性:
确保所有ES相关依赖版本为6.6.2检查Maven依赖树:mvn dependency:tree | grep elasticsearch