在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html
而Java的客户端就有两个:
不过Java API这个客户端(Transport Client)已经在7.0以后过期了,而且在8.0版本中将直接废弃。所以我们会学习Java REST Client:
然后再选择High Level REST Client这个。
Java REST Client 其实就是利用Java语言向 ES服务发 Http的请求,因此请求和操作与前面学习的REST API 一模一样。
新建基于Maven的Java项目,相关信息如下:
pom.xml:
UTF-8
UTF-8
1.8
org.elasticsearch.client
elasticsearch-rest-high-level-client
7.4.2
junit
junit
4.12
org.projectlombok
lombok
1.18.8
com.alibaba
fastjson
1.2.49
org.apache.commons
commons-lang3
3.8.1
实体类:
com.it.esdemo.pojo.User
package com.it.sh.esdemo.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @Description:
* @Version: V1.0
*/
@Data
@AllArgsConstructor
public class User {
private Long id;
private String name;// 姓名
private Integer age;// 年龄
private String gender;// 性别
private String note;// 备注
}
扩展:
使用Lombok需要两个条件:
1)引入依赖:
org.projectlombok
lombok
1.18.8
2)编辑器idea安装插件:
在线装,参考:https://plugins.jetbrains.com/plugin/6317-lombok
在官网上可以看到连接ES的教程:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-initialization.html
首先需要与ES建立连接,ES提供了一个客户端RestHighLevelClient。
代码如下:
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));
ES中的所有操作都是通过RestHighLevelClient来完成的:
为了后面测试方便,我们写到一个单元测试中,并且通过@Before
注解来初始化客户端连接。
com.it.sh.esdemo.ElasticSearchTest
package com.it.sh.esdemo;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.After;
import org.junit.Before;
import java.io.IOException;
//ES测试类
public class ElasticSearchTest {
//客户端对象
private RestHighLevelClient client;
/**
* 建立连接
*/
@Before
public void init() throws IOException {
//创建Rest客户端
client = new RestHighLevelClient(
RestClient.builder(
//如果是集群,则设置多个主机,注意端口是http协议的端口
new HttpHost("localhost", 9200, "http")
// ,new HttpHost("localhost", 9201, "http")
// ,new HttpHost("localhost", 9202, "http")
)
);
}
/**
* 创建索引库-测试
* @throws Exception
*/
@Test
public void testCreateIndex() throws Exception{
System.out.println(client);
// org.elasticsearch.client.RestHighLevelClient@6c61a903
}
/**
* 关闭客户端连接
*/
@After
public void close() throws IOException {
client.close();
}
}
开发中,往往库和映射的操作一起完成,官网详细文档地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/_index_apis.html
这里我们主要实现库和映射的创建。查询、删除等功能大家可参考文档自己实现。
按照官网给出的步骤,创建索引包括下面四个步骤:
其实仔细分析,与我们在Kibana中的Rest风格API完全一致:
PUT /hello
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
}
}
Java代码中设置mapping,依然与REST中一致,需要JSON风格的映射规则。因此我们先在kibana中给User实体类定义好映射规则。
谨记三个是否原则
User包括下面的字段:
Id:主键,在ES中是唯一标示
name:姓名
age:年龄
gender:性别
note:备注,用户详细信息
使用ik_max_word
映射如下:
PUT /user
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name":{
"type": "keyword"
},
"age":{
"type": "integer"
},
"gender":{
"type": "keyword"
},
"note":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
我们在上面新建的ElasticDemo类中新建单元测试,完成代码,思路就是之前分析的4步骤:
package com.it.sh.esdemo;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
private RestHighLevelClient client;
/**
* 创建索引
* @throws IOException
*/
@Test
public void testCreateIndex() throws IOException {
// 1.创建CreateIndexRequest对象,并指定索引库名称
CreateIndexRequest request = new CreateIndexRequest("user");
// 2.指定settings配置(可以默认)
request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 1)
);
// 3.指定mapping配置
request.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" +
" }",
//指定映射的内容的类型为json
XContentType.JSON);
// 4.发起请求,得到响应(同步操作)
CreateIndexResponse response = client.indices()
.create(request, RequestOptions.DEFAULT);
//打印结果
System.out.println("response = " + response.isAcknowledged());
}
返回结果:
response = true
文档操作包括:新增文档、查询文档、修改文档、删除文档等。
CRUD官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-supported-apis.html
新增的官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-index.html
根据官网文档,实现的步骤如下:
新增文档:
/**
* 测试插入一个文档
* @throws IOException
*/
@Test
public void addDocument() throws Exception{
//1. 准备文档数据
User user = new User(110L, "张三", 22, "0", "上海市青浦区徐金珍");
//2. 创建IndexRequest对象,并指定索引库名称
IndexRequest indexRequest = new IndexRequest("user");
//3. 指定新增的数据的id
indexRequest.id(user.getId().toString());
//4. 将新增的文档数据变成JSON格式
// user.setAge(null);
String userJson = JSON.toJSONString(user);
//5. 将JSON数据添加到IndexRequest中
indexRequest.source(userJson, XContentType.JSON);
//6. 发起请求,得到结果
IndexResponse response = client.index(indexRequest, RequestOptions.DEFAULT);
System.out.println("indexResponse= "+response.getResult());
}
结果:
indexResponse = CREATED
注意:新增的ID一致时,是执行修改操作
我们直接测试过,新增的时候如果ID存在则变成修改,我们试试,再次执行刚才的代码,可以看到结果变了:
indexResponse = UPDATED
结论:在ES中如果ID一致则执行修改操作,其实质是先删除后添加。
官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-get.html
这里的查询是根据id查询,必须知道文档的id才可以。
根据官网文档,实现的步骤如下:
/**
* 测试根据id查询一个文档
* @throws IOException
*/
@Test
public void testfindDocumentById() throws Exception{
//1. 创建GetRequest 对象,并指定索引库名称、文档ID
GetRequest getRequest = new GetRequest("user", "110");
//2. 发起请求,得到结果
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
//3. 从结果中得到source,是json字符串
String sourceAsString = response.getSourceAsString();
//4. 将JSON反序列化为对象
User user = JSON.parseObject(sourceAsString, User.class);
System.out.println(user);
}
结果如下:
User(id=110, name=张三, age=null, gender=0, note=上海市青浦区徐金珍)
官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-delete.html
/**
* 根据id删除文档
* @throws IOException
*/
@Test
public void testDeleteDocumentById() throws IOException {
// 1.创建DeleteRequest对象,指定索引库名称、文档ID
DeleteRequest request = new DeleteRequest(
"user",
"110");
// 2.发起请求
DeleteResponse deleteResponse = client.delete(
request, RequestOptions.DEFAULT);
System.out.println("deleteResponse = " + deleteResponse.getResult());
}
结果:
deleteResponse = DELETED
如果我们需要把数据库中的所有用户信息都导入索引库,可以批量查询出多个用户,但是刚刚的新增文档是一次新增一个文档,这样效率太低了。
因此ElasticSearch提供了批处理的方案:BulkRequest
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-bulk.html
# 批量导入的脚本
POST _bulk
{"index":{"_index":"user","_type":"_doc","_id":"1"}}
{"age":18,"gender":"1","id":1,"name":"Rose","note":"Rose同学在学表演11"}
{"index":{"_index":"user","_type":"_doc","_id":"2"}}
{"age":38,"gender":"1","id":2,"name":"Jack","note":"Jack同学在学JavaEE"}
{"index":{"_index":"user","_type":"_doc","_id":"3"}}
{"age":38,"gender":"1","id":2,"name":"Jack","note":"Jack同学在学JavaEE"}
{"index":{"_index":"user","_type":"_doc","_id":"4"}}
{"age":23,"gender":"0","id":3,"name":"小红","note":"小红同学在学唱歌"}
{"index":{"_index":"user","_type":"_doc","_id":"5"}}
{"age":20,"gender":"1","id":4,"name":"小明","note":"小明同学在学JavaSE"}
{"index":{"_index":"user","_type":"_doc","_id":"6"}}
{"age":33,"gender":"1","id":5,"name":"达摩","note":"达摩和尚在达摩院学唱歌"}
{"index":{"_index":"user","_type":"_doc","_id":"7"}}
{"age":24,"gender":"1","id":6,"name":"鲁班","note":"鲁班同学走在乡间小路上"}
{"index":{"_index":"user","_type":"_doc","_id":"8"}}
{"age":26,"gender":"0","id":7,"name":"孙尚香","note":"孙尚香同学想带阿斗回东吴"}
{"index":{"_index":"user","_type":"_doc","_id":"9"}}
{"age":27,"gender":"1","id":8,"name":"李白","note":"李白同学在山顶喝着酒唱着歌"}
{"index":{"_index":"user","_type":"_doc","_id":"10"}}
{"age":28,"gender":"0","id":9,"name":"甄姬","note":"甄姬同学弹奏一曲东风破"}
{"index":{"_index":"user","_type":"_doc","_id":"11"}}
{"age":27,"gender":"0","id":10,"name":"虞姬","note":"虞姬同学在和项羽谈情说爱"}
A
BulkRequest
can be used to execute multiple index, update and/or delete operations using a single request.
一个BulkRequest可以在一次请求中执行多个 新增、更新、删除请求。
所以,BulkRequest就是把多个其它增、删、改请求整合,然后一起发送到ES来执行。
我们拿批量新增来举例,步骤如下:
/**
* 大量数据批量添加
* @throws IOException
*/
@Test
public void testBulkAddDocumentList() throws IOException {
// 1.从数据库查询文档数据
//第一步:准备数据源。本案例使用List来模拟数据源。
List users = Arrays.asList(
new User(1L, "Rose", 18, "1", "Rose同学在学表演"),
new User(2L, "Jack", 38, "1", "Jack同学在学JavaEE"),
new User(3L, "小红", 23, "0", "小红同学在学唱歌"),
new User(4L, "小明", 20, "1", "小明同学在学JavaSE"),
new User(5L, "达摩", 33, "1", "达摩和尚在达摩院学唱歌"),
new User(6L, "鲁班", 24, "1", "鲁班同学走在乡间小路上"),
new User(7L, "孙尚香", 26, "0", "孙尚香同学想带阿斗回东吴"),
new User(8L, "李白", 27, "1", "李白同学在山顶喝着酒唱着歌"),
new User(9L, "甄姬", 28, "0", "甄姬同学弹奏一曲东风破"),
new User(10L, "虞姬", 27, "0", "虞姬同学在和项羽谈情说爱")
);
// 2.创建BulkRequest对象
BulkRequest bulkRequest = new BulkRequest();
// 3.创建多个IndexRequest对象,并添加到BulkRequest中
for (User user : userList) {
bulkRequest.add(new IndexRequest("user")
.id(user.getId().toString())
.source(JSON.toJSONString(user), XContentType.JSON)
);
}
// 4.发起请求
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println("status: " + bulkResponse.status());
}
结果如下:
status: OK
可以再Kibana中通过GET /user/_search
看到查询的结果。
提示:
可以批量处理增删改:
ElasticSearch的强大之处就在于它具备了完善切强大的查询功能。
搜索相关功能主要包括:
基本查询
分词查询
词条查询
范围查询
布尔查询
source筛选
排序
分页
高亮
聚合
官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-search.html
在Java客户端中,SearchSourceBuilder
就是用来构建上面提到的大JSON对象,其中包含了5个方法:
如图:
是不是与REST风格API的JSON对象一致?
接下来,再逐个来看每一个查询子属性。
SearchSourceBuilder的query(QueryBuilder)方法,用来构建查询条件,而查询分为:
这些查询有一个统一的工具类来提供:QueryBuilders
在Kibana中回顾看一下搜索结果:
搜索得到的结果整体是一个JSON对象,包含下列2个属性:
hits:查询结果,其中又包含两个属性:
total:总命中数量
hits:查询到的文档数据,是一个数组,数组中的每个对象就包含一个文档结果,又包含:
aggregations:聚合结果对象,其中包含多个属性,属性名称由添加聚合时的名称来确定:
gender_agg:这个是我们创建聚合时用的聚合名称
,其中包含聚合结果
Java客户端中的SearchResponse代表整个JSON结果
Java客户端中的SearchResponse代表整个JSON结果,包含下面的方法:
包含两个方法:
SearchHits代表查询结果的JSON对象:
包含下面的方法:
核心方法有3个:
SearchHit封装的就是结果数组中的每一个JSON对象:
包含这样的方法:
_source
GET /user/_search
{
"query": {
"match_all": {}
}
}
创建SearchSourceBuilder对象
创建SearchRequest对象,并制定索引库名称
添加SearchSourceBuilder对象到SearchRequest对象source中
发起请求,得到结果
解析结果SearchResponse
获取总条数
获取SearchHits数组,并遍历
_source
,是JSON数据_source
反序列化为User对象 /**
* 查询所有
* @throws IOException
*/
@Test
public void matchAllSearch() 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);
}
}
term查询和字段类型有关系,首先回顾一下ElasticSearch两个数据类型
ElasticSearch两个数据类型
term查询:不会对查询条件进行分词。
# 词条查询
GET /user/_search
{
"query": {
"term": {
"name": {
"value": "小红"
}
}
}
}
创建SearchSourceBuilder对象
创建SearchRequest对象,并制定索引库名称
添加SearchSourceBuilder对象到SearchRequest对象source中
发起请求,得到结果
解析结果SearchResponse
获取总条数
获取SearchHits数组,并遍历
_source
,是JSON数据_source
反序列化为User对象 /**
* 词条查询termQuery-不分词
* @throws Exception
*/
@Test
public void termQuery() throws Exception{
//1. 创建SearchSourceBuilder对象
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1. 添加查询条件QueryBuilders.termQuery()
sourceBuilder.query(QueryBuilders.termQuery("name", "小红"));
//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);
}
}
match查询:
# match查询
GET /user/_search
{
"query": {
"match": {
"note": "唱歌 javaee"
}
}
}
# 查看分词效果
GET /_analyze
{
"text": "唱歌 javaee",
"analyzer": "ik_max_word"
}
我们通过上面的代码发现,很多的代码都是重复的,所以我们来抽取一下通用代码。
我们只需要传递构建的条件对象即可完成查询。
/**
* 抽取通用构建查询条件执行查询方法
* @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 对条件进行分词
* @throws Exception
*/
@Test
public void matchQuery() throws Exception{
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("note", "唱歌 javaee");
printResultByQuery(queryBuilder);
}
小结:
# 范围查询&排序
GET user/_search
{
"query": {
"range": {
"age": { # 范围查询字段
"gte": 22,
"lt": 27
}
}
},
"sort": [ # 排序,如果是多个条件则在数组中添加排序列即可
{
"id": {
"order": "asc"
}
}
]
}
注意: 不能使用分词的字段排序
sourceBuilder
添加排序条件(排序是对结果的重组,对条件不产生影响)/**
* 条件查询 + 排序
* @throws Exception
*/
@Test
public void rangeQuery() throws Exception{
RangeQueryBuilder queryBuilder = QueryBuilders.rangeQuery("age");
// 22 <= age < 27
queryBuilder.gte(22);
queryBuilder.lt(27);
printResultByQuery(queryBuilder);
}
sourceBuilder.query(queryBuilder)
后添加排序: // ***** 添加排序
sourceBuilder.sort("id", SortOrder.DESC);
boolQuery:对多个查询条件连接。
连接方式:
得分: 即条件匹配度,匹配度越高,得分越高
# 查询note中包含同学
# 且性别为女的
# 年龄在20到30之间的
GET user/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"note": "同学"
}
}
],
"filter":[
{
"term": {
"gender": "0"
}
},
{
"range":{
"age": {
"gte": 20,
"lte": 30
}
}
}
]
}
}
}
bool查询中添加查询条件一般是一个即可,然后在后面根据结果过滤,这样效率会比较高。
布尔查询:boolQuery
must 、filter为连接方式
term、match为不同的查询方式
/**
* 匹配查询BoolQuery 布尔查询+过滤
* @throws Exception
*/
@Test
public void boolQuery() throws Exception{
// 1.构建bool条件对象
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 2.构建matchQuery对象,查询备注信息`note`包含: 同学
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("note", "同学");
queryBuilder.must(matchQueryBuilder);
// 3.过滤姓名`gender`性别为女:0
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("gender", "0");
queryBuilder.filter(termQueryBuilder);
// 4.过滤年龄`age`在:20-30
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age").gte(20).lte(30);
queryBuilder.filter(rangeQueryBuilder);
printResultByQuery(queryBuilder);
}
默认情况下ES会设置size=10,查询10条记录。 通过from
和size
来指定分页的开始位置及每页大小。
# 分页查询
GET user/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"note": "同学"
}
}
]
}
},
"sort": [
{
"id": {
"order": "asc"
}
}
],
"from": 1, # 开始记录数= (page-1) * size
"size": 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);
高亮是在搜索结果中把搜索关键字标记出来,因此必须使用match这样的条件搜索。
elasticsearch中实现高亮的语法比较简单:
高亮三要素:
pre_tags:前置标签,可以省略,默认是em
post_tags:后置标签,可以省略,默认是em
fields:需要高亮的字段
GET user/_search
{
"query": {
"match": {
"note": "同学"
}
},
"highlight": { # 设置高亮
"fields": {
"note": { # 设置高亮显示的字段
"pre_tags": "", # 高亮显示前缀
"post_tags": "" # 高亮显示后缀
}
}
}
}
结果:
printResultByQuery
创建高亮对象设置高亮三要素// ***** 设置高亮三要素
HighlightBuilder highlight = SearchSourceBuilder.highlight();
highlight.field("note"); // 高亮显示域
highlight.preTags(""); // 高亮显示前缀
highlight.postTags(""); // 高亮显示后缀
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);
}
# 按照性别分桶 分桶后计算每个分桶的年龄平均值
GET user/_search
{
"size": 0,
"aggs": {
"terms_by_gender":{
"terms": {
"field": "gender"
},
"aggs": {
"avg_by_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
结果:
新建一个测试类ElasticSearchAggsTest
,实现步骤:
/**
* 文档聚合统计
* @作者 it
* @创建日期 2023/3/3 8:54
**/
public class EsDemo05 {
RestHighLevelClient client;
@Test
public void aggregations() throws IOException {
//1. 创建搜索请求
SearchRequest searchRequest = new SearchRequest("user");
// 封装查询条件
SearchSourceBuilder builder = new SearchSourceBuilder();
// 通过工具类 AggregationBuilders 可以快捷的构建 聚合条件
// 方法名: 聚合类型 参数1: 自定义的聚合名称
TermsAggregationBuilder termsBuilder = AggregationBuilders.terms("terms_by_gender").field("gender");
AvgAggregationBuilder avgBuilder = AggregationBuilders.avg("avg_by_age").field("age");
// 分桶之后再求平均值
termsBuilder.subAggregation(avgBuilder);
builder.aggregation(termsBuilder);
// 设置搜索条件内容
searchRequest.source(builder);
//2. 执行搜索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 获取聚合结果 总的聚合结果
Aggregations aggregations = searchResponse.getAggregations();
// 根据自定义的聚合名称 找到对应的聚合类型处理结果
// 注意: 你是什么聚合类型,用对应的聚合类型接口来接收
Terms termsResult = aggregations.get("terms_by_gender");
// 处理的分桶信息
List extends Terms.Bucket> buckets = termsResult.getBuckets();
for (Terms.Bucket bucket : buckets) {
System.out.println("当前分桶的key==> " + bucket.getKeyAsString());
System.out.println("当前分桶的文档数量==> " + bucket.getDocCount());
// 获取 子聚合的总结果
Aggregations subAggs = bucket.getAggregations();
// 在子聚合结果中 找到对应自定名称的聚合处理结果
Avg avgResult = subAggs.get("avg_by_age");
System.out.println("当前分桶的平均值==>"+avgResult.getValue());
}
}
/**
* 初始化es的客户端
*/
@Before
public void init(){
client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.200.150",9200))
);
}
/**
* 关闭客户端
*/
@After
public void close(){
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
单点的elasticsearch存在哪些可能出现的问题呢?
所以,为了应对这些问题,我们需要对elasticsearch搭建集群
集群和分布式:
集群解决的问题:
分布式解决的问题:
集群和分布式架构往往是并存的
es 集群:
ES集群相关概念:
集群(cluster):一组拥有共同的 cluster name 的 节点。
节点(node) :集群中的一个 Elasticearch 实例
索引(index) :es存储数据的地方。相当于关系数据库中的database概念
分片(shard) :索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中
解决问题:数据量太大,单点存储量有限的问题。
> 此处,我们把数据分成3片:shard0、shard1、shard2
主分片(Primary shard):相对于副本分片的定义。
副本分片(Replica shard)每个主分片可以有一个或者多个副本,数据和主分片一样。
数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高了!
为了在高可用和成本间寻求平衡,我们可以这样做:
这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:
现在,每个分片都有1个备份,存储在3个节点:
本章节基于Docker安装。
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 |
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、修改每一台机器的配置文件
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
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
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 查看集群状态
健康状况结果解释: 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 查看集群
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"
#5602避免与之前的冲突
server.port: 5601
server.host: "0.0.0.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.requestTimeout: 99999
浏览器访问:http://192.168.200.151:5601/app/monitoring#/no-data?_g=()
//客户端对象
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();
}
在创建索引时,如果不指定分片配置,则默认主分片1,副本分片1。
在创建索引时,可以通过settings设置分片
分片配置
#分片配置
#"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分片标号)
2.itcast-3 挂掉
3.将挂掉节点的分片,自平衡到其他节点
4.itcast-3 恢复正常后,节点分片将自平衡回去(并不一定是原来的分片)
分片与自平衡
•当节点挂掉后,挂掉的节点分片会自平衡到其他节点中
注意:分片数量一旦确定好,不能修改。
索引分片推荐配置方案:
思考:比如有1000GB数据,应该有多少个分片?多少个节点
路由原理
文档存入对应的分片,ES计算分片编号的过程,称为路由。
Elasticsearch 是怎么知道一个文档应该存放到哪个分片中呢?
查询时,根据文档id查询文档, Elasticsearch 又该去哪个分片中查询数据呢?
查询id为5的文档:假如hash(5)=17 ,根据算法17%3=2
ElasticSearch 集群正常状态:
脑裂现象:
脑裂产生的原因:
网络原因:网络延迟
节点负载
JVM内存回收
避免脑裂:
网络原因:discovery.zen.ping.timeout
超时时间配置大一点。默认是3S
节点负载:角色分离策略
候选主节点配置为
数据节点配置为
JVM内存回收:修改 config/jvm.options 文件的 -Xms 和 -Xmx 为服务器的内存一半。