[ElasticSearch] DSL查询
🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (93平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(97平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(96平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均质量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均质量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
💻 项目总结(97平均质量分) https://blog.csdn.net/2301_80050796/category_12936070.html?spm=1001.2014.3001.5482
🎈JVM(97平均质量分) https://blog.csdn.net/2301_80050796/category_12976744.html?spm=1001.2014.3001.5482
🔍ElasticSearch(96平均质量分) https://blog.csdn.net/2301_80050796/category_12978995.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. DSL查询
- 1.1 快速入门
- 1.2 叶子查询
- 1.2.1 全文检索查询
- 1.2.2 精确查询
- 1.3 复合查询
- 1.3.1 算分函数查询
- 1.3.2 bool查询
- 1.4 排序
- 1.5 分页
- 1.5.1 基础分页
- 1.5.2 深度分页
- 1.6 高亮
- 1.6.1 高亮显示原理
- 1.6.2 实现高亮
- 1.7 总结
- 2. RestClient查询
- 2.1 快速入门
- 2.2.1 发送请求
- 2.1.2 解析响应结果
- 2.1.3 总结
- 2.2 叶子查询
- 2.3 复合查询
- 2.4 排序和分页
- 2.5 高亮
今天我们来研究以下ElasticSearch的数据搜索功能,ElasticSearch提供了基于json的DSL语句来定义查询条件,其javaAPI就是在组织DSL条件.
因此我们先学习DSL的查询语法,在基于DSL来对照学习javaAPI,就会事半功倍.
1. DSL查询
ElasticSearch的查询可以分为两大类:
- 叶子查询: 一般是在特定的字段里查询特定的值,属于简单查询,很少单独使用.
- 复合查询: 以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式.
1.1 快速入门
我们依然在Kibana的DevTools中学习查询的DSL语法,首先来看查询的语法结构:
GET /{索引库名}/_search
{"query": {"查询类型": {// .. 查询条件}}
}
说明: GET /{索引库名}/_search
: 其中的_search
是固定路径,不能更改.
例如,我们以最简单的无条件查询,无条件查询的类型是: match_all
,因此查询语句如下:
GET /items/_search
{"query": {"match_all": {}}
}
由于match_all
无条件,所以条件位置不写即可.
执行结果如下:
你会发现虽然是match_all
,但是响应结果中并不会包含索引库中的所有文档,而是仅仅有10条,这是因为出于安全考虑,elasticSearch设置了默认的查询页数.
1.2 叶子查询
叶子查询的类型也可以做进一步细分,详情大家可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/query-dsl.html
如图:
这里列举一些常见的,例如:
- 全文检索查询(Full Text Queries): 利用分词器对用户输入搜索条件先分词,得到词条,然后在利用倒排索引搜索词条,例如:
- match
- multi_match
- 精确查询(Term-level queries): 不对用户输入搜索条件分词,根据字段内容精确值匹配,但是只能查找keyword,数值,日期,boolean类型的字段,比如:
- ids
- term
- range
- 地理坐标查询: 用于搜索地理位置,搜索方式很多,例如:
geo_bounding_box
: 按照矩形搜索geo_distance
: 按点和半径搜索
1.2.1 全文检索查询
全文检索的种类也很多,详情可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/full-text-queries.html
以全文检索中的match为例,语法如下:
GET /{索引库名}/_search
{"query": {"match": {"字段名": "搜索条件"}}
}
示例:
与match
类似的语法还有multi_match
,区别在于可以同时对多个字段字段进行搜索,而且多个字段都要满足,语法示例:
GET /{索引库名}/_search
{"query": {"multi_match": {"query": "搜索条件","fields": ["字段1", "字段2"]}}
}
示例:
1.2.2 精确查询
精确查询,英文是Term-level query
,顾名思义,词条级别的查询,也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配,因此推荐查找keyword,数值,日期,boolean类型的字段,例如:
- id
- price
- 城市
- 地名
- 人名
等等,作为一个整体才有含义的字段.
详情可以查看官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/term-level-queries.html
以term为例,其语法如下:
GET /{索引库名}/_search
{"query": {"term": {"字段名": {"value": "搜索条件"}}}
}
当输入的搜索条件不是词条,而是短语是,由于不做分词,反而搜索不到:
再看接下来range
查询,语法如下:
GET /{索引库名}/_search
{"query": {"range": {"字段名": {"gte": {最小值},"lte": {最大值}}}}
}
range是范围查询,对于范围筛选的关键字有:
- gte: 大于等于
- gt: 大于
- lte: 小于等于
- lt: 小于
示例:
1.3 复合查询
复合查询大致可以分为两类:
- 第一类: 基于逻辑运算组合叶子查询,实现组合条件,例如
- bool
- 第二类: 基于某种算法修改查询时的文档相关性算分,从而改变文档排名,例如:
- function_score
- dis_max
其他复合查询以及相关语法可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/compound-queries.html
1.3.1 算分函数查询
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列.
例如我们搜索"手机",结果如下:
从es5.1开始,采用的相关性打分算法是BM25算法,公式如下:
基于这套公式,救护可以判断出某个文档与用户搜索的关键字之间的关联度,还是比较准确的,但是,在实际业务需求中,常常会有竞价排名的功能,不是相关度越高排名越靠前,而是基于掏的钱多的排名更加靠前.
例如在百度中搜索java培训,排名靠前的就是广告推广:
要想人为控制相关性算分,就需要利用elasticsearch中的function sorce查询了.
基本语法:
function socre查询中包含了四部分内容:
- 原始查询条件: query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分.
- 过滤条件: filter部分,复合该条件的文档才会重新算分.
- 算分函数: 复合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight: 函数结果是常量
- field_value_factor: 以文档中的某个字段值作为函数结果
- random_socre: 以随机数作为函数结果
- script_score: 自定义算分函数算法
- 运算模式: 算分函数的结果,原始查询的相关性算分,两者之间的运算方式,包括:
- multiply: 相乘
- replace: 用function socre替换query socre
- 其他,例如: sum,avg,max,min
function socre的运行流程如下:
- 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分.
- 根据过滤条件,过滤文档
- 复合过滤条件的文档,基于算分函数运算,得到函数算分.
- 将原始算分和函数算分基于运算模式做运算,得到最终结果,作为相关性算分.
因此,其中的关键点是:
- 过滤条件: 决定哪些文档的算分被修改
- 算分函数: 决定函数算分的算法
- 运算模式: 决定最终算分结果
示例: 给iPhone这个品牌的手机算分提高十倍,分析如下:
- 过滤条件: 品牌必须为iPhone
- 算分函数: 常量weight,值为10
- 算分模式: 相乘multiply
对应代码如下:
GET /hotel/_search
{"query": {"function_score": {"query": { .... }, // 原始查询,可以是任意条件"functions": [ // 算分函数{"filter": { // 满足的条件,品牌必须是Iphone"term": {"brand": "Iphone"}},"weight": 10 // 算分权重为2}],"boost_mode": "multipy" // 加权模式,求乘积}}
}
1.3.2 bool查询
bool查询,即布尔查询,就是利用逻辑运算来组合一个或者多个子查询子句的组合.
bool查询支持的逻辑运算有:
- must: 必须匹配每个子查询,类似于"与"
- should; 选择性匹配子查询,类似"或"
- must_not: 必须不匹配,不参与算分,类似于"非"
- filter: 必须匹配,不参与算分(和must的区别).
bool查询语法如下:
GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手机"}}],"should": [{"term": {"brand": { "value": "vivo" }}},{"term": {"brand": { "value": "小米" }}}],"must_not": [{"range": {"price": {"gte": 2500}}}],"filter": [{"range": {"price": {"lte": 1000}}}]}}
}
出于性能考虑,与搜索关键字无关的查询尽量采用must_not
或filter
逻辑运算,避免参与相关性算分.
例如黑马商城的搜索页面:
其中输入框的搜索条件肯定要参与相关性算分,可以采用match,但是价格范围过滤,品牌过滤,分类过滤尽量采用filter,不要参与相关性算分.
比如,我们要搜索"手机",但是品牌必须是"华为",价格必须是900~1599
,那么可以这样写:
GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手机"}}],"filter": [{"term": {"brand": { "value": "华为" }}},{"range": {"price": {"gte": 90000, "lt": 159900}}}]}}
}
1.4 排序
elasticSearch默认是根据相关度算分(_socre)来排序,但是也支持自定方式对搜索结果排序.不过分词字段无法排序,能参与排序字段类型有: keyword
类型,数值类型,地理坐标类型,日期类型.
详细说明可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/sort-search-results.html
语法说明:
GET /indexName/_search
{"query": {"match_all": {}},"sort": [{"排序字段": {"order": "排序方式asc和desc"}}]
}
示例,我们按照商品价格排序:
GET /items/_search
{"query": {"match_all": {}},"sort": [{"price": {"order": "desc"}}]
}
1.5 分页
elasticsearch默认情况下只返回top10的数据.而如果要查询的更多数据就需要修改分页参数了.
1.5.1 基础分页
elasticSearch中通过修改form
,size
参数来控制要返回的分页结果:
- from: 从第几个文档开始
- size: 总共查询几个文档
类似于MySQL中的limit ?,?
,官方文档如下:
官方文档如下:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html
语法如下:
GET /items/_search
{"query": {"match_all": {}},"from": 0, // 分页开始的位置,默认为0"size": 10, // 每页文档数量,默认10"sort": [{"price": {"order": "desc"}}]
}
1.5.2 深度分页
elasticSearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同的节点上,这种存储方式比较有利于数据扩展,但是给分页带来了一些麻烦.
比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据.现在每页查询10条,查询99页,那么分页查询的条件如下:
GET /items/_search
{"from": 990, // 从第990条开始查询"size": 10, // 每页查询10条"sort": [{"price": "asc"}]
}
从语句来分析,要查询第990-1000名的数据.从数显思路来分析,坑定是将所有数据排序,找出前1000名,截取其中的990-1000的部分,但是问题来了,我们如何才能找到所有数据中的前1000名呢?
要知道每一片的数据都不一样,第一片上的第990-1000,在另一个节点上并不一定依然是990-1000名,所以我们只能在每一个分片上都要找出排名前1000的数据,然后汇总到一起,重新排序,才能找出整个索引库中真正的前1000名,此时截取的990-1000的数据即可.
就像我们高中的时候有普通班,有尖子班,比如有5个班,其中有一个尖子班,我们找全年级前50的时候,肯定不能只挑每个班的前10名,全年级前50肯定是尖子班多一些,所以我们在选人的时候需要对全年级进行排名.
试想一下,加入我们现在要查询的是第999页数据呢,是不是要找地9990-10000的数据,那岂不是要把每个分片中的前10000名的数据都查询出来,汇总到一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总的数据过多,对内存和CPU会产生非常大的压力.
因此elasticSearch会禁止from+ size
超过10000的请求.
针对深度分页,elasticSearch提供了两种解决方案:
- Search after: 分页时需要排序,原理是从上一次的排序开始,查询下一页数据,官方推荐使用的方式.
- scroll: 原理将排序后的文档id形成快照,保存下来,基于快照做分页,官方已经不推荐使用.
总结:
大多数情况下,我们采用普通分页就是可以了,一般我们采用限制分页深度的方式即可,无需实现深度分页.
1.6 高亮
1.6.1 高亮显示原理
什么是高亮显示呢?
我们在百度,京东搜索的时候,关键字就会变成红色,比较醒目,这就叫高亮显示:
观察页面源码,你会发现两件事情:
- 高亮词条都被加上的了
<em>
标签. <em>
标签都添加了红色样式.
css样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签,而服务端实现搜索功能,要是有elasticSearch
做分词搜索,要是知道哪些词条需要高亮的.
因此词条的高亮标签肯定是有服务端提供数据的时候已经加上的.
因此实现高亮的思路就是:
- 用户搜索关键字搜索数据
- 服务端根据搜索关键字找到elasticSearch搜索,并给搜索结果中的关键字词条添加一个html标签
- 前端提前给约定好的
html
标签添加css
样式.
1.6.2 实现高亮
事实上elasticSearch已经提供了给搜索关键字加标签的语法,无需我们自己编码.基本语法如下:
GET /{索引库名}/_search
{"query": {"match": {"搜索字段": "搜索关键字"}},"highlight": {"fields": {"高亮字段名称": {"pre_tags": "<em>","post_tags": "</em>"}}}
}
注意:
- 搜索必须有查询条件,而且是全文检索类型的查询条件,例如match
- 参与高亮的字段必须是text类型的字段.
- 默认情况下参与高亮的字段要与搜索字段一致,除非添加
required_field_match=false
.
示例:
1.7 总结
查询的DSL是一个大的json对象,包含下列属性:
- query: 查询条件
- from和size: 分页条件
- sort: 排序条件
- hightlight: 高亮条件
示例:
2. RestClient查询
文档的查询依然使用上一节学习的RestHighLevelClient
对象,查询的基本步骤如下:
- 创建request对象,这次是搜索,所以是
searchRequest
- 准备请求参数,也就是查询DSL对应的json参数
- 发起请求
- 解析响应,响应对应的结果相对复杂,需要逐层解析
2.1 快速入门
之前说过,由于elasticSearch对外暴露的接口都是restful风格的接口,因此JavaAPI调用就是在发送http请求,而我们核心要做的就是利用Java代码组织请求参数,解析响应结果.
这个参数的格式完全参考DSL查询语句的json结构,因此我们在学习的过程中,会不断的把JavaAPI与DSL语句做对比.
2.2.1 发送请求
首先以match_all
查询为例,其DSL和JavaAPI的对比如图:
代码解读:
- 第一步,创建
SearchRequest
对象,指定索引库名 - 第二部,利用
request.source()
创建DSL,DSL中可以包含查询,分页,排序,高亮等query()
: 代表查询条件,利用QueryBuilders.matchAllQuery()
构建一个match_all
查询的DSL.
- 第三步,利用
client.search()
发送请求,得到响应
这里关键的API有两个,一个是request.source()
,它构建的就是DSL中的完整的json参数,其中包含了query、sort、from、size、highlight等所有功能:
另一个是QueryBuilders
,其中包含了我们学习过的各种叶子查询,复合查询等:
2.1.2 解析响应结果
在发送请求以后,得到了相应的结果SearchResponse
,这个类的结构与我们在kibana中看到的相应结果json接口完全一致:
{"took" : 0,"timed_out" : false,"hits" : {"total" : {"value" : 2,"relation" : "eq"},"max_score" : 1.0,"hits" : [{"_index" : "heima","_type" : "_doc","_id" : "1","_score" : 1.0,"_source" : {"info" : "Java讲师","name" : "赵云"}}]}
}
因此,我们解析SearchResponse
的代码就是在解析这个json结果,对比如下:
代码解读:
elasticsearch返回的结果是一个json字符串,结构包含:
- hit: 命中的结果
- total: 总条数,其中value是具体的总条数值
- max_score: 所有的结果中得分最高的文档的相关性算分
- hit: 搜索结果的文档数组,其中的每个文档都是json对象
- _source: 文档中的原始数据,也是json对象
因此,我们解析响应结果,就是逐层解析json字符串,流程如下:
- SearchHits: 通过
response.getHits()
获取,就是json中最外层的hits,代表命中的结果SearchHits#getTotalHits().value
: 获取总条数信息SearchHits#getHits()
: 获取SearchHit
数组,也就是文档数组SearchHit#getSourceAsString()
: 获取文档结果中的_source
,也就是原始的json文档数据
2.1.3 总结
文档搜索的基本步骤是:
- 创建
SearchRequest
对象 - 准备
request.source()
,也就是DSL.QueryBuilders
来构建查询条件- 传入
request.source()
的query()
方法
- 发送请求,得到结果
- 解析结果(参考json结果,从外到内,逐层解析)
完整代码如下:
@Test
void testMatchAll() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.matchAllQuery());// 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);System.out.println(item);}
}
2.2 叶子查询
所有条件都是由QueryBuilders来构建的,叶子查询页不例外,因此整套代码中变化的部分仅仅是query条件构造的方式,其他的不动:
@Test
void testMatch() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
再比如multi_match
查询:
@Test
void testMultiMatch() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
还有range查询:
@Test
void testRange() throws IOException {// 1.创建RequestSearchRequest 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查询:
@Test
void testTerm() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.termQuery("brand", "华为"));// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
2.3 复合查询
复合查询也是由QueryBuilders
构建,我们以bool
查询为例,DSL和javaAPI的对比如图:
完整代码如下:
@Test
void testBool() throws IOException {// 1.创建RequestSearchRequest 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);
}
2.4 排序和分页
之前说过,requeset.source()
就是整个请求json参数,所以排序,分页都是基于这个来设置,其DSL和JavaAPI的对比如下:
完整的示例代码:
@Test
void testPageAndSort() throws IOException {int pageNo = 1, pageSize = 5;// 1.创建RequestSearchRequest 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);
}
2.5 高亮
高亮查询与前面的查询有两点不同:
- 条件同样是在
request.source()
中指定,只不过高亮条件要基于HighlightBuilder
来构造 - 高亮相应结果与搜索的文档结果不在一起,需要单独解析
首先来看高亮条件构造,其DSL和JavaAPI的对比如图:
示例代码如下:
@Test
void testHighlight() throws IOException {// 1.创建RequestSearchRequest 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);
}
再来看结果解析,文档解析的部分不变,主要是高亮内容需要单独解析出来,其DSL和JavaAPI的对比如图:
代码解读:
- 第3、4步:从结果中获取_source。hit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为ItemDoc对象
- 第5步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值
- 第5.1步:从Map中根据高亮字段名称,获取高亮字段值对象HighlightField
- 第5.2步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
- 最后:用高亮的结果替换ItemDoc中的非高亮结果
完整代码如下:
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);}
}