一、问题描述
公司将ES从原来的6.4.3版本升级到7.12.0后,原来的ES_client是6.4.3,在进行分页搜索的时候,发现总数一直为0。
二、问题排查
从旧版本的ES中查询时,得到的结果
升级后版本7.12.0查询结果为:
从上面的结果来看,发现ES6.4版本返回的直接是 "total": 5,ES7.12.0返回的结果是total里面封装的一个对象
"total": {
"value": 5,
"relation": "eq"
}
ES-server-7.12版本:response.getHits().getTotalHits()返回的是一个对象,除了有value标识总条目数,还有relation字段。
ES-server-6.4版本:response.getHits().getTotalHits()返回的直接就是总条目数total。
ES的结果发生了变化,那我们来看看Java是怎样处理的。
三、Java客户端源码分析
6.4版本
public final class SearchHits implements Streamable, ToXContentFragment, Iterable<SearchHit> {
public static final SearchHit[] EMPTY = new SearchHit[0];
private SearchHit[] hits;
public long totalHits; //重点看这里
private float maxScore;
}
7.12版本
public final class SearchHits implements Writeable, ToXContentFragment, Iterable<SearchHit> {
public static final SearchHit[] EMPTY = new SearchHit[0];
private final SearchHit[] hits;
private final TotalHits totalHits; //重点看这里
private final float maxScore;
}
从以上SearchHits可以看出来,两个版本的totalHits返回的类型果然是不一样的。那么使用ES-client-6.4版本在进行JSON转化时,由于服务端使用的时ES-Server-7.x版本,不能拿到正常的值,默认返回了的值为0。
看到这里,是不是已经知道为什么获取总条目时看到的结果一直是0了吧?
我们继续往下探讨:
四、问题解决
1、简单粗暴的方式
升级ES客户端,与ES版本一致,使用以下方式获取总数
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
response.getHits().getTotalHits().value;
但是对于已经上线多年的项目来说,升级客户端涉及到太多的代码改动了,为了节省代码的改动量,继续一探究竟。
2、深入研究
我们都知道在JSON反序列化时,需要JSON中key应该和对象的字段名一一对应才可以,那ES客户端时如何处理的呢?具体是怎么转化的?以下是ES-client-6.4版本与ES-client-7.x版本的分析。
ES 6.4
public final class SearchHits implements Streamable, ToXContentFragment, Iterable<SearchHit> {
public static final SearchHit[] EMPTY = new SearchHit[0];
private SearchHit[] hits;
public long totalHits;
private float maxScore;
}
"hits": {
"total": 10, //对应的java字段为totalHits
"max_score": 1, //对应的java字段为maxScore
"hits": [ ]
}
在toXContent方法中进行的字段名称的替换:注意标注替换的地方
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject("hits");
builder.field("total", this.totalHits); //替换
if (Float.isNaN(this.maxScore)) {
builder.nullField("max_score"); //替换
} else {
builder.field("max_score", this.maxScore); //替换
}
builder.field("hits");
builder.startArray();
SearchHit[] var3 = this.hits;
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
SearchHit hit = var3[var5];
hit.toXContent(builder, params);
}
builder.endArray();
builder.endObject();
return builder;
}
ES 7.12
public final class SearchHits implements Writeable, ToXContentFragment, Iterable<SearchHit> {
public static final SearchHit[] EMPTY = new SearchHit[0];
private final SearchHit[] hits;
private final TotalHits totalHits;
private final float maxScore;
}
"hits": {
"total": { //对应的java字段为totalHits
"value": 2,
"relation": "eq"
},
"max_score": 1, //对应的java字段为maxScore
"hits": []
}
在toXContent方法中进行的字段名称的替换:注意标注替换的地方
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject("hits");
boolean totalHitAsInt = params.paramAsBoolean("rest_total_hits_as_int", false);
if (totalHitAsInt) {
long total = this.totalHits == null ? -1L : this.totalHits.value;
builder.field("total", total); //替换
} else if (this.totalHits != null) {
builder.startObject("total"); //替换
builder.field("value", this.totalHits.value);
builder.field("relation", this.totalHits.relation == Relation.EQUAL_TO ? "eq" : "gte");
builder.endObject();
}
if (Float.isNaN(this.maxScore)) {
builder.nullField("max_score"); //替换
} else {
builder.field("max_score", this.maxScore); //替换
}
builder.field("hits");
builder.startArray();
SearchHit[] var8 = this.hits;
int var5 = var8.length;
for(int var6 = 0; var6 < var5; ++var6) {
SearchHit hit = var8[var6];
hit.toXContent(builder, params);
}
builder.endArray();
builder.endObject();
return builder;
}
看到这里突然有点兴奋,在ES-client-7.12版本的toXContent方法时,有一个条件判断rest_total_hits_as_int
boolean totalHitAsInt = params.paramAsBoolean("rest_total_hits_as_int", false);
if (totalHitAsInt) {
long total = this.totalHits == null ? -1L : this.totalHits.value;
builder.field("total", total);
} else if (this.totalHits != null) {
builder.startObject("total");
builder.field("value", this.totalHits.value);
builder.field("relation", this.totalHits.relation == Relation.EQUAL_TO ? "eq" : "gte");
builder.endObject();
}
如果rest_total_hits_as_int是ture的话,这不是直接把totalHits.value直接赋值给total了吗?
很惊奇的发现:在HTTP请求上添加了rest_total_hits_as_int=true参数之后,结果报文的total结构确实有改变。那么,我们是不是在客户端调用的时候加上rest_total_hits_as_int参数进行请求就OK了,不用更换包的版本使客户端与服务端版本保持一致。
当我以为不用升级客户端版本了,只需要在High Level Client的api调用中加入上面的那个参数,就能解决问题的时候,“屁颠屁颠”的在High Level Client的api中找添加Http参数调用的接口。
但是。
我们来看下
restHighLevelClient.search源码
public final SearchResponse search(SearchRequest searchRequest, RequestOptions options) throws IOException {
return (SearchResponse)this.performRequestAndParseEntity((ActionRequest)searchRequest, (r) -> {
return RequestConverters.search(r, "_search"); //重点
}, (RequestOptions)options, (CheckedFunction)(SearchResponse::fromXContent), (Set)Collections.emptySet());
}
static Request search(SearchRequest searchRequest, String searchEndpoint) throws IOException {
Request request = new Request("POST", endpoint(searchRequest.indices(), searchRequest.types(), searchEndpoint));
RequestConverters.Params params = new RequestConverters.Params(request);
addSearchRequestParams(params, searchRequest); //重点
if (searchRequest.source() != null) {
request.setEntity(createEntity(searchRequest.source(), REQUEST_BODY_CONTENT_TYPE));
}
return request;
}
private static void addSearchRequestParams(RequestConverters.Params params, SearchRequest searchRequest) {
params.putParam("typed_keys", "true");
params.withRouting(searchRequest.routing());
params.withPreference(searchRequest.preference());
params.withIndicesOptions(searchRequest.indicesOptions());
params.putParam("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT));
params.putParam("ccs_minimize_roundtrips", Boolean.toString(searchRequest.isCcsMinimizeRoundtrips()));
params.putParam("pre_filter_shard_size", Integer.toString(searchRequest.getPreFilterShardSize()));
params.putParam("max_concurrent_shard_requests", Integer.toString(searchRequest.getMaxConcurrentShardRequests()));
if (searchRequest.requestCache() != null) {
params.putParam("request_cache", Boolean.toString(searchRequest.requestCache()));
}
if (searchRequest.allowPartialSearchResults() != null) {
params.putParam("allow_partial_search_results", Boolean.toString(searchRequest.allowPartialSearchResults()));
}
params.putParam("batched_reduce_size", Integer.toString(searchRequest.getBatchedReduceSize()));
if (searchRequest.scroll() != null) {
params.putParam("scroll", searchRequest.scroll().keepAlive());
}
}
通过走读search方法的源码,ES-client在请求的时候,会自己处理一下请求的参数信息,但是没有为用户提供添加参数的API接口,也没有接口来添加Http参数。
在绝望之际,想着要大改,结果查看在ES-github的Pull request中找到答案
地址:https://github.com/elastic/elasticsearch/pull/46076
意思是:在6.8
版本后,把rest_total_hits_as_int
参数加入请求中,请看
到了这里,问题不就简单了?然后我把ES client的版本升级到6.8.4,问题妥妥的解决了,又省了一大把时间。
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.8.4</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.8.4</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>6.8.4</version>
</dependency>
五、总结
- 如果改造不是很大,建议ES client版本随着ES升级,简单粗暴。
- 另外,高版本的ES无法升级解决时,可以使用参数
rest_total_hits_as_int
来让totalHits
字段,仍然以int
格式返回(即:使用es client 6.8以上版本)。 - ES更新很快,一定要注意版本问题带来的坑,最好让集群和客户端使用官方推荐的匹配版本。