一、Elasticsearch 是什么?
Elasticsearch(简称 ES) 是基于 Lucene 的分布式全文搜索引擎,支持近实时搜索、海量数据存储、高可用集群、RESTful API,广泛用于日志分析、商品搜索、数据可视化等场景。
在搜索时为什么我们要用 Elasticsearch,而不是 MySQL 模糊匹配?
在实际业务中,商品搜索、日志检索、全文搜索 这些场景,MySQL 根本扛不住。
1. MySQL 的 like '%关键词%' 为什么慢?
- 无法走索引,必须全表扫描
- 数据量越大,速度越慢
- 不支持分词,不支持相关性打分
- 不支持高亮、同义词、拼音搜索
简单说:数据量一大,like 查询直接拖垮数据库。
2. Elasticsearch 为什么快?------ 倒排索引原理
ES 快的核心原因只有一个:它用了 倒排索引(Inverted Index)。
我用最简单的方式讲清楚:
正常数据库(正排索引)
文档1 → 我喜欢华为手机
文档2 → 华为手机信号好
文档3 → 苹果手机也不错
倒排索引(关键词 → 文档)
华为 → 文档1、文档2
手机 → 文档1、文档2、文档3
苹果 → 文档3 喜欢 → 文档1
当你搜索:"华为"
ES 不需要扫全表,直接查倒排索引:华为 → 文档 1、文档 2 直接返回,毫秒级响应。
3. 倒排索引三大优势
- 检索极快:不需要全表扫描
- 支持分词:把一句话切成词语
- 相关性打分:最匹配的排前面
MySQL 适合存数据,Elasticsearch 适合搜数据。
MySQL 用 B+ 树,适合精确查询。
Elasticsearch 用倒排索引,适合全文检索。
Elasticsearch核心概念
- 索引(Index):对应 MySQL 的数据库
- 文档(Document):对应 MySQL 的行数据
- 映射(Mapping):对应 MySQL 的表结构
- 分片 / 副本:保证分布式存储与高可用
文档数据会被序列化为json格式后存储在elasticsearch
我们统一的把mysql与elasticsearch的概念做一下对比

Kibana是elastic公司提供的用于操作Elasticsearch的可视化控制台。它的功能非常强大,包括:
-
对Elasticsearch数据的搜索、展示
-
对Elasticsearch数据的统计、聚合,并形成图形化报表、图形
-
对Elasticsearch的集群状态监控
-
它还提供了一个开发控制台(DevTools),在其中对Elasticsearch的Restful的API接口提供了语法提示
elasticsearch安装
通过下面的Docker命令即可安装单机版本的elasticsearch:
XML
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hm-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
访问9200端口,即可看到响应的Elasticsearch服务的基本信息:

安装kibana
XML
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601 \
kibana:7.12.1
访问5601端口,即可看到控制台页面:


ik分词器
Elasticsearch的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK分词器就是这样一个中文分词算法。
安装:
XML
docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
IK分词器包含两种模式:
-
ik_smart:智能语义切分 -
ik_max_word:最细粒度切分
词典:随着互联网的发展出现了很多新的词语,在原有的词汇列表中并不存在,IK分词器无法对这些词汇分词,要想正确分词,IK分词器的词库也需要不断的更新,IK分词器提供了扩展词汇的功能
在分词器的配置目录下

XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
<entry key="ext_dict">ext.dic</entry>
</properties>
改目录下目录新建一个 ext.dic,然后在里面添加新的词汇,重启后ik分词器就能正确识别ext.dic的这些单词
CRUD操作:
由于Elasticsearch采用的是Restful风格的API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用JSON风格。
我们直接基于Kibana的DevTools来编写请求做测试,由于有语法提示,会非常方便。
.创建索引库和映射
基本语法:
-
请求方式:
PUT -
请求路径:
/索引库名,可以自定义 -
请求参数:
mapping映射
javascript
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
查询索引库
基本语法:
-
请求方式:GET
-
请求路径:/索引库名
-
请求参数:无
比如:GET /item
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。
语法:
javascript
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
删除索引库
语法:
-
请求方式:DELETE
-
请求路径:/索引库名
-
请求参数:无
文档操作
有了索引库,接下来就可以向索引库中添加数据了。
4.1 新增文档(指定 ID)
javascript
PUT /product/_doc/1
{
"id": 1,
"name": "华为Mate70",
"price": 5999,
"category": "手机"
}
新增文档(自动 ID)
java
POST /product/_doc
{
"id": 2,
"name": "小米14",
"price": 4299,
"category": "手机"
}
4.3 查询文档(根据 ID)
java
GET /product/_doc/1
全量更新文档(相当于覆盖原文档)
java
PUT /product/_doc/1
{
"id": 1,
"name": "华为Mate70 Pro",
"price": 6999,
"category": "手机"
}
局部更新文档(推荐)
java
POST /product/_update/1
{
"doc": {
"price": 6799
}
}
删除文档
java
DELETE /product/_doc/1
批处理
java
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
-
index代表新增操作-
_index:指定索引库名 -
_id指定要操作的文档id -
{ "field1" : "value1" }:则是要新增的文档内容
-
-
delete代表删除操作-
_index:指定索引库名 -
_id指定要操作的文档id
-
-
update代表更新操作-
_index:指定索引库名 -
_id指定要操作的文档id -
{ "doc" : {"field2" : "value2"} }:要更新的文档字段
-
总结
-
创建文档:
POST /{索引库名}/_doc/文档id { json文档 } -
查询文档:
GET /{索引库名}/_doc/文档id -
删除文档:
DELETE /{索引库名}/_doc/文档id -
修改文档:
-
全量修改:
PUT /{索引库名}/_doc/文档id { json文档 } -
局部修改:
POST /{索引库名}/``_update``/文档id { "doc": ``{字段}``}
-
在java中访问客户端
引入依赖
XML
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
因为SpringBoot默认的ES版本是7.17.10,所以我们需要覆盖默认的ES版本
XML
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
初始化RestHighLevelClient:
java
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
java
package com.hmall.item.es;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class IndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
@Test
void testConnect() {
System.out.println(client);
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
RestClient操作索引库

代码分为三步:
-
1)创建Request对象。
- 因为是创建索引库的操作,因此Request是
CreateIndexRequest。
- 因为是创建索引库的操作,因此Request是
-
2)添加请求参数
- 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量
MAPPING_TEMPLATE,让代码看起来更加优雅。
- 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量
-
3)发送请求
client.``indices``()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等
RestClient操作文档
新增文档
java
// 1.准备Request对象
IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
// 2.准备Json文档
request.source(doc, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
-
1)创建Request对象,这里是
IndexRequest,因为添加文档就是创建倒排索引的过程 -
2)准备请求参数,本例中就是Json文档
-
3)发送请求
查询文档

删除文档
java
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request,两个参数,第一个是索引库名,第二个是文档id
DeleteRequest request = new DeleteRequest("item", "100002644680");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
修改文档
修改我们讲过两种方式:
-
全量修改:本质是先根据id删除,再新增
-
局部修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
-
如果新增时,ID已经存在,则修改
-
如果新增时,ID不存在,则新增
这里不再赘述,我们主要关注局部修改的API即可。

.批量导入文档
java
@Test
void testBulk() throws IOException {
// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备请求参数
request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));
request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
总结
-
初始化
RestHighLevelClient -
创建XxxRequest。
- XXX是
Index、Get、Update、Delete、Bulk
- XXX是
-
准备参数(
Index、Update、Bulk时需要) -
发送请求。
- 调用
RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 调用
-
解析结果(
Get时需要)
DSL查询
在 Elasticsearch 中,所有复杂查询都是通过 DSL(Domain Specific Language) 来实现的 。简单说:DSL = 用 JSON 写查询语句
所有查询都分为两类:
(1)查询上下文(Query Context)
会**计算相关性分数(_score)**用于:全文搜索、模糊匹配例:match、match_phrase、multi_match
(2)过滤上下文(Filter Context)
不计算分数,只判断 是 / 否速度极快,会自动缓存用于:精确匹配、范围、布尔过滤例:term、range、bool filter
最常用 DSL 查询
match 全文检索
对字段分词后匹配
javascript
{
"query": {
"match": {
"name": "拉杆箱"
}
}
}
查询结果一般格式

