- RestHighLevelClient介绍
- 引入依赖
- ES的配置
- (1)、创建索引
- (2)、application.yml 配置文件
- (3)、实体对象
- (4)、java 连接配置类
- 索引操作
- 文档操作
- Bulk操作
- DSL高级查询操作
- 精确查询(term)
- 全文查询(match)
- 通配符查询(wildcard)
- 模糊查询(fuzzy)
- 排序查询(sort)
- 分页查询(page)
- 方式一: from + size
- 方式二: scroll
- 方式三: search_after
- 范围查询(range)
- 布尔查询(bool)
- queryString查询
- 查询结果过滤 (fetchSource)
- 高亮查询(highlight)
- 聚合查询
- 集群健康状况
- 索引信息
官方文档 : https://www.elastic.co/guide/en/elasticsearch/reference/8.3/index.html
RestHighLevelClient介绍JavaREST客户端有两种模式:
- Java Low Level REST Client:ES官方的低级客户端。低级别的客户端通过http与Elasticearch集群通信。
- Java High Level REST Client:ES官方的高级客户端。基于上面的低级客户端,也是通过HTTP与ES集群进行通信。它提供了更多的接口。
下面介绍下 SpringBoot 如何通过 elasticsearch-rest-high-level-client 工具操作ElasticSearch。
注意事项:客户端(Client) Jar包的版本尽量不要大于Elasticsearch本体的版本,否则可能出现客户端中使用的某些API在Elasticsearch中不支持。
这里需要说一下,能使用RestHighLevelClient尽量使用它,笔记该RestClient是官方支持的ES客户端,更新和维护都是比较好的。
为什么不推荐使用 Spring 家族封装的 spring-data-elasticsearch。主要原因是灵活性和更新速度,Spring 将 ElasticSearch 过度封装,让开发者很难跟 ES 的 DSL 查询语句进行关联。再者就是更新速度,ES 的更新速度是非常快,但是 spring-data-elasticsearch 更新速度比较缓慢。并且spring-data-elasticsearch在Elasticsearch6.x和7.x版本上的Java API差距很大,如果升级版本需要花点时间来了解。
引入依赖
org.elasticsearch.client
elasticsearch-rest-high-level-client
7.6.2
org.elasticsearch.client
elasticsearch-rest-client
7.6.2
org.elasticsearch
elasticsearch
7.6.2
ES的配置
(1)、创建索引
PUT /goods
{
"mappings": {
"properties": {
"brandName": {
"type": "keyword"
},
"categoryName": {
"type": "keyword"
},
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"id": {
"type": "keyword"
},
"price": {
"type": "double"
},
"saleNum": {
"type": "integer"
},
"status": {
"type": "integer"
},
"stock": {
"type": "integer"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}
(2)、application.yml 配置文件
elasticsearch:
host: 116.201.200.113
port: 9200
spring:
# 应用名称
application:
name: elasticsearch-spring-data
elasticsearch:
rest:
# 定位ES的位置
uris: http://116.201.200.113:9200
(3)、实体对象
@Data
@Accessors(chain = true) // 链式赋值(连续set方法)
@AllArgsConstructor // 全参构造
@NoArgsConstructor // 无参构造
public class Goods {
/**
* 商品编号
*/
private Long id;
/**
* 商品标题
*/
private String title;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 商品库存
*/
private Integer stock;
/**
* 商品销售数量
*/
private Integer saleNum;
/**
* 商品分类
*/
private String categoryName;
/**
* 商品品牌
*/
private String brandName;
/**
* 上下架状态
*/
private Integer status;
/**
* 商品创建时间
*/
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
(4)、java 连接配置类
/**
* ES的配置类
* ElasticSearchConfig
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {
private String host;
private Integer port;
/**
* 如果@Bean没有指定bean的名称,那么这个bean的名称就是方法名
*/
@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(
RestClient.builder(
new HttpHost(host, port, "http")
)
);
}
}
索引操作
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestIndex {
@Autowired
RestHighLevelClient restHighLevelClient;
/**
* 创建索引库和映射表结构
* 注意:索引一般不会怎么创建
*/
@Test
public void indexCreate() throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建索引
CreateIndexRequest indexRequest = new CreateIndexRequest("goods");
// 创建表 结构
String mapping = "{\n" +
" \"properties\": {\n" +
" \"brandName\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"categoryName\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"createTime\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss\"\n" +
" },\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"price\": {\n" +
" \"type\": \"double\"\n" +
" },\n" +
" \"saleNum\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"status\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"stock\": {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"title\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
" }";
// 把映射信息添加到request请求里面
// 第一个参数:表示数据源
// 第二个参数:表示请求的数据类型
indexRequest.mapping(mapping, XContentType.JSON);
// 请求服务器
CreateIndexResponse response = indicesClient.create(indexRequest, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
/**
* 获取表结构
* GET goods/_mapping
*/
@Test
public void getMapping() throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建get请求
GetIndexRequest request = new GetIndexRequest("goods");
// 发送get请求
GetIndexResponse response = indicesClient.get(request, RequestOptions.DEFAULT);
// 获取表结构
Map mappings = response.getMappings();
for (String key : mappings.keySet()) {
System.out.println("key--" + mappings.get(key).getSourceAsMap());
}
}
/**
* 删除索引库
*/
@Test
public void indexDelete() throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建delete请求方式
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("goods");
// 发送delete请求
AcknowledgedResponse response = indicesClient.delete(deleteIndexRequest, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
/**
* 判断索引库是否存在
*/
@Test
public void indexExists() throws Exception {
IndicesClient indicesClient = restHighLevelClient.indices();
// 创建get请求
GetIndexRequest request = new GetIndexRequest("goods");
// 判断索引库是否存在
boolean result = indicesClient.exists(request, RequestOptions.DEFAULT);
System.out.println(result);
}
}
文档操作
/**
* 增加文档信息
*/
@Test
public void addDocument() throws IOException {
// 创建商品信息
Goods goods = new Goods();
goods.setId(1L);
goods.setTitle("Apple iPhone 13 Pro (A2639) 256GB 远峰蓝色 支持移动联通电信5G 双卡双待手机");
goods.setPrice(new BigDecimal("8799.00"));
goods.setStock(1000);
goods.setSaleNum(599);
goods.setCategoryName("手机");
goods.setBrandName("Apple");
goods.setStatus(0);
goods.setCreateTime(new Date());
// 将对象转为json
String data = JSON.toJSONString(goods);
// 创建索引请求对象
IndexRequest indexRequest = new IndexRequest("goods").id(goods.getId() + "").source(data, XContentType.JSON);
// 执行增加文档
IndexResponse response = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
log.info("创建状态:{}", response.status());
/*
// 也可以通过Map来接收文档数据
//文档内容
//准备json数据
Map jsonMap = new HashMap();
jsonMap.put("name", "spring cloud实战");
jsonMap.put("description", "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。");
jsonMap.put("studymodel", "201001");
SimpleDateFormat dateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
jsonMap.put("timestamp", dateFormat.format(new Date()));
jsonMap.put("price", 5.6f);
//创建索引创建对象
IndexRequest indexRequest = new IndexRequest("xc_course","doc");
//文档内容
indexRequest.source(jsonMap);
//通过client进行http的请求
IndexResponse indexResponse = client.index(indexRequest);
DocWriteResponse.Result result = indexResponse.getResult();
System.out.println(result);
*/
}
/**
* 获取文档信息
*/
@Test
public void getDocument() throws IOException {
// 创建获取请求对象
GetRequest getRequest = new GetRequest("goods", "1");
GetResponse response = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
// Map sourceAsMap = response.getSourceAsMap();
System.out.println(response.getSourceAsString());
}
/**
* 更新文档信息
*/
@Test
public void updateDocument() throws IOException {
// 设置商品更新信息
Goods goods = new Goods();
goods.setTitle("Apple iPhone 13 Pro Max (A2644) 256GB 远峰蓝色 支持移动联通电信5G 双卡双待手机");
goods.setPrice(new BigDecimal("9999"));
// 将对象转为json
String data = JSON.toJSONString(goods);
// 创建索引请求对象
UpdateRequest updateRequest = new UpdateRequest("goods", "1");
// 设置更新文档内容
updateRequest.doc(data, XContentType.JSON);
// 执行更新文档
UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
log.info("创建状态:{}", response.status());
}
/**
* 删除文档信息
*/
@Test
public void deleteDocument() throws IOException {
// 创建删除请求对象
DeleteRequest deleteRequest = new DeleteRequest("goods", "1");
// 执行删除文档
DeleteResponse response = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
log.info("删除状态:{}", response.status());
}
Bulk操作
从MySQL数据库中查询一些数据, 批量导入到ES中
/**
* 批量导入测试数据
*/
@Test
public void importData() throws IOException {
//1.查询所有数据,mysql
List goodsList = goodsMapper.findAll();
//2.bulk导入
BulkRequest bulkRequest = new BulkRequest();
//2.1 循环goodsList,创建IndexRequest添加数据
for (Goods goods : goodsList) {
//将goods对象转换为json字符串
String data = JSON.toJSONString(goods);//map --> {}
IndexRequest indexRequest = new IndexRequest("goods");
indexRequest.id(goods.getId() + "").source(data, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse response = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(response.status());
}
DSL高级查询操作
精确查询(term)
-
term查询:不会分析查询条件,只有当词条和查询字符串完全匹配时才匹配,也就是精确查找,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型)
-
terms查询:terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去 做匹配:
/**
* 精确查询(termQuery)
*/
@Test
public void termQuery() {
try {
// 构建查询条件(注意:termQuery 支持多种格式查询,如 boolean、int、double、string 等,这里使用的是 string 的查询)
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 表示检索title字段值为华为的文档
searchSourceBuilder.query(QueryBuilders.termQuery("title", "华为"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info("=======" + userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
/**
* terms:多个查询内容在一个字段中进行查询
*/
@Test
public void termsQuery() {
try {
// 构建查询条件(注意:termsQuery 支持多种格式查询,如 boolean、int、double、string 等,这里使用的是 string 的查询)
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 表示title字段值为华为,OPPO,TCL的文档都能检索出来
searchSourceBuilder.query(QueryBuilders.termsQuery("title", "华为", "OPPO", "TCL"));
// 展示100条,默认只展示10条记录
searchSourceBuilder.size(100);
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
全文查询(match)
全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集。
term和match的区别是:match是经过analyer的(进行分词),也就是说,文档首先被分析器给处理了。根据不同的分析器,分析的结果也稍显不同,然后再根据分词结果进行匹配。term则不经过分词,它是直接去倒排索引中查找了精确的值了。
match 查询语法汇总:
- match_all:查询全部。
- match:返回所有匹配的分词。
- match_phrase:短语查询,在match的基础上进一步查询词组,可以指定slop分词间隔。
- match_phrase_prefix:前缀查询,根据短语中最后一个词组做前缀匹配,可以应用于搜索提示,但注意和max_expanions搭配。其实默认是50…
- multi_match:多字段查询,使用相当的灵活,可以完成match_phrase和match_phrase_prefix的工作。
/**
* 匹配查询符合条件的所有数据,并设置分页
*/
@Test
public void matchAllQuery() {
try {
// 构建查询条件 (查询全部)
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
// 创建查询源构造器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchAllQueryBuilder);
// 设置分页
searchSourceBuilder.from(0);
searchSourceBuilder.size(3);
// 设置排序
searchSourceBuilder.sort("price", SortOrder.ASC);
//设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段;查询的文档只包含哪些指定的字段
searchSourceBuilder.fetchSource(new String[]{"title","categoryName"},new String[]{});
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
/**
* 匹配查询数据
*/
@Test
public void matchQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 查询title为华为的文档
searchSourceBuilder.query(QueryBuilders.matchQuery("title", "华为"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
/**
* 词语匹配查询
*/
@Test
public void matchPhraseQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchPhraseQuery("title", "三星"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
/**
* 内容在多字段中进行查询
*/
@Test
public void matchMultiQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// "手机"在title字段,categoryName字段中存在的文档
searchSourceBuilder.query(QueryBuilders.multiMatchQuery("手机", "title", "categoryName")
.minimumShouldMatch("50%") // 设置匹配度
.field("title",10)); // 指定指定设置匹配度
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
通配符查询(wildcard)
wildcard查询:会对查询条件进行分词。还可以使用通配符 ?(任意单个字符) 和 * (0个或多个字符)
/**
* 查询所有以 “三” 结尾的商品信息
*
* *:表示多个字符(0个或多个字符)
* ?:表示单个字符
*/
@Test
public void wildcardQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.wildcardQuery("title", "*三"));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
模糊查询(fuzzy)
fuzzy 模糊查询 最大模糊错误 必须在0-2之间 搜索关键词长度为 2 不允许存在模糊 0 搜索关键词长度为3-5 允许一次模糊 0 1 搜索关键词长度大于5 允许最大2模糊
/**
* 模糊查询所有以 “三” 结尾的商品信息
*/
@Test
public void fuzzyQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.fuzzyQuery("title", "三").fuzziness(Fuzziness.AUTO));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
排序查询(sort)
注意:需要分词的字段不可以直接排序,比如:text类型,如果想要对这类字段进行排序,需要特别设置:对字段索引两次,一次索引分词(用于搜索)一次索引不分词(用于排序),es默认生成的text类型字段就是通过这样的方法实现可排序的。
/**
* 排序查询(sort) 代码同matchAllQuery
* 匹配查询符合条件的所有数据,并设置分页
*/
@Test
public void matchAllQuery() {
try {
// 构建查询条件
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
// 创建查询源构造器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchAllQueryBuilder);
// 设置分页
searchSourceBuilder.from(0);
searchSourceBuilder.size(3);
// 设置排序
searchSourceBuilder.sort("price", SortOrder.ASC);
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
分页查询(page)
Elasticsearchde 的分页查询和 SQL 使用 LIMIT 关键字返回只有一页的结果一样,Elasticsearch 接受 from 和 size 参数:
- size: 结果数,默认10
- from: 跳过开始的结果数,即从哪一行开始获取数据,默认0
这种方式分页查询如果需要深度分页,那么这种方式性能不太好。
深度分页: 建议使用 scroll滚动查询 和 search_after滚动查询
方式一: from + size实现原理
- es是通过协调节点从每个shard(分片)中都获取from+size条数据返回给协调节点后,由协调节点汇总排序,然后查找[from , frome+size] 之间的数据,并返回给前端。
存在的问题
- 该搜索请求占用的堆内存和时间与from+size大小成正比,值越大,性能越差、效率越低,一般用于浅分页中。 查询的值超过默认(10000)时会报错 :
from+size > max_result_window时
,会报错。
Kibana中测试示例
GET /book/_search
{
"from" : 100 , "size" : 10,
"query" : {
"match" : { "title" : "区块链" }
}
}
Java代码示例
// 构建搜索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
...
searchSourceBuilder.from(100).size(10);
// 构建请求
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
详细代码
/**
* 分页查询(page) 代码同matchAllQuery
* 匹配查询符合条件的所有数据,并设置分页
*/
@Test
public void matchAllQuery() {
try {
// 构建查询条件
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
// 创建查询源构造器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchAllQueryBuilder);
// 设置分页
searchSourceBuilder.from(0);
searchSourceBuilder.size(3);
// 设置排序
searchSourceBuilder.sort("price", SortOrder.ASC);
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
方式二: scroll
实现原理
- scroll API可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与在传统数据库上使用光标的方式大致相同。
- 第一次查询时,会生产当时查询的快照,后续的查询只要携带上次返回的scroll_id即可(类似数据库游标) 。
特点
- 可以深查询(甚至全部数据),性能、效率优于from+size方式。
- 快照查询,在scroll查询期间 ,外部对索引内的数据进行增删改查等操作不会影响到这个快照的结果。
Kibana中测试示例
第一次查询 ,并设置上下文scroll_id存活时间为1分钟。
POST book/_search?scroll=1m
{
"size": 10,
"query": {
"match" : {
"name": "明朝的那些事"
}
}
}
响应
{
"_scroll_id":"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAD1K-0WdzM3OF9WRVFUcXk3bFNDcjZKZ1pZdw==",
"took":22,
"timed_out":false,
"_shards":{
"total":1,
"successful":1,
"skipped":0,
"failed":0
},
"hits":{
"total":1302017,
"max_score":2.5815613,
"hits":Array[10]
}
}
后续查询,使用前一个scroll_id即可:
POST _search/scroll
{
"scroll":"1m",
"scroll_id":"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAD1K-0WdzM3OF9WRVFUcXk3bFNDcjZKZ1pZdw=="
}
Java代码示例
首次调用
// 构建搜索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
...
searchSourceBuilder.from(100).size(10);
// 构建请求
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.types("title");
// ===》 关键是多加了这句话
searchRequest.scroll(new Scroll(TimeValue.timeValueMinutes(1)));
searchRequest.source(searchSourceBuilder);
return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
后续调用
// 传入上次返回的scroll_id
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueMinutes(1));
SearchResponse response = restHighLevelClient.getRestClient().scroll(scrollRequest, RequestOptions.DEFAULT);
return response;
详细代码
/**
* 根据查询条件滚动查询
* 可以用来解决深度分页查询问题
*/
@Test
public void scrollQuery() {
// 假设用户想获取第70页数据,其中每页10条
int pageNo = 70;
int pageSize = 10;
// 定义请求对象
SearchRequest searchRequest = new SearchRequest("goods");
// 构建查询条件
SearchSourceBuilder builder = new SearchSourceBuilder();
searchRequest.source(builder.query(QueryBuilders.matchAllQuery()).size(pageSize));
String scrollId = null;
// 3、发送请求到ES
SearchResponse scrollResponse = null;
// 设置游标id存活时间
Scroll scroll = new Scroll(TimeValue.timeValueMinutes(2));
// 记录所有游标id
List scrollIds = new ArrayList();
for (int i = 0; i 0) {
SearchHits hits = searchResponse.getHits();
log.info(hits.getTotalHits().value + "");
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
/**
* 查询距离现在 10 年间的商品信息
* [年(y)、月(M)、星期(w)、天(d)、小时(h)、分钟(m)、秒(s)]
* 例如:
* now-1h 查询一小时内范围
* now-1d 查询一天内时间范围
* now-1y 查询最近一年内的时间范围
*/
@Test
public void dateRangeQuery() {
try {
// 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// includeLower(是否包含下边界)、includeUpper(是否包含上边界)
searchSourceBuilder.query(QueryBuilders.rangeQuery("createTime")
.gte("now-10y").includeLower(true).includeUpper(true));
// 创建查询请求对象,将查询对象配置到其中
SearchRequest searchRequest = new SearchRequest("goods");
searchRequest.source(searchSourceBuilder);
// 执行查询,然后处理响应结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 根据状态和数据条数验证是否返回了数据
if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods userInfo = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(userInfo.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
布尔查询(bool)
bool 查询可以用来合并多个条件查询结果的布尔逻辑,它包含一下操作符:
- must:多个查询条件必须完全匹配,相当于关系型数据库中的 and。
- should:至少有一个查询条件匹配,相当于关系型数据库中的 or。
- must_not: 多个查询条件的相反匹配,相当于关系型数据库中的 not。
- filter:过滤满足条件的数据。
- range:条件筛选范围。
- gt:大于,相当于关系型数据库中的 >。
- gte:大于等于,相当于关系型数据库中的 >=。
- lt:小于,相当于关系型数据库中的 0) {
SearchHits hits = searchResponse.getHits();
for (SearchHit hit : hits) {
// 将 JSON 转换成对象
Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class);
// 输出查询信息
log.info(goods.toString());
}
}
} catch (IOException e) {
log.error("", e);
}
}
- Operator.AND : 要检索的内容, 进行分词后, 分词的内容, 要在文档中全部存在, 才能检索出来
- Operator.OR : 要检索的内容, 进行分词后, 分词的内容, 在文档中存在一个就能检索出来
我们在查询数据的时候,返回的结果中,所有字段都给我们返回了,但是有时候我们并不需要那么多,所以可以对结果进行过滤处理。
高亮查询(highlight)/** * 过滤source获取部分字段内容 * 案例:只获取 title、categoryName和price的数据 */ @Test public void sourceFilter() { try { //查询条件(词条查询:对应ES query里的match) BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("title", "金立")) .must(QueryBuilders.matchQuery("categoryName", "手机")) .filter(QueryBuilders.rangeQuery("price").gt(1000).lt(2000)); // 构建查询源构建器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(boolQueryBuilder); // 如果查询的属性很少,那就使用includes,而excludes设置为空数组 // 如果排序的属性很少,那就使用excludes,而includes设置为空数组 String[] includes = {"title", "categoryName", "price"}; String[] excludes = {}; searchSourceBuilder.fetchSource(includes, excludes); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info(goods.toString()); } } } catch (IOException e) { log.error("", e); } }
聚合查询/** * 高亮查询 * 案例:把标题中为 三星手机 的词语高亮显示 */ @Test public void highlightBuilder() { try { //查询条件(词条查询:对应ES query里的match) MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "三星手机"); //设置高亮三要素 field: 你的高亮字段 // preTags :前缀 // postTags:后缀 HighlightBuilder highlightBuilder = new HighlightBuilder().field("title").preTags("").postTags(""); // 构建查询源构建器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchQueryBuilder); searchSourceBuilder.highlighter(highlightBuilder); searchSourceBuilder.size(100); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 获取高亮的数据 HighlightField highlightField = hit.getHighlightFields().get("title"); System.out.println("高亮名称:" + highlightField.getFragments()[0].string()); // 替换掉原来的数据 Text[] fragments = highlightField.getFragments(); if (fragments != null && fragments.length > 0) { StringBuilder title = new StringBuilder(); for (Text fragment : fragments) { //System.out.println(fragment); title.append(fragment); } goods.setTitle(title.toString()); } // 输出查询信息 log.info(goods.toString()); } } } catch (IOException e) { log.error("", e); } }
- 聚合查询对比DB的讲解
- DSL聚合
我们平时在使用Elasticsearch时,更多会用到聚合操作,它类似SQL中的group by操作。ES的聚合查询一定是先查出结果,然后对结果使用聚合函数做处理,常用的操作有:avg:求平均、max:最大值、min:最小值、sum:求和等。
在ES中聚合分为指标聚合和分桶聚合:
- Metric 指标聚合:指标聚合对一个数据集求最大、最小、和、平均值等
- Bucket 分桶聚合:除了有上面的聚合函数外,还可以对查询出的数据进行分组group by,再在组上进行游标聚合。
Metric 指标聚合分析
/** * 聚合查询 * Metric 指标聚合分析 * 案例:分别获取最贵的商品和获取最便宜的商品 */ @Test public void metricQuery() { try { // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 获取最贵的商品 AggregationBuilder maxPrice = AggregationBuilders.max("maxPrice").field("price"); searchSourceBuilder.aggregation(maxPrice); // 获取最便宜的商品 AggregationBuilder minPrice = AggregationBuilders.min("minPrice").field("price"); searchSourceBuilder.aggregation(minPrice); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedMax max = aggregations.get("maxPrice"); log.info("最贵的价格:" + max.getValue()); ParsedMin min = aggregations.get("minPrice"); log.info("最便宜的价格:" + min.getValue()); } catch (IOException e) { log.error("", e); } } /** * 聚合查询 * Bucket 分桶聚合分析 * 案例:根据品牌进行聚合查询 */ @Test public void bucketQuery() { try { // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 根据商品分类进行分组查询 TermsAggregationBuilder aggBrandName = AggregationBuilders.terms("brandNameName").field("brandName"); searchSourceBuilder.aggregation(aggBrandName); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms aggBrandName1 = aggregations.get("brandNameName"); for (Terms.Bucket bucket : aggBrandName1.getBuckets()) { // 分组名 ==== 数量 System.out.println(bucket.getKeyAsString() + "====" + bucket.getDocCount()); } } catch (IOException e) { log.error("", e); } }
Bucket 分桶聚合分析
/** * 聚合查询 * Bucket 分桶聚合分析 * 案例:根据品牌进行聚合查询 */ @Test public void bucketQuery() { try { // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 根据商品分类进行分组查询 TermsAggregationBuilder aggBrandName = AggregationBuilders.terms("brandNameName").field("brandName"); searchSourceBuilder.aggregation(aggBrandName); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms aggBrandName1 = aggregations.get("brandNameName"); for (Terms.Bucket bucket : aggBrandName1.getBuckets()) { System.out.println(bucket.getKeyAsString() + "====" + bucket.getDocCount()); } } catch (IOException e) { log.error("", e); } } /** * 子聚合聚合查询 * Bucket 分桶聚合分析 * 案例:根据商品分类进行分组查询,并且获取分类商品中的平均价格 */ @Test public void subBucketQuery() { try { // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 根据商品分类进行分组查询,并且获取分类商品中的平均价格 TermsAggregationBuilder subAggregation = AggregationBuilders.terms("brandNameName").field("brandName") .subAggregation(AggregationBuilders.avg("avgPrice").field("price")); searchSourceBuilder.aggregation(subAggregation); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms aggBrandName1 = aggregations.get("brandNameName"); for (Terms.Bucket bucket : aggBrandName1.getBuckets()) { // 获取聚合后的品牌的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象 ParsedAvg avgPrice = bucket.getAggregations().get("avgPrice"); // 分组名 ==== 平均价格 System.out.println(bucket.getKeyAsString() + "====" + avgPrice.getValueAsString()); } } catch (IOException e) { log.error("", e); } }
综合聚合查询
/** * 综合聚合查询 * 根据商品分类聚合,获取每个商品类的平均价格,并且在商品分类聚合之上子聚合每个品牌的平均价格 */ @Test public void subSubAgg() throws IOException { // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 注意这里聚合写的位置不要写错,很容易搞混,错一个括号就不对了 TermsAggregationBuilder subAggregation = AggregationBuilders.terms("categoryNameAgg").field("categoryName") .subAggregation(AggregationBuilders.avg("categoryNameAvgPrice").field("price")) .subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName") .subAggregation(AggregationBuilders.avg("brandNameAvgPrice").field("price"))); searchSourceBuilder.aggregation(subAggregation); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); //获取总记录数 System.out.println("totalHits = " + searchResponse.getHits().getTotalHits().value); // 获取聚合信息 Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms categoryNameAgg = aggregations.get("categoryNameAgg"); //获取值返回 for (Terms.Bucket bucket : categoryNameAgg.getBuckets()) { // 获取聚合后的分类名称 String categoryName = bucket.getKeyAsString(); // 获取聚合命中的文档数量 long docCount = bucket.getDocCount(); // 获取聚合后的分类的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象 ParsedAvg avgPrice = bucket.getAggregations().get("categoryNameAvgPrice"); System.out.println(categoryName + "======平均价:" + avgPrice.getValue() + "======数量:" + docCount); ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandNameAgg"); for (Terms.Bucket brandeNameAggBucket : brandNameAgg.getBuckets()) { // 获取聚合后的品牌名称 String brandName = brandeNameAggBucket.getKeyAsString(); // 获取聚合后的品牌的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象 ParsedAvg brandNameAvgPrice = brandeNameAggBucket.getAggregations().get("brandNameAvgPrice"); System.out.println(" " + brandName + "======" + brandNameAvgPrice.getValue()); } } }
集群健康状况
索引信息@Test public void clusterHealthStatus() throws IOException { ClusterHealthRequest request = new ClusterHealthRequest(); ClusterHealthResponse response = restHighLevelClient.cluster().health(request, RequestOptions.DEFAULT); ClusterHealthStatus status = response.getStatus(); System.out.println("集群名称:" + response.getClusterName()); System.out.println("集群健康状态:" + status.name()); }
@Test public void printIndexInfo() throws IOException { Response response = client.getLowLevelClient().performRequest(new Request("GET", "/_cat/indices")); HttpEntity entity = response.getEntity(); String responseStr = EntityUtils.toString(entity, StandardCharsets.UTF_8); String[] indexInfoArr = responseStr.split("\n"); for (String indexInfo : indexInfoArr) { String[] infoArr = indexInfo.split("\\s+"); String status = infoArr[0]; String open = infoArr[1]; String name = infoArr[2]; String id = infoArr[3]; String mainShardNum = infoArr[4]; String viceShardNum = infoArr[5]; String docNum = infoArr[6]; String deletedDocNum = infoArr[7]; String allShardSize = infoArr[8]; String mainShardSize = infoArr[9]; System.out.println("》》》》》》》》索引信息》》》》》》》》"); System.out.println("名称:" + name); System.out.println("id:" + id); System.out.println("状态:" + status); System.out.println("是否开放:" + open); System.out.println("主分片数量:" + mainShardNum); System.out.println("副本分片数量:" + viceShardNum); System.out.println("Lucene文档数量:" + docNum); System.out.println("被删除文档数量:" + deletedDocNum); System.out.println("所有分片大小:" + allShardSize); System.out.println("主分片大小:" + mainShardSize); } }
- range:条件筛选范围。