1. Elasticsearch
1.1 安装ES
- 启动Docker:
service docker restart / systemctl restart docker
- 基于Docker创建网络
docker network create hm-net
- 向云服务器上传elasticsearch以及kibana的tar包,并使用
docker load -i xxx.tar
进行加载 - 使用如下命令启动es:
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
- 使用如下命令启动kibana:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601 \
kibana:7.12.1
- 使用
docker logs -f es / kibana
可查看运行日志:
- 验证在浏览器中访问:9200以及5601端口能观察到如下现象证明启动成功:
1.2 初识Elasticsearch
1.2.1 简介
官方网站:https://www.elastic.co/cn/elasticsearch
**Elasticsearch:**是一个高性能的搜索引擎,它是ELK技术栈的一部分:
- Logstash / Beats:用于数据收集
- Elasticsearch:用于数据的存储、搜索、计算
- Kibana:用于数据可视化(内置Devtools等高效工具)
**应用场景:**搜索引擎技术在日常生活中非常常见,例如:
- github等网站的高亮搜索关键词
- 百度/搜狗等搜索引擎
- 各大网站支持的模糊查询(关键字匹配)
- 打车软件地理位置搜索
1.2.2 倒排索引
ES的高性能得益于其底层实现的倒排索引!
倒排索引中有两个重要概念:
- 文档(Document):一条数据就是一个文档,例如商品记录、用户记录都是一个文档
- 词条(Item):对于一个文档中的内容或者用户搜索句使用某种算法,基于语义划分得到的一组词就是词条,例如我喜欢学习Java,就可以分成:“我”、“喜欢”、“学习”、"Java"这些词条
倒排索引构建流程:
假设此时正向索引为:
id | title | price |
---|---|---|
1 | 小米手机 | 4999 |
2 | 华为手机 | 3999 |
3 | 华为手表 | 2999 |
4 | 小米汽车 | 199999 |
此时每一条数据就是一个文档,我们可以对文档中的title内容构建倒排索引:
- 使用某种分词算法将title进行分词,例如"小米手机"就可以得到"小米"、"手机"两个词条
- 以词条作为键,文档id列表作为值
- 如果词条已经出现过,那么就在文档列表末尾追加当前文档,如果该词条没有出现过,则构建一个新的键值对,键为当前词条,值为当前文档id
当对上述四条数据分词结束后倒排索引为:
词条 | 文档id列表 |
---|---|
小米 | [1, 4] |
手机 | [1, 2] |
华为 | [2, 3] |
手表 | [3] |
汽车 | [4] |
倒排索引工作流程:
- 此时当用户搜索关键词为"小米手表",使用相同的分词算法得出词条"小米"、“手表”
- 然后就会查询倒排索引,小米对应的文档id有1、4, 手表对应的文档有3
- 然后再根据正排索引查询id对应的具体文档内容
1.2.3 IK分词器
1.2.3.1 基本使用
我们已经了解到ES工作原理需要借助一定的分词算法进行分词匹配,那么就需要一个字典(dict)来保存一些常用的词语,分词算法才可以判断某一个序列是否可以作为一个词条。
而ES标准的分词器对于中文分词支持力度不大!例如我们尝试在devtools发起如下分词请求:
POST /_analyze
{"analyzer": "standard","text": "我喜欢学习Java"
}
可以观察到"standard"模式下的响应结果并非是我们想得到的!这个时候我们就需要使用到IK分词器了
安装步骤:
- 方式一:使用在线安装的方式,输入以下命令:
# 在线安装
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
# 重启docker es服务
docker restart es
- 方式二:离线安装,将下载好的ik分词压缩包挂载在数据卷中
# 查看所有的数据卷位置
docker volume ls
# 查看docker es-plugins数据卷位置
docker volume inspect es-plugins
# cd xxx
# 上传ik压缩包
# 重启docker es服务
docker restart es
此时再次发起请求,结果如下:
1.2.3.2 设置扩展词以及停用词
**扩展词:**我们需要自定义一些单词放在字典当中,例如"白嫖"、“米饭论坛”
**停用词:**在中文中一些语气词比如啊、哦、了是不需要进行分词的,可以剔除
前面我们提到IK分词器需要依赖字典,该配置文件就可以在ik文件夹目录中的/config/IKAnalyzer.cfg.xml进行配置扩展词以及暂停词的文件
我们进行上述配置:然后重启es: docker restart es
此后es就会读取同级目录下的ext.dic获取扩展字典当中的内容,读取stopwords.dic获取停用词列表
证明拓展词以及停用词配置成功!
1.2.4 索引库操作
- 创建索引库和映射:
请求方式:PUT
请求路径:/索引库名(自己定义)
请求参数:mapping映射
PUT /索引库名
{"mappings": {"properties": {"字段名": {"type": "text","index": true,"analyzer": "ik_smart"},"字段名": {"type": object,"properties": {"字段名": {"type": boolean}}}}}
}
- 查询索引库:
请求方式:GET
请求路径:/索引库名
请求参数:无
GET /索引库名
- 删除索引库:
请求方式:DELETE
请求路径:/索引库名
请求参数:无
DELETE /索引库名
- 修改索引库
修改索引结构在es中是不被允许的,因此如果修改了字段结构会导致倒排索引重建,开销巨大,但是我们可以新增字段
请求方式:PUT
请求路径:/索引库名/_mapping
PUT /索引库名/_mapping
{"properties": {"新字段名": {}}
}
索引库操作总结:
- 新增:PUT /索引库名
- 查询:GET /索引库名
- 修改 PUT /索引库名/_mapping
- 删除:DELETE /索引库名
1.2.5 文档操作
1.3 Java客户端
1.3.1 Java客户端初始化
上述我们实践了借助Kibana发送请求,但是我们还是需要学习使用Java客户端编程的方式实现:
- 在pom文件中引入
RestHignLevelClient
依赖
<!-- 引入es依赖 -->
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.12.1</version>
</dependency>
- 初始化RestHignLevelClient对象
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class EsDemoApplicationTests {private RestHighLevelClient client;@BeforeEachpublic void init() {client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://129.211.6.176:9200")));}@Testpublic void testClient() {System.out.println(client);}
}
1.3.2 创建映射以及索引库
原先我们在Kibana中使用如下HTTP请求配置映射信息以及索引库:
PUT /user
{"mappings": {"properties": {"desc": {"type": "text","index": "true","analyzer": "ik_smart"},"name": {"type": "text","index": "false"},"age": {"type": "integer","index": "false"}}}
}
而在Java代码中我们需要通过以下的方式来创建:
/*** 测试创建索引库*/
@Test
public void testCreateIndex() throws IOException {// 1. 创建Request对象CreateIndexRequest request = new CreateIndexRequest("user");// 2. 配置携带参数request.source(MAPPING_TEMPLATE, XContentType.JSON);// 3. 发起请求client.indices().create(request, RequestOptions.DEFAULT);
}
1.3.3 查看索引库
原先我们在Kibana中使用如下HTTP请求查看索引库:
GET /user
/*** 测试获取索引库信息*/
@Test
public void testGetIndex() throws IOException {// 1. 创建request对象GetIndexRequest request = new GetIndexRequest("user");// 2. 发起请求boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);System.out.println("exists: " + exists);
}
1.3.4 删除索引库
原先我们在Kibana中使用如下HTTP请求删除索引库:
DELETE /user
/*** 测试删除索引库*/
@Test
public void testDeleteIndex() throws IOException {// 1. 创建request对象DeleteIndexRequest request = new DeleteIndexRequest("user");// 2. 发起请求client.indices().delete(request, RequestOptions.DEFAULT);
}
1.3.5 新增文档
原先我们在Kibana中使用如下HTTP请求增加文档:
# 插入文档
POST /user/_doc/1
{"desc": "测试描述","name": "ricejson","age": 20
}
/*** 新增文档*/
@Test
public void testCreateDocument() throws IOException {// 1. 创建requestIndexRequest request = new IndexRequest("user").id("1");// 创建用户对象User user1 = new User("测试描述", "ricejson", 20);Gson gson = new Gson();String userStr = gson.toJson(user1);// 2. 填写内容request.source(userStr, XContentType.JSON);// 3. 发起请求client.index(request, RequestOptions.DEFAULT);
}
1.3.6 查看文档
原先我们在Kibana中使用如下HTTP请求查看文档:
GET /user/_doc/1
/*** 获取文档*/
@Test
public void testGetDocument() throws IOException {// 1. 创建requestGetRequest request = new GetRequest("user", "1");// 2. 发起请求GetResponse resp = client.get(request, RequestOptions.DEFAULT);// 3. 获取返回内容String source = resp.getSourceAsString();System.out.println(source);
}
1.3.7 删除文档
原先我们在Kibana中使用如下HTTP请求删除文档:
DELETE /user/_doc/1
/*** 删除文档*/
@Test
public void deleteDocument() throws IOException {// 1. 创建request对象DeleteRequest request = new DeleteRequest("user", "1");// 2. 发起删除文档请求client.delete(request, RequestOptions.DEFAULT);
}
1.3.8 修改文档
全量修改:与新增文档一致
局部修改:
POST /user/_update/1
{"doc": {"name": "米饭好好吃"}
}
/*** 局部更新文档*/
@Test
public void updateDocument() throws IOException {// 1. 创建request对象UpdateRequest request = new UpdateRequest("user", "1");// 2. 设置更新内容request.doc("name", "米饭好好吃");// 3. 发起请求client.update(request, RequestOptions.DEFAULT);
}
1.3.9 批处理
/*** 测试批处理操作*/
@Test
public void testBatch() throws IOException {// 1. 创建request对象BulkRequest bulkRequest = new BulkRequest();// 2. 添加多个request操作// 创建用户对象User user = new User("测试描述", "ricejson", 20);Gson gson = new Gson();String userStr = gson.toJson(user);bulkRequest.add(new IndexRequest("user").id("1").source(userStr, XContentType.JSON));bulkRequest.add(new IndexRequest("user").id("2").source(userStr, XContentType.JSON));// 3. 发起批处理请求client.bulk(bulkRequest, RequestOptions.DEFAULT);
}
1.4 DSL语句
1.4.1 快速入门
现在我们需要设计一个商品表,其中MySQL存储如下字段内容:
id: int 编号
name: varchar(20) 名称
brand:varchar(20) 品牌
price: int 价格
desc: varchar(100) 描述内容
# 定义以及新增商品索引库
PUT /goods
{"mappings": {"properties": {"id": {"type": "integer"},"name": {"type": "text","analyzer": "ik_smart"},"brand": {"type": "keyword"},"price": {"type": "integer"},"desc": {"type": "text","analyzer": "ik_smart"}}}
}
现在插入几条模拟数据:
POST /goods/_doc/1
{"id": 1,"name": "超级智能手表","brand": "FutureTech","price": 1299,"desc": "这款超级智能手表拥有最新的健康监测功能,能够实时追踪您的心率和睡眠质量。"
}POST /goods/_doc/2
{"id": 2,"name": "无线蓝牙耳机","brand": "AudioMaster","price": 499,"desc": "享受无线自由,这款无线蓝牙耳机提供卓越的音质和长达24小时的续航能力。"
}POST /goods/_doc/3
{"id": 3,"name": "便携式咖啡机","brand": "CoffeeCraft","price": 699,"desc": "随时随地享受新鲜咖啡,这款便携式咖啡机设计精巧,操作简便,是咖啡爱好者的理想选择。"
}POST /goods/_doc/4
{"id": 4,"name": "智能扫地机器人","brand": "CleanRobotics","price": 2999,"desc": "智能扫地机器人,自动导航,高效清洁,为您节省宝贵的时间。"
}POST /goods/_doc/5
{"id": 5,"name": "多功能运动相机","brand": "ActionCam","price": 1499,"desc": "这款多功能运动相机防水防尘,适合各种极限运动场景,记录您的每一个精彩瞬间。"
}
搜索索引库中全部的文档内容语法如下:
GET /goods/_search
{"query": {"match_all": {}}
}
1.4.2 叶子查询
常见的叶子查询主要有如下两类:
- 全量查询:要求查询条件必须是可以分词的
- 精确查询
1.4.2.1 全量查询
其中全量查询语法格式如下:
# 全量查询
GET /goods/_search
{"query": {"match": {"name": "智能"}}
}
除此以外我们还可以指定查询多个字段:
# 全量查询(查询多个字段)
GET /goods/_search
{"query": {"multi_match": {"query": "您","fields": ["name", "desc"]}}
}
1.4.2.2 精确查询
精确查询有如下两种常见方式:
- term:词条完全匹配
- range:范围查询,比如price >= 1000 以及 price <= 1999
# 精确查询(词条查询)
GET /goods/_search
{"query": {"term": {"brand": "ActionCam"}}
}
# 精确查询(范围查询)
GET /goods/_search
{"query": {"range": {"price": {"gte": 1499,"lte": 2999}}}
}
1.4.3 复合查询
bool查询:
将多个叶子查询经过must
,should
、must_not
、filter
等与或非操作合并成一个复杂查询:
# 复合查询(查询name中包含机器人并且价格在1499-2999之间)
GET /goods/_search
{"query": {"bool": {"must": [{"match": {"name": "机器人"}}],"filter": [{"range": {"price": {"gte": 1499,"lte": 2999}}}]}}
}
1.4.4 排序+分页
排序:
我们还可以在query的同级目录下加入order: {"排序字段": "asc|desc"}
- ASC:表示正序
- DESC:表示倒序
# 排序
GET /goods/_search
{"query": {"match_all": {}},"sort": {"price": "asc"}
}
分页:
我们还可以在query的同级目录下加入"from": x, "size": x
- from:表示从哪一条数据开始
- size:表示单页的数量
# 分页
GET /goods/_search
{"query": {"match_all": {}},"from": 0,"size": 3
}
1.4.5 高亮显示
想必大家都遇到过以下场景:
即百度搜索返回的结果中,与关键字匹配的结果都会高亮(红色)显示,打开开发者工具能够发现被高亮的部分由标签进行包括并使用CSS语法标红醒目。
那么问题就出现了,这个标签到底是由前端分析生成的还是由后端返回的?
- 前端:如果让前端进行处理,都需要对查询内容进行分词,然后在内容中进行匹配,但是这样一来性能开销就非常大,前端一般只做数据展示,数据处理一般让后端处理
- 后端:事实上ES在进行倒排索引的构建过程中,会保存查询分词在文档中的位置,然后在查询词前后加上等标签返回给前端。✔
DSL添加高亮显示:
# 高亮显示
GET /goods/_search
{"query": {"match": {"name": "智能"}},"highlight": {"fields": {"name": {"pre_tags": "<em>","post_tags": "</em>"}}}
}
1.5 Java客户端操作DSL
1.5.1 快速入门
我们现在想要查询出索引库为goods
下全部的文档内容,与之匹配的DSL语句如下:
GET /goods/_search
{"query": {"match_all": {}}
}
相对应的java代码如下:
/*** 测试查询全部文档*/
@Test
public void testMatchAll() throws IOException {// 1. 创建requestSearchRequest request = new SearchRequest("goods");// 2. 添加query查询条件request.source().query(QueryBuilders.matchAllQuery());// 3. 发起请求获取响应SearchResponse resp = client.search(request, RequestOptions.DEFAULT);// 4. 解析响应// 4.1 获取到hitsSearchHits searchHits = resp.getHits();// 4.2 获取到数据条数TotalHits hitsCount = searchHits.getTotalHits();// 4.3 获取到数据数组SearchHit[] hitsHits = searchHits.getHits();// 4.3 逐个解析for (SearchHit searchHit : hitsHits) {// 4.4 获取source数据String source = searchHit.getSourceAsString();System.out.println(source);}
}
1.5.2 叶子查询
前面我们已经介绍过叶子查询主要有如下几类:
- 全量查询
- match:单字段匹配
- match_all:多字段匹配
- 精确查询
- term:根据词条内容匹配
- range:根据范围匹配
需求1:现在我们需要找出desc中包含"这款"的文档
对应的DSL语句如下:
GET /goods/_search
{"query": {"match": {"desc": "这款"}}
}
对应的Java代码如下:
/*** 查询desc中包含"这款"的内容*/@Testpublic void testMatch() throws IOException {// 1. 创建request对象SearchRequest request = new SearchRequest("goods");// 2. 设置查询条件request.source().query(QueryBuilders.matchQuery("desc", "这款"));// 3. 发起请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.1 获取到hitsSearchHits hits = response.getHits();// 4.2 获取到数据总条数TotalHits totalHits = hits.getTotalHits();// 4.3 获取到数据集合SearchHit[] searchHits = hits.getHits();// 4.4 逐条解析for (SearchHit searchHit : searchHits) {String source = searchHit.getSourceAsString();System.out.println(source);}}
需求2:现在我们需要查询desc中包含"相机"并且name中包含"相机"的文档
对应的DSL语句如下:
GET /goods/_search
{"query": {"multi_match": {"query": "相机","fields": ["name", "desc"]}}
}
对应的Java代码如下:
/*** 查询desc并且name中都包含"相机"的文档*/
@Test
public void testMultiMatch() throws IOException {// 1. 创建requestSearchRequest request = new SearchRequest("goods");// 2. 添加查询条件request.source().query(QueryBuilders.multiMatchQuery("相机", "name", "desc"));// 3. 发起请求获取响应SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.1 获取到hitsSearchHits hits = response.getHits();// 4.2 获取到数据总数TotalHits totalHits = hits.getTotalHits();// 4.3 获取全部查询文档SearchHit[] searchHits = hits.getHits();// 4.4 解析每条数据for (SearchHit searchHit : searchHits) {// 4.5 获取source数据String source = searchHit.getSourceAsString();System.out.println(source);}
需求3:现在我们需要精确查找手机品牌为"ActionCam"的文档
对应的DSL语句如下:
GET /goods/_search
{"query": {"term": {"brand": "ActionCam"}}
}
对应的Java代码如下:
/*** 查询手机品牌为"ActionCam"的文档*/
@Test
public void testTerm() throws IOException {// 1. 创建request对象SearchRequest request = new SearchRequest("goods");// 2. 设置查询条件request.source().query(QueryBuilders.termQuery("brand", "ActionCam"));// 3. 发起请求获取响应SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.1 获取hitsSearchHits hits = response.getHits();// 4.2 获取数据总数TotalHits totalHits = hits.getTotalHits();// 4.3 获取全部匹配文档SearchHit[] searchHits = hits.getHits();// 4.4 解析出每一条数据for (SearchHit searchHit : searchHits) {// 4.5 获取sourceString source = searchHit.getSourceAsString();System.out.println(source);}
需求4:现在我们需要查询价格高于1999的文档
对应的DSL语句如下:
GET /goods/_search
{"query": {"range": {"price": {"gt": 1999}}}
}
对应的Java代码如下:
/*** 查询价格高于1499的文档*/
@Test
public void testRange() throws IOException {// 1. 创建requestSearchRequest request = new SearchRequest("goods");// 2. 设置查询条件request.source().query(QueryBuilders.rangeQuery("price").gt(1499));// 3. 发起请求,获取响应SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.1 获取hitsSearchHits hits = response.getHits();// 4.2 获取数据总数TotalHits totalHits = hits.getTotalHits();// 4.3 获取数据全集SearchHit[] searchHits = hits.getHits();// 4.4 解析每条数据for (SearchHit searchHit : searchHits) {// 4.5 获取sourceString source = searchHit.getSourceAsString();System.out.println(source);}
}
1.5.3 复合查询
需求:查询价格低于2999并且name中包含"耳机"并且品牌为"AudioMaster"的文档
GET /goods/_search
{"query": {"bool": {"must": [{"match": {"name": "耳机"}}],"filter": [{"term": {"brand": "AudioMaster"}},{"range": {"price": {"lt": 2999}}}]}}
}
对应的Java代码如下:
/*** 查询价格低于2999并且name中包含"耳机"并且品牌为"AudioMaster"的文档*/
@Test
public void testBool() throws IOException {// 1. 创建requestSearchRequest request = new SearchRequest("goods");// 2. 设置查询条件request.source().query(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("name", "耳机")).filter(QueryBuilders.termQuery("brand", "AudioMaster")).filter(QueryBuilders.rangeQuery("price").lt(2999)));// 3. 发起请求,获取响应SearchResponse response = client.search(request, RequestOptions.DEFAULT);process(response);
}
1.5.4 分页+排序查询
需求:查询全部文档,按照价格从低到高,分页为第一页,三条数据
GET /goods/_search
{"query": {"match_all": {}},"sort": {"price": "ASC"},"from": 1,"size": 3
}
对应的Java代码如下:
/*** 查询全部文档,按照价格从低到高,分页为第一页,三条数据*/
@Test
public void testPageAndSort() throws IOException {// 1. 创建requestSearchRequest request = new SearchRequest("goods");// 2. 设置查询条件request.source().query(QueryBuilders.matchAllQuery()).sort("price", SortOrder.ASC).from(0).size(3);// 3. 发起请求,获取响应SearchResponse response = client.search(request, RequestOptions.DEFAULT);process(response);
}
1.5.5 高亮显示
DSL高亮语法:
GET /goods/_search
{"query": {"match": {"name": "相机"}},"highlight": {"fields": {"name": {"pre_tags": "<em>","post_tags": "</em>"}}}
}
Java客户端实现:
/*** 测试高亮*/
@Test
public void testHighlight() throws IOException {// 1. 创建requestSearchRequest request = new SearchRequest("goods");// 2. 设置查询条件request.source().query(QueryBuilders.matchQuery("name", "相机"));// 3. 设置高亮条件request.source().highlighter(new HighlightBuilder().field("name").preTags("<em>").postTags("</em>"));// 4. 进行查询SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 5. 进行解析SearchHits hits = response.getHits();SearchHit[] searchHits = hits.getHits();for (SearchHit searchHit : searchHits) {Map<String, HighlightField> hlfs = searchHit.getHighlightFields();HighlightField hlf = hlfs.get("name");String value = hlf.getFragments()[0].string();System.out.println(value);}
}
1.5.6 聚合
1.5.6.1 聚合分类
在DSL中,聚合可以分为如下几类:
- 桶(Buckets)聚合
- Terms Aggregation: 词条聚合
- Date 日期时间聚合
- 度量(Metric)聚合
- min:最小值
- max:最大值
- avg:平均值
- stats:统计最小值、最大值、平均值
- 管道(Pipeline)聚合
基于别的聚合再次聚合
1.5.6.2 聚合DSL语句
聚合三要素:
- 聚合名称
- 聚合类型
- 聚合字段
聚合DSL语法:
GET /goods/_search
{"size": 0,"aggs": {"brand_agg": {"terms": {"field": "brand","size": 5}}}
}
1.5.6.3 Java客户端操作聚合DSL语句
/*** 测试聚合DSL*/
@Test
public void testAggregation() throws IOException {// 1. 创建request对象SearchRequest request = new SearchRequest("goods");// 2. 编写查询条件request.source().query(QueryBuilders.matchAllQuery());// 2.1 设置分页request.source().size(0);// 2.2 设置聚合三要素request.source().aggregation(AggregationBuilders.terms("brand_agg").field("brand").size(5));// 3. 查询结果SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4. 解析结果Aggregations aggregations = response.getAggregations();Terms terms = aggregations.get("brand_agg");List<? extends Terms.Bucket> buckets = terms.getBuckets();for (Terms.Bucket bucket : buckets) {System.out.println("key: " + bucket.getKeyAsString());System.out.println("count: " + bucket.getDocCount());}
}