term 精确匹配(不分词)
用于 keyword、数字、布尔
javascript
{
"query": {
"term": {
"category": "手机"
}
}
}
range 范围查询
javascript
{
"query": {
"range": {
"price": {
"gte": 3000,
"lte": 8000
}
}
}
}
搜索的关键字高亮
javascript
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
bool :组合查询包含四个子句:
- must:必须满足(算分)
- should:满足任意一个
- must_not:必须不满足
- filter:必须满足(过滤,不计分)
示例
javascript
{
"query": {
"bool": {
"must": [
{ "match": { "name": "手机" } }
],
"filter": [
{ "range": { "price": { "gte": 2000 } } }
]
}
}
}
分页、排序、指定返回字段
javascript
{
"from": 0,
"size": 10,
"_source": ["name", "price"],
"sort": [
{ "price": { "order": "desc" } }
],
"query": {
"match_all": {}
}
}
总结
查询的DSL是一个大的JSON对象,包含下列属性:
-
query:查询条件 -
from和size:分页条件 -
sort:排序条件 -
highlight:高亮条件
Java中RestClient查询

-
第一步,创建
SearchRequest对象,指定索引库名 -
第二步,利用
request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
-
第三步,利用
client.search()发送请求,得到响应
request.source(),它构建的就是DSL中的完整JSON参数。其中包含了query、sort、from、size、highlight等所有功能

QueryBuilders,其中包含了叶子查询 、复合查询

解析响应结果

elasticsearch返回的结果是一个JSON字符串,结构包含:
-
hits:命中的结果-
total:总条数,其中的value是具体的总条数值 -
max_score:所有结果中得分最高的文档的相关性算分 -
hits:搜索结果的文档数组,其中的每个文档都是一个json对象_source:文档中的原始数据,也是json对象
-
因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:
-
SearchHits:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果-
SearchHits``#``getTotalHits().value:获取总条数信息 -
SearchHits#getHits():获取SearchHit数组,也就是文档数组SearchHit#getSourceAsString():获取文档结果中的_source,也就是原始的json文档数据
-
叶子查询
所有的查询条件都是由QueryBuilders来构建的,叶子查询也不例外。因此整套代码中变化的部分仅仅是query条件构造的方式,其它不动。
match查询:
java
@Test
void testMatch() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
multi_match查询
java
@Test
void testMultiMatch() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
还有range查询:
java
@Test
void testRange() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
term查询:
java
@Test
void testTerm() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.termQuery("brand", "华为"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
复合查询
复合查询也是由QueryBuilders来构建,我们以bool查询为例,DSL和JavaAPI的对比如图:

java
@Test
void testBool() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.准备bool查询
BoolQueryBuilder bool = QueryBuilders.boolQuery();
// 2.2.关键字搜索
bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.3.品牌过滤
bool.filter(QueryBuilders.termQuery("brand", "德亚"));
// 2.4.价格过滤
bool.filter(QueryBuilders.rangeQuery("price").lte(30000));
request.source().query(bool);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
排序和分页

java
@Test
void testPageAndSort() throws IOException {
int pageNo = 1, pageSize = 5;
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.搜索条件参数
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2.排序参数
request.source().sort("price", SortOrder.ASC);
// 2.3.分页参数
request.source().from((pageNo - 1) * pageSize).size(pageSize);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
高亮
java
@Test
void testHighlight() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.query条件
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2.高亮条件
request.source().highlighter(
SearchSourceBuilder.highlight()
.field("name")
.preTags("<em>")
.postTags("</em>")
);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
private void handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
// 1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
// 2.遍历结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// 3.得到_source,也就是原始json文档
String source = hit.getSourceAsString();
// 4.反序列化
ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);
// 5.获取高亮结果
Map<String, HighlightField> hfs = hit.getHighlightFields();
if (CollUtils.isNotEmpty(hfs)) {
// 5.1.有高亮结果,获取name的高亮结果
HighlightField hf = hfs.get("name");
if (hf != null) {
// 5.2.获取第一个高亮结果片段,就是商品名称的高亮值
String hfName = hf.getFragments()[0].string();
item.setName(hfName);
}
}
System.out.println(item);
}
}
数据聚合
聚合 = 对数据进行统计、分组、求和、平均值、最大值、最小值、计数。
你可以把它理解为:MySQL 的 group by + sum + avg + count 超级加强版。
聚合的基本结构
javascript
{
"size": 0, // 不返回原始数据,只返回统计结果
"aggs": { // 聚合固定写 aggs
"自定义名称": {
"聚合类型": {
"field": "字段名"
}
}
}
}
聚合常见的有三类:
-
桶(
Bucket**)**聚合:用来对文档做分组-
TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组 -
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
-
-
度量(
Metric**)**聚合:用以计算一些值,比如:最大值、最小值、平均值等-
Avg:求平均值 -
Max:求最大值 -
Min:求最小值 -
Stats:同时求max、min、avg、sum等
-
-
管道(
pipeline**)**聚合:其它聚合的结果为基础做进一步运算
**注意:**参加聚合的字段必须是keyword、日期、数值、布尔类型
.Bucket聚合
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合。
javascript
GET /items/_search
{
"size": 0,
"aggs": {
"category_agg": {
"terms": {
"field": "category",
"size": 20
}
}
}
}
-
size:设置size为0,就是每页查0条,则结果中就不包含文档,只包含聚合 -
aggs:定义聚合-
category_agg:聚合名称,自定义,但不能重复-
terms:聚合的类型,按分类聚合,所以用term-
field:参与聚合的字段名称 -
size:希望返回的聚合结果的最大数量
-
-
-
查询的结果:

