ElasticSearch 高级
今日目标:
- ElasticSearch 高级操作
- ElasticSearch 集群管理
1 ElasticSearch查询
ElasticSearch的强大之处就在于它具备了完善切强大的查询功能。
搜索相关功能主要包括:
- 基本查询
- 分词查询
- 词条查询
- 范围查询
- 布尔查询
- Filter功能
- source筛选
- 排序
- 分页
- 高亮
- 聚合
官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-search.html
1.1 查询所有-matchAll
1.1.1 脚本
GET /user/_search
{
"query": {
"match_all": {}
}
}
1.1.2 JavaAPI
1.1.2.1 思路分析
-
创建SearchSourceBuilder对象
- 添加查询条件QueryBuilders
- 如:添加排序、分页等其它条件
-
创建SearchRequest对象,并制定索引库名称
-
添加SearchSourceBuilder对象到SearchRequest对象source中
-
发起请求,得到结果
-
解析结果SearchResponse
-
获取总条数
-
获取SearchHits数组,并遍历
-
获取其中的
_source
,是JSON数据 -
把
_source
反序列化为User对象
-
-
1.1.2.2 代码实现
/**
* 查询所有
* @throws IOException
*/
@Test
public void testBasicSearch() throws IOException {
// 1.创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1.1.添加查询条件QueryBuilders,这里选择match_all,查询所有
sourceBuilder.query(
QueryBuilders.matchAllQuery()
);
// 1.2.添加排序、分页等其它条件(暂忽略)
// 2.创建SearchRequest对象,并指定索引库名称
SearchRequest request = new SearchRequest("user");
// 3.添加SearchSourceBuilder对象到SearchRequest对象中
request.source(sourceBuilder);
// 4.发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 5.解析结果
SearchHits searchHits = response.getHits();
// 5.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 5.2.获取SearchHit数组,并遍历
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
//获取分数
System.out.println("文档得分:"+hit.getScore());
// - 获取其中的`_source`,是JSON数据
String json = hit.getSourceAsString();
// - 把`_source`反序列化为User对象
User user = JSON.parseObject(json, User.class);
System.out.println("user = " + user);
}
}
1.2.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lVwYsCtC-1614743333231)(assets/image-20201116002222916.png)]
1.2 词条查询-termQuery
1.2.1 脚本
term查询和字段类型有关系,首先回顾一下ElasticSearch两个数据类型
ElasticSearch两个数据类型
-
text:会分词,不支持聚合
-
keyword:不会分词,将全部内容作为一个词条,支持聚合
term查询:不会对查询条件进行分词。
# 词条查询
GET /user/_search
{
"query": {
"term": {
"name": {
"value": "小红"
}
}
}
}
term查询,查询text类型字段时,只有其中的单词相匹配都会查到,text字段会对数据进行分词
GET /user/_search
{
"query": {
"term": {
"note": { # note 的类型是 text
"value": "黑马"
}
}
}
}
1.2.2 JavaAPI
1.2.2.1 思路分析
-
创建SearchSourceBuilder对象
- 添加查询条件QueryBuilders.termQuery()
-
创建SearchRequest对象,并制定索引库名称
-
添加SearchSourceBuilder对象到SearchRequest对象source中
-
发起请求,得到结果
-
解析结果SearchResponse
-
获取总条数
-
获取SearchHits数组,并遍历
-
获取其中的
_source
,是JSON数据 -
把
_source
反序列化为User对象
-
-
1.2.2.2 代码实现
/**
* 词条查询termQuery-不分词
* @throws Exception
*/
@Test
public void testTermQuery() throws Exception{
//1. 创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1. 添加查询条件QueryBuilders.termQuery()
sourceBuilder.query(QueryBuilders.termQuery("name", "小红"));
// sourceBuilder.query(QueryBuilders.termQuery("note", "黑马"));
//2. 创建SearchRequest对象,并制定索引库名称
SearchRequest request = new SearchRequest("user");
//3. 添加SearchSourceBuilder对象到SearchRequest对象source中
request.source(sourceBuilder);
//4. 发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//5. 解析结果SearchResponse
SearchHits searchHits = response.getHits();
// 1. 获取总条数
System.out.println("总记录数:" + searchHits.getTotalHits().value);
// 2. 获取SearchHits数组,并遍历
for (SearchHit searchHit : searchHits) {
// * 获取其中的`_source`,是JSON数据
String userJson = searchHit.getSourceAsString();
// * 把`_source`反序列化为User对象
User user = JSON.parseObject(userJson, User.class);
System.out.println(user);
}
}
1.2.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WdNnF9T-1614743333236)(assets/image-20201116004622692.png)]
小结:
term查询特点对搜索的关键词不分词查询
1.3 分词匹配查询-matchQuery
1.3.1 脚本
match查询:
- 会对查询条件进行分词。
- 然后将分词后的查询条件和词条进行等值匹配
- 默认取并集(OR)
# match查询
GET /user/_search
{
"query": {
"match": {
"name": "小红"
}
}
}
# 查看分词效果
GET /_analyze
{
"text": "小红"
}
1.3.2 JavaAPI
1.3.2.1 思路分析
我们通过上面的代码发现,很多的代码都是重复的,所以我们来抽取一下通用代码。
我们只需要传递构建的条件对象即可完成查询。
1.3.2.2 代码实现
- 抽取通用方法代码
/**
* 抽取通用构建查询条件执行查询方法
* @throws Exception
*/
public void printResultByQuery(QueryBuilder queryBuilder) throws Exception{
//1. 创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// ************ 构建查询条件************
sourceBuilder.query(queryBuilder);
//2. 创建SearchRequest对象,并制定索引库名称
SearchRequest request = new SearchRequest("user");
//3. 添加SearchSourceBuilder对象到SearchRequest对象source中
request.source(sourceBuilder);
//4. 发起请求,得到结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//5. 解析结果SearchResponse
SearchHits searchHits = response.getHits();
// 1. 获取总条数
System.out.println("总记录数:" + searchHits.getTotalHits().value);
// 2. 获取SearchHits数组,并遍历
for (SearchHit searchHit : searchHits) {
// * 获取其中的`_source`,是JSON数据
String userJson = searchHit.getSourceAsString();
// * 把`_source`反序列化为User对象
User user = JSON.parseObject(userJson, User.class);
System.out.println(user);
}
}
- 基于抽取方法测试 matchQuery 匹配查询
/**
* 匹配查询MatchQuery 对条件进行分词
* @throws Exception
*/
@Test
public void testMatchQuery() throws Exception{
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "小红");
printResultByQuery(queryBuilder);
}
1.3.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bSPrjwnI-1614743333238)(assets/image-20201116010112780.png)]
小结:
- term query会去倒排索引中寻找确切的term,它并不知道分词器的存在。这种查询适合keyword 、numeric、date
- match query知道分词器的存在。并且理解是如何被分词的
1.4 模糊查询-fuzzy
1.4.1 脚本
fuzzy查询默认会按照一定的规则来修正当前查询的条件,修正的次数最大是 2次,可取值为:0、1、2,大于2则按照最大值计算。
# fuzzy 模糊查询
GET /user/_search
{
"query": {
"fuzzy": {
"note": {
"value": "黑马程序员", # 输入 黑程马序员
"fuzziness": 2 # 修正的次数
}
}
}
}
1.4.2 JavaAPI
1.4.2.1 思路分析
- 通过 QueryBuilders.fuzzyQuery
- 并且设置 修正的次数,最大为2
1.4.2.2 代码实现
/**
* 模糊查询FuzzyQuery 对查询条件可以修正
* @throws Exception
*/
@Test
public void testFuzzyQuery() throws Exception{
FuzzyQueryBuilder queryBuilder = QueryBuilders.fuzzyQuery("note", "黑程马序员");
queryBuilder.fuzziness(Fuzziness.TWO);
printResultByQuery(queryBuilder);
}
1.4.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BMhl1jc6-1614743333241)(assets/image-20201116011853178.png)]
小结:最大的修正次数是2次
1.5 范围&排序查询-range&sort
1.5.1 脚本
# 范围查询&排序
GET user/_search
{
"query": {
"range": {
"age": { # 范围查询字段
"gte": 22,
"lt": 27
}
}
},
"sort": [ # 排序,如果是多个条件则在数组中添加排序列即可
{
"id": {
"order": "asc"
}
}
]
}
注意: 不能使用分词的字段排序
1.5.2 JavaAPI
1.5.2.1 思路分析
- 构建范围查询对象 QueryBuilders.rangeQuery
- 在
sourceBuilder
添加排序条件(排序是对结果的重组,对条件不产生影响)
1.5.2.2 代码实现
- 编写测试方法
/**
* 条件查询 + 排序
* @throws Exception
*/
@Test
public void testRangeQuery() throws Exception{
RangeQueryBuilder queryBuilder = QueryBuilders.rangeQuery("age");
// 22 <= age < 27
queryBuilder.gte(22);
queryBuilder.lt(27);
printResultByQuery(queryBuilder);
}
- 在printResultByQuery方法中
sourceBuilder.query(queryBuilder)
后添加排序:
// ***** 添加排序
sourceBuilder.sort("id", SortOrder.DESC);
1.5.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8LsxL9o-1614743333243)(assets/image-20201116110306131.png)]
1.6 多条件查询-queryString
- queryString会对查询条件进行分词。
- 然后将分词后的查询条件和词条进行等值匹配
- 默认取并集(OR)default_operator
- 可以指定多个查询字段
query_string:识别query中的连接符(or 、and)
1.6.1 脚本
# queryString
GET user/_search
{
"query": {
"query_string": {
"fields": ["name","note"],
"query": "黑马同学" # 测试:"黑马 AND 同学"
}
}
}
query_string:有default_operator连接符的脚本
# queryString
GET user/_search
{
"query": {
"query_string": {
"fields": ["name","note"],
"query": "黑马同学",
"default_operator": "AND"
}
}
}
默认搜索域:如果我们查询时不指定搜索的域,那么就按照默认给定的搜索域查询
# queryString
GET user/_search
{
"query": {
"query_string": {
#"fields": ["name","note"],
"query": "黑马同学",
"default_operator": "AND",
"default_field": "note" # 设置默认搜索域
}
}
}
simple_query_string
:不识别query中的连接符(or 、and),查询时会将 “黑马”、“AND”、“同学”分别进行查询
# simpleQueryString
GET user/_search
{
"query": {
"simple_query_string": {
"fields": ["name","note"],
"query": "黑马同学" # 测试:"黑马 AND 同学"
}
}
}
1.6.2 JavaAPI
1.6.2.1 思路分析
- 创建QueryBuilders.queryStringQuery 条件查询指定查询的域和分隔符
1.6.2.2 代码实现
/**
* QueryString 多条件查询
* @throws Exception
*/
@Test
public void testQueryStringQuery() throws Exception{
QueryStringQueryBuilder queryBuilder =
QueryBuilders.queryStringQuery("黑马同学")
.field("name")
.field("note")
// .defaultField("note") // 默认搜索域
.defaultOperator(Operator.AND);
printResultByQuery(queryBuilder);
}
1.6.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w6YXzp36-1614743333244)(assets/image-20201116112405469.png)]
1.7 bool查询&结果过滤-boolQuery
boolQuery:对多个查询条件连接。
连接方式:
-
must(and):条件必须成立
-
must_not(not):条件必须不成立
-
should(or):条件可以成立
-
filter:条件必须成立,性能比must高。不会计算得分
**得分:**即条件匹配度,匹配度越高,得分越高
默认情况下,所有的查询条件、过滤条件都会影响打分和排名。而对搜索结果打分是比较影响性能的,因此我们一般只对用户输入的搜索条件对应的字段打分,其它过滤项不打分。此时就不能简单实用布尔查询的must来组合条件了,而是实用filter
方式。
1.7.1 脚本
# boolquery
#must和filter配合使用时,max_score(得分)是显示的
#must 默认数组形式
GET user/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"note": "同学"
}
}
],
"filter":[
{
"match": {
"note": "黑马"
}
},
{
"range":{
"age": {
"gte": 20,
"lte": 27
}
}
}
]
}
}
}
#filter 单独使用 filter可以是单个条件,也可多个条件(数组形式)
GET user/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"name": {
"value": "小武"
}
}
}
]
}
}
}
bool查询中添加查询条件一般是一个即可,然后在后面根据结果过滤,这样效率会比较高。
1.7.2 JavaAPI
1.7.2.1 思路分析
布尔查询:boolQuery
- 查询相信信息
note
为: 黑马 - 过滤姓名
name
包含:小武 - 过滤年龄
age
在:23-27
must 、filter为连接方式
term、match为不同的查询方式
1.7.2.2 代码实现
/**
* 匹配查询BoolQuery 布尔查询+过滤
* @throws Exception
*/
@Test
public void testBoolQuery() throws Exception{
// 1.构建bool条件对象
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 2.构建matchQuery对象,查询相信信息`note`为: 黑马
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("note", "黑马");
queryBuilder.must(matchQueryBuilder);
// 3.过滤姓名`name`包含:小武
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "小武");
queryBuilder.filter(termQueryBuilder);
// 4.过滤年龄`age`在:23-27
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age").gte(23).lte(27);
queryBuilder.filter(rangeQueryBuilder);
printResultByQuery(queryBuilder);
}
1.7.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4h9oQbY4-1614743333245)(assets/image-20201116120346941.png)]
1.8 分页查询-from
默认情况下ES会设置size=10,查询10条记录。 通过from
和size
来指定分页的开始位置及每页大小。
1.8.1 脚本
# 分页查询
GET user/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"note": "黑马"
}
}
]
}
},
"sort": [
{
"id": {
"order": "asc"
}
}
],
"from": 1, # 开始记录数 from = (page-1) * size
"size": 2
}
1.8.2 JavaAPI
1.8.2.1 思路分析
- 设置bool查询match匹配
- 设置id排序
- 设置分页查询
1.8.2.2 代码实现
- 新增查询方法,设置查询条件
/**
* 布尔查询 分页
* @throws Exception
*/
@Test
public void testBoolQueryByPage() throws Exception{
// 1.构建bool条件对象
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 2.构建matchQuery对象,查询相信信息`note`为: 黑马
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("note", "黑马");
queryBuilder.must(matchQueryBuilder);
printResultByQuery(queryBuilder);
}
- 在
printResultByQuery
设置分页参数
// ***** 设置分页 from size
int page = 2; // 当前页
int size = 2; // 一页显示条数
int from = (page - 1) * size; // 每一页起始条数
sourceBuilder.from(from);
sourceBuilder.size(size);
1.8.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Lzm0rXp-1614743333246)(assets/image-20201116163422591.png)]
小结:
其本质是逻辑分页,因此为了避免深度分页的问题,ES限制最多查到第10000条。
如果需要查询到10000以后的数据,你可以采用两种方式:
- scroll滚动查询
- search after
可以使用
from
和对结果进行分页,size
但是当达到深度分页时,成本变得过高。的index.max_result_window
默认值是10,000 ,这是一种保护措施,搜索请求占用的堆内存和时间成正比from + size
。建议使用滚动api进行有效的深度滚动,但是滚动上下文的成本很高,因此不建议将其用于实时用户请求。该search_after
参数通过提供活动光标来解决此问题。想法是使用上一页的结果来帮助下一页的检索。
1.9 高亮查询-highlight
高亮是在搜索结果中把搜索关键字标记出来,因此必须使用match这样的条件搜索。
elasticsearch中实现高亮的语法比较简单:
高亮三要素:
- pre_tags:前置标签,可以省略,默认是em
- post_tags:后置标签,可以省略,默认是em
- fields:需要高亮的字段
- title:这里声明title字段需要高亮,后面可以为这个字段设置特有配置,也可以空
1.9.1 脚本
# <font style="color:red">手机</font>
GET user/_search
{
"query": {
"match": {
"note": "黑马"
}
},
"highlight": { # 设置高亮
"fields": {
"note": { # 设置高亮显示的字段
"pre_tags": "<font color='red'>", # 高亮显示前缀
"post_tags": "</font>" # 高亮显示后缀
}
}
}
}
结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fgCkswWb-1614743333246)(assets/image-20201116172729832.png)]
1.9.2 JavaAPI
1.9.2.1 思路分析
- 创建高亮对象设置高亮三要素
- 解析高亮结果
- 封装到结果集中
1.9.2.2 代码实现
- 在
printResultByQuery
创建高亮对象设置高亮三要素
// ***** 设置高亮三要素
HighlightBuilder highlight = SearchSourceBuilder.highlight();
highlight.field("note"); // 高亮显示域
highlight.preTags("<font color='red'>"); // 高亮显示前缀
highlight.postTags("</font>"); // 高亮显示后缀
sourceBuilder.highlighter(highlight);
- 在
printResultByQuery
执行完成后解析结果并封装
//5. 解析结果SearchResponse
SearchHits searchHits = response.getHits();
// 1. 获取总条数
System.out.println("总记录数:" + searchHits.getTotalHits().value);
// 2. 获取SearchHits数组,并遍历
for (SearchHit searchHit : searchHits) {
// 获取其中的`_source`,是JSON数据
String userJson = searchHit.getSourceAsString();
// 把`_source`反序列化为User对象
User user = JSON.parseObject(userJson, User.class);
// ***** 解析高亮数据
HighlightField highlightField = searchHit.getHighlightFields().get("note"); // get("高亮显示域名称")
Text[] fragments = highlightField.getFragments();
String note = StringUtils.join(fragments);
// 判断如果是可以获取到数据则更新到用户对象中
if (StringUtils.isNotBlank(note)) {
user.setNote(note);
}
System.out.println(user);
}
1.9.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-elJwVn3T-1614743333247)(assets/image-20201116172339412.png)]
1.10 聚合查询-aggregation
**聚合(aggregations)**可以让我们极其方便的实现对数据的统计、分析。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
要注意:参与聚合的字段,必须是keyword类型。
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫桶
,一个叫度量
:
- **桶(bucket)-- 分组 **
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个桶
,例如我们根据国籍对人划分,可以得到中国桶
、英国桶
,日本桶
……或者我们按照年龄段对人进行划分:010,1020,2030,3040等。
Elasticsearch中提供的划分桶的方式有很多:
- Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
- Histogram Aggregation:根据数值阶梯分组,与日期类似,需要知道分组的间隔(interval)
- Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组,类似数据库group by
- Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
- ……
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
- **度量(metrics)-- 聚合 **
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量
比较常用的一些度量聚合方式:
- Avg Aggregation:求平均值
- Max Aggregation:求最大值
- Min Aggregation:求最小值
- Percentiles Aggregation:求百分比
- Stats Aggregation:同时返回avg、max、min、sum、count等
- Sum Aggregation:求和
- Top hits Aggregation:求前几
- Value Count Aggregation:求总数
小结:
-
度量聚合:相当于MySQL的聚合函数。max、min、avg、sum、count、top等
-
桶聚合:相当于MySQL的 group by 操作。不要对text类型的数据进行分组,会失败。
1.10.1 桶分组查询脚本
- 数据准备
POST /car/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2014-02-12" }
- 按照 汽车的颜色
color来
划分桶--分组
,按照颜色分桶
# 按照颜色分组
GET /car/_search
{
"aggs": {
"popular_colors": {
"terms": {
"field": "color.keyword"
}
}
},
"size": 0
}
注意:分组的域是terms
- size: 查询条数,这里设置为0,因为我们不关心搜索到的数据,只关心聚合结果,提高效率
- aggs:声明这是一个聚合查询,是aggregations的缩写
- popular_colors:给这次聚合起一个名字,可任意指定。
- terms:聚合的类型,这里选择terms,是根据词条内容(这里是颜色)划分
- field:划分桶时依赖的字段
1.10.2 桶分组查询JavaAPI
1.10.2.1 思路分析
新建一个测试类ElasticSearchAggsTest
,实现步骤:
- 创建SearchRequest对象,并制定索引库名称
- 创建SearchSourceBuilder对象,设置分组相关参数
- 添加SearchSourceBuilder对象到SearchRequest对象source中
- 执行查询
- 得到查询结果
- 解析分组查询数据
1.10.2.2 代码实现
//ES测试类
public class ElasticSearchAggsTest {
//客户端对象
private RestHighLevelClient client;
/**
* 桶聚合分组查询
* @throws Exception
*/
@Test
public void testAggsTest() throws Exception{
// 1.创建SearchRequest对象,并制定索引库名称
SearchRequest searchRequest = new SearchRequest("car");
//2.创建SearchSourceBuilder对象,设置分组相关参数
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(
AggregationBuilders
.terms("popular_color")
.field("color.keyword"));
// 分页参数
sourceBuilder.size(0);
// 3.添加SearchSourceBuilder对象到SearchRequest对象source中
searchRequest.source(sourceBuilder);
// 4.执行查询
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 5.得到查询结果
SearchHits responseHits = response.getHits();
TotalHits totalHits = responseHits.getTotalHits();
System.out.println("total:"+totalHits.value);
// 6.解析分组查询数据
Terms terms = (Terms) response.getAggregations().asMap().get("popular_color");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket : buckets) {
System.out.println(bucket.getKey()+" : "+ bucket.getDocCount());
}
}
/**
* 建立连接
*/
@Before
public void init() throws IOException {
//创建Rest客户端
client = new RestHighLevelClient(
RestClient.builder(
//如果是集群,则设置多个主机,注意端口是http协议的端口
new HttpHost("192.168.200.129", 9200, "http")
)
);
}
/**
* 关闭客户端连接
*/
@After
public void close() throws IOException {
client.close();
}
}
1.10.2.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pTibfBId-1614743333248)(assets/image-20201117153714947.png)]
1.10.3 桶内度量查询脚本
前面的例子告诉我们每个桶里面的文档数量,这很有用。 但通常,我们的应用需要提供更复杂的文档度量。 例如,每种颜色汽车的平均价格是多少?
因此,我们需要告诉Elasticsearch使用哪个字段
,使用何种度量方式
进行运算,这些信息要嵌套在桶
内,度量
的运算会基于桶
内的文档进行
需求:各个颜色的车的平均价格
# 求各个颜色的车的平均价格
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color.keyword"
},
"aggs":{
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
- aggs:我们在上一个aggs(popular_colors)中添加新的aggs。可见度量也是一个聚合
- avg_price:聚合的名称
- avg:度量的类型,这里是求平均值
- field:度量运算的字段
1.10.4 桶内度量JavaAPI
1.10.4.1 思路分析
- 设置分组参数popular_color
- 分组后平均价格计算
- 按颜色分组后添加到子聚合计算subAggregation
- 执行查询后解析平均价格
1.10.4.2 代码实现
/**
* 桶聚合分组查询
* @throws Exception
*/
@Test
public void testAggsAvgTest() throws Exception{
// 1.创建SearchRequest对象,并制定索引库名称
SearchRequest searchRequest = new SearchRequest("car");
//2.创建SearchSourceBuilder对象,设置分组相关参数
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// ***设置分组参数
TermsAggregationBuilder popularColorAggs = AggregationBuilders.terms("popular_color").field("color.keyword");
// ***分组后平均价格计算
AvgAggregationBuilder priceAggs = AggregationBuilders.avg("avg_price").field("price");
// ***按颜色分组后添加到子聚合计算
popularColorAggs.subAggregation(priceAggs);
sourceBuilder.aggregation(popularColorAggs);
// 分页参数
sourceBuilder.size(0);
// 3.添加SearchSourceBuilder对象到SearchRequest对象source中
searchRequest.source(sourceBuilder);
// 4.执行查询
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 5.得到查询结果
SearchHits responseHits = response.getHits();
TotalHits totalHits = responseHits.getTotalHits();
System.out.println("total:"+totalHits.value);
// 6.解析分组查询数据
Terms terms = (Terms) response.getAggregations().asMap().get("popular_color");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket : buckets) {
// ***解析分组数据
System.out.println(bucket.getKey()+" : "+ bucket.getDocCount());
// ***解析平均价格
ParsedAvg parsedAvg = (ParsedAvg) bucket.getAggregations().getAsMap().get("avg_price");
System.out.println(bucket.getKey()+" 平均价格:" + parsedAvg.getValue());
}
}
1.10.4.3 测试运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YUwbE9ge-1614743333249)(assets/image-20201117160124073.png)]
1.11 相关API总结
1.11.1 构建查询条件API
- **SearchSourceBuilder **
在Java客户端中,SearchSourceBuilder
就是用来构建上面提到的大JSON对象,其中包含了5个方法:
- query(QueryBuilder):查询条件
- sort(String, SortOrder):排序条件
- from(int)和size(int):分页条件
- highlight(HighlightBuilder):高亮条件
- aggregation(AggregationBuilder):聚合条件
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHpOsOhN-1614743333250)(assets/image-20200105154343366-1581587688758.png)]
是不是与REST风格API的JSON对象一致?
接下来,再逐个来看每一个查询子属性。
- **查询条件QueryBuilders **
SearchSourceBuilder的query(QueryBuilder)方法,用来构建查询条件,而查询分为:
- 分词查询:MatchQuery
- 词条查询:TermQuery
- 布尔查询:BooleanQuery
- 范围查询:RangeQuery
- 模糊查询:FuzzyQuery
- …
这些查询有一个统一的工具类来提供:QueryBuilders
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EErumi9w-1614743333250)(assets/image-20200105155220039-1581587688758.png)]
1.11.2 搜索结果API
在Kibana中回顾看一下搜索结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U96JGd7Q-1614743333251)(assets/image-20200105163216526.png)]
搜索得到的结果整体是一个JSON对象,包含下列2个属性:
- hits:查询结果,其中又包含两个属性:
- total:总命中数量
- hits:查询到的文档数据,是一个数组,数组中的每个对象就包含一个文档结果,又包含:
- _source:文档原始信息
- highlight:高亮结果信息
- aggregations:聚合结果对象,其中包含多个属性,属性名称由添加聚合时的名称来确定:
- gender_agg:这个是我们创建聚合时用的
聚合名称
,其中包含聚合结果- buckets:聚合结果数组
- gender_agg:这个是我们创建聚合时用的
Java客户端中的SearchResponse代表整个JSON结果
- SearchResponse
Java客户端中的SearchResponse代表整个JSON结果,包含下面的方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q1fZeloS-1614743333252)(assets/image-20200105164513323.png)]
包含两个方法:
- getHits():返回的是SearchHits,代表查询结果
- getAggregations():返回的是Aggregations,代表聚合结果
- **SearchHits查询结果 **
SearchHits代表查询结果的JSON对象:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7uvJaalR-1614743333253)(assets/image-20200105171202561.png)]
包含下面的方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7oks6E9k-1614743333253)(assets/image-20200105165201949.png)]
核心方法有3个:
- getTotalHists():返回TotalHists,总命中数
- getHits():返回SearchHit数组
- getMaxScore():返回float,文档的最大得分
- **SearchHit结果对象 **
SearchHit封装的就是结果数组中的每一个JSON对象:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8pwbekJl-1614743333254)(assets/image-20200105171414852.png)]
包含这样的方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRNUX6sn-1614743333255)(assets/image-20200105171625893.png)]
- getSourceAsString():返回的是
_source
- getHighLightFields():返回是高亮结果
2 ElasticSearch 集群
2.1 集群概述
单点的elasticsearch存在哪些可能出现的问题呢?
- 单台机器存储容量有限
- 单服务器容易出现单点故障,无法实现高可用
- 单服务的并发处理能力有限
所以,为了应对这些问题,我们需要对elasticsearch搭建集群
- 集群和分布式:
- 集群:多个人做一样的事。
- 分布式:多个人做不一样的事
-
集群解决的问题:
- 让系统高可用
- 分担请求压力
-
分布式解决的问题:
- 分担存储和计算的压力,提速
- 解耦
-
集群和分布式架构往往是并存的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BbnsG24a-1614743333255)(img/1581042245219.png)]
2.2 ES集群相关概念
es 集群:
- ElasticSearch 天然支持分布式
- ElasticSearch 的设计隐藏了分布式本身的复杂性
ES集群相关概念:
-
集群(cluster):一组拥有共同的 cluster name 的 节点。
-
节点(node) :集群中的一个 Elasticearch 实例
-
索引(index) :es存储数据的地方。相当于关系数据库中的database概念
-
分片(shard) :索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中
解决问题:数据量太大,单点存储量有限的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Rky6ECA-1614743333256)(assets/image-20200104124440086-5602723.png)]
此处,我们把数据分成3片:shard0、shard1、shard2
-
主分片(Primary shard):相对于副本分片的定义。
-
副本分片(Replica shard)每个主分片可以有一个或者多个副本,数据和主分片一样。
数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高了!
为了在高可用和成本间寻求平衡,我们可以这样做:
- 首先对数据分片,存储到不同节点
- 然后对每个分片进行备份,放到对方节点,完成互相备份
这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXyMZwOX-1614743333257)(assets/image-20200104124551912.png)]
现在,每个分片都有1个备份,存储在3个节点:
- node0:保存了分片0和1
- node1:保存了分片0和2
- node2:保存了分片1和2
2.3 集群搭建
本章节基于Docker安装。
2.3.1 集群机器规划
cluster name | node name | IP Addr | http端口 / 通信端口 |
---|---|---|---|
itcast-es | node1 | 192.168.200.151 | 9200 / 9700 |
itcast-es | node2 | 192.168.200.152 | 9200 / 9700 |
itcast-es | node3 | 192.168.200.153 | 9200 / 9700 |
2.3.2 搭建步骤
1)在三台机器上同时执行以下命令
docker run -id --name elasticsearch \
-e "http.host=0.0.0.0" \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e http.cors.enabled=true \
-e http.cors.allow-origin="*" \
-e http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization \
-e http.cors.allow-credentials=true \
-v es-data:/usr/share/elasticsearch/data \
-v es-logs:/usr/share/elasticsearch/logs \
-v es-plugins:/usr/share/elasticsearch/plugins \
-v es-config:/usr/share/elasticsearch/config \
--privileged \
--hostname elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-p 9700:9700 \
elasticsearch:7.4.2
2)分别在三台机器上修改elasticsearch.yml
配置文件
-
配置文件位置:
1、查看目录数据卷
docker volume inspect es-config
[
{
"CreatedAt": "2020-11-17T14:32:14+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-config/_data",
"Name": "es-config",
"Options": null,
"Scope": "local"
}
]
2、进入Mountpoint对应的目录
cd /var/lib/docker/volumes/es-config/_data
3、修改每一台机器的配置文件
- node1机器
elasticsearch.yml
配置
#集群名称
cluster.name: itcast-es
#节点名称
node.name: node1
#是不是有资格主节点
node.master: true
#是否存储数据
node.data: true
#最大集群节点数
node.max_local_storage_nodes: 3
#ip地址
network.host: 0.0.0.0
network.publish_host: 192.168.200.151
#端口
http.port: 9200
#内部节点之间沟通端口
transport.tcp.port: 9700
#es7.x 之后新增的配置,节点发现
discovery.seed_hosts: ["192.168.200.151","192.168.200.152","192.168.200.153"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node1", "node2","node3"]
bootstrap.memory_lock: false
- node2机器
elasticsearch.yml
配置
#集群名称
cluster.name: itcast-es
#节点名称
node.name: node2
#是不是有资格主节点
node.master: true
#是否存储数据
node.data: true
#最大集群节点数
node.max_local_storage_nodes: 3
#ip地址
network.host: 0.0.0.0
network.publish_host: 192.168.200.152
#端口
http.port: 9200
#内部节点之间沟通端口
transport.tcp.port: 9700
#es7.x 之后新增的配置,节点发现
discovery.seed_hosts: ["192.168.200.151","192.168.200.152","192.168.200.153"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node1", "node2","node3"]
bootstrap.memory_lock: false
- node3 机器
elasticsearch.yml
配置
#集群名称
cluster.name: itcast-es
#节点名称
node.name: node3
#是不是有资格主节点
node.master: false
#是否存储数据
node.data: true
#最大集群节点数
node.max_local_storage_nodes: 3
#ip地址
network.host: 0.0.0.0
network.publish_host: 192.168.200.153
#端口
http.port: 9200
#内部节点之间沟通端口
transport.tcp.port: 9700
#es7.x 之后新增的配置,节点发现
discovery.seed_hosts: ["192.168.200.151","192.168.200.152","192.168.200.153"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node1", "node2","node3"]
bootstrap.memory_lock: false
3)分别重启三台es机器
docker restart elasticsearch
# 注意:重启之前把 data和logs文件夹清空
4)访问http://192.168.200.151:9200/_cat/health?v 查看集群状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ti7vZ4wU-1614743333258)(assets/image-20201117181600809.png)]
健康状况结果解释: cluster: 集群名称 status: 集群状态 #green代表健康; #yellow代表分配了所有主分片,但至少缺少一个副本,此时集群数据仍旧完整; #red 代表部分主分片不可用,可能已经丢失数据。 node.total: 代表在线的节点总数量 node.data: 代表在线的数据节点的数量 shards: 存活的分片数量 pri: 存活的主分片数量 正常情况下 shards的数量是pri的两倍。 relo: 迁移中的分片数量,正常情况为 0 init: 初始化中的分片数量 正常情况为 0 unassign: 未分配的分片 正常情况为 0 pending_tasks: 准备中的任务,任务指迁移分片等 正常情况为 0 max_task_wait_time: 任务最长等待时间 active_shards_percent: 正常分片百分比 正常情况为 100%
可以访问:http://192.168.200.153:9200/_cat/nodes?v&pretty 查看集群
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NJWD7QEb-1614743333258)(assets/image-20201117181713952.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ri87oqXi-1614743333259)(assets/image-20201117185007437.png)]
2.4 kibana管理集群
Docker 执行下方命令:
docker run -di --name kibana -p 5601:5601 -v kibana-config:/usr/share/kibana/config kibana:7.4.2
kibana.yml 其他配置:
#支持中文
i18n.locale: "zh-CN"
##5601避免与之前的冲突
server.port: 5601
server.host: "0"
server.name: "kibana-itcast-cluster"
elasticsearch.hosts: ["http://192.168.200.151:9200","http://192.168.200.152:9200","http://192.168.200.153:9200"]
#elasticsearch.hosts: ["http://192.168.200.151:9200"]
elasticsearch.requestTimeout: 99999
浏览器访问:http://192.168.200.151:5601/app/monitoring#/no-data?_g=()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrtYsIyK-1614743333260)(assets/image-20201117192554150.png)]
2.5 JavaAPI 访问集群
//客户端对象
private RestHighLevelClient client;
/**
* 建立连接
*/
@Before
public void init() throws IOException {
//创建Rest客户端
client = new RestHighLevelClient(
RestClient.builder(
//如果是集群,则设置多个主机,注意端口是http协议的端口
new HttpHost("192.168.200.151", 9200, "http")
,new HttpHost("192.168.200.152", 9200, "http")
,new HttpHost("192.168.200.153", 9200, "http")
)
);
}
/**
* 创建索引库-测试
* @throws Exception
*/
@Test
public void testCreateIndex() throws Exception{
// 1 创建CreateIndexRequest对象,并指定索引库名称
CreateIndexRequest indexRequest = new CreateIndexRequest("user");
// 2 设置指定settings配置(可以默认)
indexRequest.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 1)
);
// 3 设置mapping
indexRequest.mapping( "{\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"gender\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"note\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }", XContentType.JSON);
// 4 发起请求
CreateIndexResponse response = client.indices().create(indexRequest, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
/**
* 关闭客户端连接
*/
@After
public void close() throws IOException {
client.close();
}
2.6 分片配置
在创建索引时,如果不指定分片配置,则默认主分片1,副本分片1。
在创建索引时,可以通过settings设置分片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DGo0FHjT-1614743333261)(img/1581043174004.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kwosUAdf-1614743333263)(img/1581043214369.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgqH7hna-1614743333264)(img/1581043148796.png)]
分片配置
#分片配置
#"number_of_shards": 3, 主分片数量
#"number_of_replicas": 1 主分片备份数量,每一个主分片有一个备份
# 3个主分片+3个副分片=6个分片
PUT cluster_test1
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name":{
"type": "text"
}
}
}
}
1.三个节点正常运行(0、1、2分片标号)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wu1jYlkv-1614743333264)(img/1581044158693.png)]
2.itcast-3 挂掉
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tl2Y9XEY-1614743333265)(img/1581044220349.png)]
3.将挂掉节点的分片,自平衡到其他节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LpeUtVWW-1614743333266)(img/1581044251012.png)]
4.itcast-3 恢复正常后,节点分片将自平衡回去(并不一定是原来的分片)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zm3j4VHE-1614743333266)(img/1581044368389.png)]
分片与自平衡
•当节点挂掉后,挂掉的节点分片会自平衡到其他节点中
注意:分片数量一旦确定好,不能修改。
索引分片推荐配置方案:
- 每个分片推荐大小10-30GB
- 分片数量推荐 = 节点数量 * 1~3倍
思考:比如有1000GB数据,应该有多少个分片?多少个节点
- 每个分片20GB 则可以分为50个分片
- 分片数量推荐 = 节点数量 * 1~3倍 --> 50/2=25 即25个节点
2.7 路由原理
路由原理
文档存入对应的分片,ES计算分片编号的过程,称为路由。
Elasticsearch 是怎么知道一个文档应该存放到哪个分片中呢?
查询时,根据文档id查询文档, Elasticsearch 又该去哪个分片中查询数据呢?
- 路由算法 :shard_index = hash(id) % number_of_primary_shards
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zztgIT1E-1614743333267)(img/1581044026981.png)]
查询id为5的文档:假如hash(5)=17 ,根据算法17%3=2
2.8 脑裂
ElasticSearch 集群正常状态:
- 一个正常es集群中只有一个主节点(Master),主节点负责管理整个集群。如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点。
- 集群的所有节点都会选择同一个节点作为主节点。
脑裂现象:
- 脑裂问题的出现就是因为从节点在选择主节点上出现分歧导致一个集群出现多个主节点从而使集群分裂,使得集群处于异常状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gcbx1c5w-1614743333268)(img/1581042550583.png)]
脑裂产生的原因:
-
网络原因:网络延迟
-
一般es集群会在内网部署,也可能在外网部署,比如阿里云。
-
内网一般不会出现此问题,外网的网络出现问题的可能性大些。
-
-
节点负载
- 主节点的角色既为master又为data。数据访问量较大时,可能会导致Master节点停止响应(假死状态)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rPUVwOP-1614743333269)(img/1581042578379.png)]
-
JVM内存回收
- 当Master节点设置的JVM内存较小时,引发JVM的大规模内存回收,造成ES进程失去响应。
避免脑裂:
-
网络原因:
discovery.zen.ping.timeout
超时时间配置大一点。默认是3S -
节点负载:角色分离策略
- 候选主节点配置为
- node.master: true
- node.data: false
- 数据节点配置为
- node.master: false
- node.data: true
可以通过
discovery.zen.minimum_master_nodes
来设置最少可工作的候选主节点个数,建议设置为**(候选主节点数 / 2) + 1,** 比如,当有三个候选主节点时,该配置项的值为(3/2)+1=2,也就是保证集群中有半数以上的候选主节点。 - 候选主节点配置为
-
JVM内存回收:修改 config/jvm.options 文件的 -Xms 和 -Xmx 为服务器的内存一半。