1. 问题描述

ES-server版本为7版本,ES-client版本为6.3版本。在进行分页查询时获取总条目数一直为0:

SearchResponse response = restHighLevelClient.search(searchRequest);
long total = response.getHits().getTotalHits();

total得到的结果一直时0。

2. 问题排查

既然使用Java api查询总条目数有问题,那可以直接使用http从ES集群中进行查询。

以下提到的system_log_index索引是6.3版本、bate_sys_operate_log_index是7版本,以此进行对比分析。

2.1. 使用http从es中分别进行查询

在这里插入图片描述
从图中可以看到ES-server版本为7.x中的total是有数据的且大于0,但是对比ES-server版本为6.3中的total数据结构有所不同。

ES-server-7.x版本:response.getHits().getTotalHits()返回的是一个对象,除了有value标识总条目数,还有relation字段。
ES-server-6.3版本:response.getHits().getTotalHits()返回的直接就是总条目数total。

既然服务端返回的数据结构不一致,那么我们接下来查看Java客户端是如何处理的。

2.2. Java客户端源码分析

查看客户端 response.getHits() 返回的SearchHits源码。

2.2.1. SearchHits源码

6.3版本

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.x版本

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.3版本在进行JSON转化时,由于服务端使用的时ES-Server-7.x版本,不能拿到正常的值,默认返回了的值为0。

截止到此,小伙伴们是不是看到了光(●’◡’●)。
我们已经知道了为什么在获取总条目时得到的结果始终是0了。
那如何解决呢?继续追随着光寻找吧

3. 如何解决

3.1. 简单粗暴法

直接撸起袖子去更换ES-Java客户端版本,与服务端保持一致。

使用以下方式获取条目总数:

SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
response.getHits().getTotalHits().value;

3.2. 一探究竟

细心的小伙伴有没有发现服务端返回的JSON数据结构和客户端的字段是不一致的,究竟怎么不一致,为什么不一致,我们继续来探索吧。

3.2.1. 客户端与服务端字段对比

我们都知道在JSON反序列化时,需要JSON中key应该和对象的字段名一一对应才可以,那ES客户端时如何处理的呢?具体是怎么转化的?以下是ES-client-6.3版本与ES-client-7.x版本的分析。

ES-client 6.3

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-client 7.3

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.x版本的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的话,则total就以long类型返回了。

使用HTTP接口加上rest_total_hits_as_int参数试一试,看报文中的total结构是否有改变:
在这里插入图片描述
很惊奇的发现:在HTTP请求上添加了rest_total_hits_as_int=true参数之后,结果报文的total结构确实有改变。那么,我们是不是在客户端调用的时候加上rest_total_hits_as_int参数进行请求就OK了,不用更换包的版本使客户端与服务端版本保持一致。

理想很丰满,显示很残酷
ES-client并没有为我们提供添加HTTP参数的接口。那HTTP接口参数使如何处理的呢?我们继续查看restHighLevelClient.search()的源码。

3.2.1.1. 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接口。

是不是无路可走了/(ㄒoㄒ)/~~,我们必须使用简单粗暴的方法。不要放弃,成功就在眼前!!!!!

3.2.1.2. ES的github中找答案

在ES-github的Pull request中找到答案:请点击
在这里插入图片描述
译文:

此提交向所有 HLRC 的搜索请求添加了一个名为 rest_total_hits_as_int 的查询参数
支持它(_search,_msearch,...)。这使得 HLRC 客户端的搜索与版本 >= 6.6.0 中的任何节点兼容,因为在 6.6.0 中添加了 rest_total_hits_as_int。这意味着版本 < 6.6.0 中的节点将无法处理版本 6.8.3 中的 HLRC 发送的搜索请求,但我们已经在文档中警告说客户端仅向前兼容

经尝试,ES在6.8.3版本开始,已经将rest_total_hits_as_int=true参数加入到请求当中:

private static void addSearchRequestParams(RequestConverters.Params params, SearchRequest searchRequest) {
        params.putParam("typed_keys", "true");
        params.putParam("rest_total_hits_as_int", "true");  //重点要看
        params.withRouting(searchRequest.routing());
        params.withPreference(searchRequest.preference());
        params.withIndicesOptions(searchRequest.indicesOptions());
        params.putParam("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT));
        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());
        }

    }

所以,我们将ES-client版本更换为6.8.3即可。这样获取总条目的方式与6.3版本一致:

SearchResponse response = restHighLevelClient.search(searchRequest);
long total = response.getHits().getTotalHits();

4. 总结

E是客户端版本:6.3版本->7.x版本->6.8.3版本

ES-server使用7.x版本,获取总条目的方式:

  1. ES-Client使用6.3版本——得到的total为0
  2. ES-Client使用与ES-Server一致的版本:response.getHits().getTotalHits().value
  3. ES-Client使用6.8.3版本:response.getHits().getTotalHits()
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