嵌套聚合
先按分类分组 → 再求每组的平均价格
javascript
GET /product/_search
{
"size": 0,
"aggs": {
"group_by_category": {
"terms": {
"field": "category"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
结果
手机 → 平均价格 5800
电脑 → 平均价格 7200
按价格区间统计(range 聚合)
电商最常用:价格分段统计
javascript
GET /product/_search
{
"size": 0,
"aggs": {
"price_range": {
"range": {
"field": "price",
"ranges": [
{ "to": 3000 },
{ "from": 3000, "to": 6000 },
{ "from": 6000 }
]
}
}
}
}
对搜索结果聚合。
例如,我想知道价格高于3000元的手机品牌有哪些
javascript
GET /items/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brand_agg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
求平均值(avg)
javascript
GET /product/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
求和(sum)
javascript
GET /product/_search
{
"size": 0,
"aggs": {
"total_price": {
"sum": {
"field": "price"
}
}
}
}
最大值、最小值(max /min)
javascript
GET /product/_search
{
"size": 0,
"aggs": {
"max_price": {
"max": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
}
}
}
例:
统计价格高于3000的手机品牌,形成一个个桶。对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。
javascript
GET /items/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brand_agg": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": {
"stats_meric": {
"stats": {
"field": "price"
}
}
}
}
}
}
stat聚合,可以同时获取min、max、avg的结果。
.RestClient实现聚合
可以看到在DSL中,aggs聚合条件与query条件是同一级别,都属于查询JSON参数。因此依然是利用request.source()方法来设置。
不过聚合条件的要利用AggregationBuilders这个工具类来构造。DSL与JavaAPI的语法对比如下:

聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下:

java
@Test
void testAgg() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.准备请求参数
BoolQueryBuilder bool = QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery("category", "手机"))
.filter(QueryBuilders.rangeQuery("price").gte(300000));
request.source().query(bool).size(0);
// 3.聚合参数
request.source().aggregation(
AggregationBuilders.terms("brand_agg").field("brand").size(5)
);
// 4.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 5.解析聚合结果
Aggregations aggregations = response.getAggregations();
// 5.1.获取品牌聚合
Terms brandTerms = aggregations.get("brand_agg");
// 5.2.获取聚合中的桶
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 5.3.遍历桶内数据
for (Terms.Bucket bucket : buckets) {
// 5.4.获取桶内key
String brand = bucket.getKeyAsString();
System.out.print("brand = " + brand);
long count = bucket.getDocCount();
System.out.println("; count = " + count);
}
}