• ES查询时,通过response.getHits().getTotalHits()获取总条目结果始终为0


    1. 问题描述

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

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从以上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;
    
    • 1
    • 2

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    "hits": {
        "total": 10,    //对应的java字段为totalHits
        "max_score": 1, //对应的java字段为maxScore
        "hits": [ ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    "hits": {
        "total": {  //对应的java字段为totalHits
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1, //对应的java字段为maxScore
        "hits": []
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    重点来了,在分析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();
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    从代码中可以看到如果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());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    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());
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    通过走读search方法的源码,ES-client在请求的时候,会自己处理一下请求的参数信息,但是没有为用户提供添加参数的API接口。
    
    • 1

    是不是无路可走了/(ㄒ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 发送的搜索请求,但我们已经在文档中警告说客户端仅向前兼容
    
    • 1
    • 2

    经尝试,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());
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

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

    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()
  • 相关阅读:
    网关、网桥、路由器和交换机之【李逵与李鬼】
    驾驶证识别易语言代码
    uniapp基础学习笔记01
    NLP - GIZA++ 实现词对齐
    【微服务的集成测试】python实现-附ChatGPT解析
    A Philosophy of Software Design读书笔记——异常处理
    【C++】C++ 语言对 C 语言的加强 ④ ( C 语言中的三目运算符 - 不能作为左值 | C++ 语言中的三目运算符增强 | C 语言中三目运算符作为左值使用 )
    【驱动】块设备驱动(一)-驱动框架
    接口测试1
    Spring中加密工具类DigestUtils和BCryptPasswordEncoder
  • 原文地址:https://blog.csdn.net/hdn_kb/article/details/126179863