闲的无聊,于是写了这一篇爽文,米娜桑可直接用,除非几乎不可能有bug,有bug当我没说(doge)
QA:无想的一刀欧为啥不用springboot封装的操作依赖涅?
欧认为springboot对操作类过度封装,实现普通简单操作还行,但是涉及到较为复杂的操作时,难以使用,尤其是不同版本的springboot推出的api变化频繁,更加难以使用,es官方推出的api更新不会让操作类变化太频繁,个人感觉spboot操作不如es官方推出的api灵活强大,之前在工作中遇到的需求使用springboot提供的报错难以琢磨,且难以满足需求,所以使用了官方api
elasticsearch版本:7.4
安装操作文档:https://blog.csdn.net/UnicornRe/article/details/121747039?spm=1001.2014.3001.5501
依赖最好保持与es版本一致,如果以下依赖报错,在maven < parent > 同级标签旁加上
<properties> <java.version>1.8</java.version> <!-- <spring-cloud.version>2020.0.2</spring-cloud.version> --> <!--解决版本问题--> <elasticsearch.version>7.4.0</elasticsearch.version> </properties>
<!--elasticsearch--> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.4.0</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.4.0</version> </dependency>
可自行修改配置和代码增加多台es机器,address逗号隔开
elasticsearch: schema: http address: 192.168.52.43:9200 connectTimeout: 5000 socketTimeout: 5000 connectionRequestTimeout: 5000 maxConnectNum: 100 maxConnectPerRoute: 100
import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import java.time.Duration; import java.util.ArrayList; import java.util.List; @Configuration public class EsHighLevalConfigure { //协议 @Value("${elasticsearch.schema:http}") private String schema="http"; // 集群地址,如果有多个用“,”隔开 @Value("${elasticsearch.address}") private String address; // 连接超时时间 @Value("${elasticsearch.connectTimeout:5000}") private int connectTimeout; // Socket 连接超时时间 @Value("${elasticsearch.socketTimeout:10000}") private int socketTimeout; // 获取连接的超时时间 @Value("${elasticsearch.connectionRequestTimeout:5000}") private int connectionRequestTimeout; // 最大连接数 @Value("${elasticsearch.maxConnectNum:100}") private int maxConnectNum; // 最大路由连接数 @Value("${elasticsearch.maxConnectPerRoute:100}") private int maxConnectPerRoute; @Bean public static RestHighLevelClient restHighLevelClient() { List<HttpHost> hostLists = new ArrayList<>(); String[] hostList = address.split(","); for (String addr : hostList) { String host = addr.split(":")[0]; String port = addr.split(":")[1]; hostLists.add(new HttpHost(host, Integer.parseInt(port), schema)); } HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{}); // 构建连接对象 RestClientBuilder builder = RestClient.builder(httpHost); // 异步连接延时配置 builder.setRequestConfigCallback(requestConfigBuilder -> { requestConfigBuilder.setConnectTimeout(connectTimeout); requestConfigBuilder.setSocketTimeout(socketTimeout); requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeout); return requestConfigBuilder; }); // 异步连接数配置 builder.setHttpClientConfigCallback(httpClientBuilder -> { httpClientBuilder.setMaxConnTotal(maxConnectNum); httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute); httpClientBuilder.setKeepAliveStrategy((response, context) -> Duration.ofMinutes(5).toMillis()); return httpClientBuilder; }); return new RestHighLevelClient(builder); } }
虽然索引结构肯定不是和你们一样的,但是代码结构不需要伤经动骨,
我来简单说说这个结构吧,一条知识产权信息內包含n个文档annex,包含n个(申请人发明人)applicant,
所以使用了 “type”: “nested"嵌套类型,不晓得与"type”: "object"区别的小伙伴自行学习吧,这里就不多说了。
想要学习部分优化的,安装,数据迁移冷备份的可以看看我的文章:(东西太多,部分就没写)https://blog.csdn.net/UnicornRe/article/details/121747039?spm=1001.2014.3001.5501
PUT /intellectual { "settings": { "number_of_shards": 1, "number_of_replicas": 1 } } PUT /intellectual/_mapping { "properties": { "id": { "type": "long" }, "name": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "type": { "type": "keyword" }, "keycode": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "officeId": { "type": "keyword" }, "officeName": { "type": "keyword" }, "titular": { "type": "keyword" }, "applyTime": { "type": "long" }, "endTime": { "type": "long" }, "status": { "type": "keyword" }, "agentName": { "type": "text", "analyzer": "ik_smart", "search_analyzer": "ik_smart" }, "annex": { "type": "nested", "properties": { "id": { "type": "long" }, "name": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" }, "createTime": { "type": "long" } } }, "applicant": { "type": "nested", "properties": { "id": { "type": "long" }, "applicantId": { "type": "long" }, "isOffice": { "type": "integer" }, "userName": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "outUsername": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" } } } } }
先不管"type": "nested"嵌套的对象,只对普通字段操作
我先定义一个实体类IntellectualEntity字段和上面的mapping一致
所有操作都注入了RestHighLevelClient restHighLevelClient
public void insertIntel(IntellectualEntity intellectualEntity) throws IOException { //intellectual为索引名 IndexRequest indexRequest = new IndexRequest("intellectual") .source(JSON.toJSONString(intellectualEntity), XContentType.JSON) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .id(intellectualEntity.getId()+"");//手动指定es文档的id IndexResponse out = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT); log.info("状态:{}", out.status()); }
只会更新entity不为空的字段,如同mybatisplus默认自带的update
因为es文档的id一定唯一,所以方法最多只能更新一条
public void updateIntel(IntellectualEntity entity) throws IOException { //根据IntellectualEntity的id更新文档 UpdateRequest updateRequest = new UpdateRequest("intellectual", entity.getId()+""); byte[] json = JSON.toJSONBytes(entity); updateRequest.doc(json, XContentType.JSON); UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); log.info("状态:{}", response.status()); }
painless脚本适用很多业务复杂的场合,比如如下更新值字段为map里的字段
private void updateByQuery(IntellectualEntity entity) throws IOException { UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(); updateByQueryRequest.indices("intellectual"); //搜索条件为id(因为插入时指定doc的id和实体类id一致,这样就保证了搜索结果唯一) //如果搜索条件查出的结果很多,使用需谨慎 updateByQueryRequest.setQuery(new TermQueryBuilder("id", entity.getId())); //map存储脚本实体参数值 Map<String,Object> map=new HashMap<>(); map.put("intelName", entity.getName()); map.put("intelStatus", entity.getStatus()); map.put("intelApplyTime", entity.getApplyTime()); map.put("intelKeyCode", entity.getKeycode()); map.put("intelEndTime", entity.getEndTime()); map.put("intelType", entity.getType()); map.put("intelTitular", entity.getTitular()); //指定哪些字段需要更新,ctx._source.xxx为es的字段,使用map的值赋值更新 updateByQueryRequest.setScript(new Script(ScriptType.INLINE, "painless", "ctx._source.intelName=params.intelName;" + "ctx._source.intelStatus=params.intelStatus;"+ "ctx._source.intelApplyTime=params.intelApplyTime;"+ "ctx._source.intelKeyCode=params.intelKeyCode;"+ "ctx._source.intelType=params.intelType;"+ "ctx._source.intelTitular=params.intelTitular;" , map)); BulkByScrollResponse bulkByScrollResponse = restHighLevelClient.updateByQuery(updateByQueryRequest, RequestOptions.DEFAULT); log.info("创建状态:{}", bulkByScrollResponse.getStatus()); }
public void deleteIntel(IntellectualEntity entity) throws IOException { DeleteRequest deleteRequest=new DeleteRequest("intellectual",entity.getId()+""); DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT); log.info("状态:{}", deleteResponse.status()); }
和更新搜索条件操作类似,结合删除操作替换DeleteRequest为DeleteByQueryRequest,相信机智的你已经会了
这块代码暂时不涉及nested的字段的嵌套高亮
条件设置时,should=or,must=and
步骤:设置高亮构造器->搜索出结果->将高亮数据替换掉非高亮数据->返回结果
先写一个高亮构造器吧
高亮构造器:
private static void HighlightBuilder highlightBuilder; static { highlightBuilder = new HighlightBuilder(); highlightBuilder.numOfFragments(0);//从第一个分片获取高亮片段 highlightBuilder.preTags("<font color='#e75213'>");//自定义高亮标签 highlightBuilder.postTags("</font>"); highlightBuilder.highlighterType("unified");//高亮类型 highlightBuilder .field("name")//需要高亮的属性值 .field("keycode") ; highlightBuilder.requireFieldMatch(false);//多个字段高亮 }
搜索步骤:
public List<Map<String,Object>> queryByContent(String content,Integer pageCurrent, Date startTimeApply,Date endTimeApply,Date startTimeEnd,Date endTimeEnd ) throws IOException { //空格分割多条件,本搜索支持多搜索词条空格分开,多词条搜索关系用and String[] manyStr = content.split("\\s+"); //定义一个list<map>作为返回结果 List<Map<String,Object>> list = new LinkedList<>(); //首先构造条件构造器 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); if(manyStr.length>1){ for (int i=0;i<manyStr.length;i++){ BoolQueryBuilder innerBoolQueryBuilder = QueryBuilders.boolQuery(); //nestedQuery,嵌套搜索条件 innerBoolQueryBuilder.should(QueryBuilders.nestedQuery("annex",QueryBuilders.matchQuery("annex.content", manyStr[i]) , ScoreMode.Max).boost(2)); innerBoolQueryBuilder.should(QueryBuilders.nestedQuery("annex",QueryBuilders.matchQuery("annex.simpleContent", manyStr[i]) , ScoreMode.Max).boost(2)); innerBoolQueryBuilder.should(QueryBuilders.nestedQuery("applicant",QueryBuilders.matchQuery("applicant.userName", manyStr[i]).prefixLength(2).maxExpansions(4).boost(5) , ScoreMode.Max)); innerBoolQueryBuilder.should(QueryBuilders.nestedQuery("applicant",QueryBuilders.matchQuery("applicant.outUsername", manyStr[i]).prefixLength(2).maxExpansions(4).boost(5) , ScoreMode.Max)); innerBoolQueryBuilder.should(QueryBuilders.matchQuery("name", manyStr[i]).boost(8)); innerBoolQueryBuilder.should(QueryBuilders.termsQuery("officeName", manyStr[i]).boost(100)); innerBoolQueryBuilder.should(QueryBuilders.fuzzyQuery("keycode", manyStr[i]).boost(5)); innerBoolQueryBuilder.should(QueryBuilders.matchQuery("agentName", manyStr[i]).boost(5)); innerBoolQueryBuilder.should(QueryBuilders.termsQuery("status", manyStr[i]).boost(30)); //and关系 boolQueryBuilder.must(innerBoolQueryBuilder);// } } else { //没有空格的 boolQueryBuilder.should(QueryBuilders.nestedQuery("annex",QueryBuilders.matchQuery("annex.content", content) , ScoreMode.Max).boost(2)); boolQueryBuilder.should(QueryBuilders.nestedQuery("annex",QueryBuilders.matchQuery("annex.simpleContent", content) , ScoreMode.Max).boost(2)); //暂不用嵌套高亮.innerHit(new InnerHitBuilder().setHighlightBuilder(highlightBuilder) boolQueryBuilder.should(QueryBuilders.nestedQuery("applicant",QueryBuilders.matchQuery("applicant.userName", content).prefixLength(2).maxExpansions(4).boost(5) , ScoreMode.Max)); boolQueryBuilder.should(QueryBuilders.nestedQuery("applicant",QueryBuilders.matchQuery("applicant.outUsername", content).prefixLength(2).maxExpansions(4).boost(5) , ScoreMode.Max)); boolQueryBuilder.should(QueryBuilders.matchQuery("name", content).boost(8)); boolQueryBuilder.should(QueryBuilders.termsQuery("officeName", content).boost(100)); boolQueryBuilder.should(QueryBuilders.fuzzyQuery("keycode", content).boost(5)); boolQueryBuilder.should(QueryBuilders.matchQuery("agentName", content).boost(5)); boolQueryBuilder.should(QueryBuilders.termsQuery("status", content).boost(30)); } if(startTimeApply!=null){ //filter 不参与评分,不会由于搜索时间条件造成搜索评分较高导致排序不准确 boolQueryBuilder.filter(QueryBuilders.rangeQuery("applyTime").gte(startTimeApply.getTime())); } if(endTimeApply!=null){ boolQueryBuilder.filter(QueryBuilders.rangeQuery("applyTime").lte(endTimeApply.getTime())); } if(startTimeEnd!=null){ boolQueryBuilder.filter(QueryBuilders.rangeQuery("endTime").gte(startTimeEnd.getTime())); } if(endTimeEnd!=null){ boolQueryBuilder.filter(QueryBuilders.rangeQuery("endTime").lte(endTimeEnd.getTime())); } //新建请求 SearchRequest searchRequest = new SearchRequest("intellectual"); //新建搜索配置器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //搜索配置器 -> 高亮配置器 searchSourceBuilder.highlighter(highlightBuilder); //搜索配置器 -> 配置组合条件 //解释:minScore为搜索匹配项的最小评分,低于此分数的项不计入搜索结果,评分大小受到 搜索条件的.boost(权值)的影响, //boost值越大,导致计算评分越大,如果搜索结果不满意,可以测试调整boost的值达到比较满意的结果,还有一种方案就是自定义计算评分公式,属于专家级使用方案 //分页解释:这种from-size分页当 页数过大的集群 可能导致搜索崩溃(因为搜索结果汇总数据条数过大,需要较大jvm内存,原理我就懒得讲太多,写不下,米娜桑有兴趣自行学习去吧), //解决方案是使用深度分页,当然了,单机单分片的es机器from-size不会导致搜索崩溃 searchSourceBuilder .minScore(9)//设置最小评分 .query(boolQueryBuilder)//装载搜索条件 .from((pageCurrent-1)*10)//起始条数,从0 .size(10)//每页展示记录 ; //装载搜索配置器 searchRequest.source(searchSourceBuilder); //搜索返回结果 SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); //可测试搜索的分数,结合调整boot值可以让搜索结果更加 “尽人意” log.info("总条数"+search.getHits().getTotalHits().value); log.info("符合条件的文档最大得分: "+search.getHits().getMaxScore()); //遍历搜索结果列表 for(SearchHit documentFields : search.getHits().getHits()){ //sourceAsMap是不包含高亮的结果,如果搜索不要求高亮,就直接返回结果 Map<String, Object> sourceAsMap = documentFields.getSourceAsMap(); //highlightFieldsMap是高亮的结果 Map<String, HighlightField> highlightFieldsMap = documentFields.getHighlightFields(); //通过getHighLightMap方法将原不高亮的字段结果替换为高亮结果 sourceAsMap = changeHighLightMap(sourceAsMap, highlightFieldsMap); //因为es存的时间设置为long时间戳类型,需要转化 sourceAsMap.put("applyTime", new Date(Long.parseLong(sourceAsMap.get("applyTime")+""))); if(sourceAsMap.get("endTime")!=null){ sourceAsMap.put("endTime",new Date(Long.parseLong(sourceAsMap.get("endTime")+""))); } //打印分值 log.info(documentFields.getScore()); //存入list list.add(sourceAsMap); } return list; }
changeHighLightMap方法,这里暂时去除nested字段高亮显示
private Map<String, Object> changeHighLightMap(Map<String, Object> map, Map<String, HighlightField> highlightFields) { //从高亮结果获取高亮属性值,因为一条数据有多个属性值,高亮设器也可以设置多个属性值, //搜索的结果可能有的属性值搜索命中被存入highlightFields,有的属性值搜索没有命中则不会存入highlightFields, //所以判断!=null时则认为这条数据的这个属性值被命中 HighlightField highlightName = highlightFields.get("name"); HighlightField highlightKC = highlightFields.get("keycode"); if (highlightName != null) { //可以看到fragments()本身是个数组,如果是nested类型数据fragments()数组长度可能较大, //但是这里选的高亮类型数据没有nested类型的,所有要么highlightName=null要么fragments长度=1 //替换高亮的数据到不高亮的结果集 map.put("name", highlightName.fragments()[0].string()); } if (highlightKC != null) { map.put("keycode", highlightKC.fragments()[0].string()); } }
上面的一条知识产权信息內包含n个文档annex,包含n个(申请人发明人)applicant,这两个属性类型都是nested类型,属于列表
先写一个格式化工具:
private static com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); //格式化参数 private static Map<String, Object> convertValueToMap(Object data) { return mapper.convertValue(data, new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {}); }
假如:这条知识产权数据有了,用户上传一个pdf文档上来,我们读取完pdf文档内容后,需要将这个pdf信息存入到对应知识产权的annex列表里
public void addAnnex(IntellectualEntity entity,AnnexEntity annexEntity) throws IOException { UpdateRequest updateRequest=new UpdateRequest("intellectual",entity.getId()+""); Map<String, Object> param = new HashMap<>(); //格式化参数 param.put("data",convertValueToMap(annexEntity)); //ctx._source为固定写法 StringBuffer sc = new StringBuffer("ctx._source.annex.add(params.data);"); Script script = new Script(INLINE, Script.DEFAULT_SCRIPT_LANG, sc.toString(), param); updateRequest.script(script); //按脚本更新,第一次插入之后,也会执行脚本,没有数据的话,插入upsert的内容 //updateRequest.scriptedUpsert(true); //这个必须有不然没有数据的话,会报错 //updateRequest.upsert(param); UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); }
这时用户要删除归属某条知识产权的某个annex文档
public void deleteAnnex(String intelId,Integer annexId) throws IOException { UpdateRequest updateRequest=new UpdateRequest("intellectual",intelId); Map<String, Object> param = new HashMap<>(); param.put("id",annexId);//字符串无效 StringBuffer sc = new StringBuffer("ctx._source.annex.removeIf(item -> item.id == params.id)"); Script script = new Script(INLINE, Script.DEFAULT_SCRIPT_LANG, sc.toString(), param); updateRequest.script(script); UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); }
用户修改归属某条知识产权的某个annex文档
public void addAnnex(IntellectualEntity entity,AnnexEntity annexEntity) throws IOException { UpdateRequest updateRequest=new UpdateRequest("intellectual",entity.getId()+""); Map<String, Object> param = new LinkedHashMap<>(); //格式化参数 param.put("data",convertValueToMap(annexEntity)); //ctx._source为固定写法 StringBuffer sc = new StringBuffer( "int i = 0;for(LinkedHashMap item:ctx._source.annex){if(item.id == params.data.id){ctx._source.annex[i] = params.data;}i++;}" ); Script script = new Script(INLINE, Script.DEFAULT_SCRIPT_LANG, sc.toString(), param); updateRequest.script(script); UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); }
同理先写个高亮配置器
对nested类型数据annex的多个属性加高亮,(最好不要写在普通高亮配置器里)
private static HighlightBuilder highlightBuilder2; static { highlightBuilder2 = new HighlightBuilder(); highlightBuilder2.numOfFragments(0); highlightBuilder2.preTags("<font color='#e75213'>"); highlightBuilder2.postTags("</font>"); highlightBuilder2.highlighterType("unified"); highlightBuilder2 .field("annex.content") .field("annex.simpleContent") .field("applicant.userName") .field("applicant.outUsername") ; highlightBuilder2.requireFieldMatch(false); }
搜索代码nestedQuery加上
.innerHit(new InnerHitBuilder().setHighlightBuilder(highlightBuilder2))
boolQueryBuilder.should(QueryBuilders.nestedQuery("annex",QueryBuilders.matchQuery("annex.content", "文本") , ScoreMode.Max).boost(2).innerHit(new InnerHitBuilder().setHighlightBuilder(highlightBuilder2)));
依然是普通高亮代码,做修改
for(SearchHit documentFields : search.getHits().getHits()){ //sourceAsMap是不包含高亮的结果 Map<String, Object> sourceAsMap = documentFields.getSourceAsMap(); //获取嵌套命中数据 Map<String, SearchHits> innerHits = documentFields.getInnerHits(); //引用传参,替换nested高亮 changeNestedHighLightMap(innerHits,sourceAsMap); //highlightFieldsMap是高亮的结果,获取普通高亮 Map<String, HighlightField> highlightFieldsMap = documentFields.getHighlightFields(); //通过getHighLightMap方法将替换普通高亮 sourceAsMap = changeHighLightMap(sourceAsMap,highlightFieldsMap); //因为es存的时间设置为long时间戳类型,需要转化 if(sourceAsMap.get("applyTime")!=null){ sourceAsMap.put("applyTime",new Date(Long.parseLong(sourceAsMap.get("applyTime")+""))); } if(sourceAsMap.get("endTime")!=null){ sourceAsMap.put("endTime",new Date(Long.parseLong(sourceAsMap.get("endTime")+""))); } //打印分值 //存入list list.add(sourceAsMap); }
changeNestedHighLightMap方法
private static void changeNestedHighLightMap(Map<String, SearchHits> innerHits, Map<String, Object> sourceAsMap) { SearchHit[] annexes = innerHits.get("annex").getHits(); if(annexes!=null){ for(SearchHit searchHit:annexes){ int offset = searchHit.getNestedIdentity().getOffset();//高亮命中的数组的下标 Map<String, HighlightField> highlightFields = searchHit.getHighlightFields(); List<Map<String,Object>> lm=(List<Map<String,Object>>)sourceAsMap.get("annex"); Map<String, Object> map = lm.get(offset); HighlightField content = highlightFields.get("annex.content"); if(content!=null){ map.put("content", content.fragments()[0].string()); } lm.set(offset,map); } } }
图解结果json解释nested高亮
{ "took": 7, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1.1507283, "hits": [ { "_index": "intellectual", "_type": "_doc", "_id": "1", "_score": 1.1507283, "_source": { "keycode": "keycode-1", "name": "无痛更新2", "id": 1, "applyTime": 1645442231033, "annex": [ { "size": null, "createTime": null, "filePath": null, "apId": null, "name": null, "simpleContent": null, "annexs": null, "id": 1, "type": null, "isLogimg": null, "content": "文本=====dddd" } ] }, ==========以上是原数据 ==========以下是nested数据并携带高亮数据 "inner_hits": { "annex": { "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 0.5753642, "hits": [ { "_index": "intellectual", "_type": "_doc", "_id": "1", "_nested": { "field": "annex", "offset": 0 ==========nested数组命中下标 }, "_score": 0.5753642, "_source": { "size": null, "createTime": null, "filePath": null, "apId": null, "name": null, "simpleContent": null, "annexs": null, "id": 1, "type": null, "isLogimg": null, "content": "文本=====dddd" }, "highlight": { "annex.content": [ ==========这里需要取出来替换原数据下标=offset的原数据 "<font color='#e75213'>文</font><font color='#e75213'>本</font>=====dddd" ] } } ] } } } } ] } }