0
点赞
收藏
分享

微信扫一扫

[5.1]帮学弟解决了一个生产环境中ES深分页SearchAfter Date类型字段排序的问题-这个坑你早晚会遇到

豆丁趣 2022-05-02 阅读 60

目录

1、案发现场 

2、初步推断

3、亮出杀手锏

3.1  SearchAfter调试过程

3.1.1 测试索引

3.1.2 测试方法

3.2 跟踪关键代码 

3.2.1 重大发现

3.2.2 按图索骥

4、解决方案

方案一:将排序的字符串或Date类型转化为毫秒

方案二:直接从Hits返回数据中获取


1、案发现场 

前两天有一个学弟,找我解决了一个关于ES深分页SearchAfter支持Date类型排序的问题。

报错信息如下:

  ES源码SearhAfter不支持日期排序吗? 对这一点我表示怀疑。୧(๑•̀◡•́๑)૭

2、初步推断

我找学弟看了下ES定义的createTime类型,如下:

从异常异常信息提示看,应该是String类型无法转化Date类型导致的。

当时的第一反应是,把传入的字符串转化为Date类型,这样不就能支持了吗?

~~ 报错依然存在......

当有排序字段时,ES的SearchAfter会执行这么一段代码:

   public SearchAfterBuilder setSortValues(Object[] values) {
        if (values == null) {
            throw new NullPointerException("Values cannot be null.");
        } else if (values.length == 0) {
            throw new IllegalArgumentException("Values must contains at least one value.");
        } else {
            for(int i = 0; i < values.length; ++i) {
                if (values[i] != null 
                && !(values[i] instanceof String) 
                && !(values[i] instanceof Text) 
                && !(values[i] instanceof Long) 
                && !(values[i] instanceof Integer) 
                && !(values[i] instanceof Short) 
                && !(values[i] instanceof Byte) 
                && !(values[i] instanceof Double) 
                && !(values[i] instanceof Float) 
                && !(values[i] instanceof Boolean)) {
                    throw new IllegalArgumentException("Can't handle " + SEARCH_AFTER + " field value of type [" + values[i].getClass() + "]");
                }
            }

            this.sortValues = new Object[values.length];
            System.arraycopy(values, 0, this.sortValues, 0, values.length);
            return this;
        }
    }

我们会发现,for循环中只有String、Text、Long、Integer、Short、Byte、Double、Float和Boolean这9种类型。

看来转Date这条路行不通,似乎一切显示这个错误出现的又是那么的理所当然~~😡😡😡

好吧,看看换个别的思路能不能发现什么线索。

3、亮出杀手锏

作为一名老司机,根据多年玩ES的经验,这个问题不简单。那就直接祭出杀手锏吧~~

撸Demo代码并调试跟踪 🌶🌶🌶。

具体 Elasticsearch CRUD 示例在下一篇文章会讲到。今天我们先来说下关键问题点以及解决方案。

3.1  SearchAfter调试过程

3.1.1 测试索引

{
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword"
      },
      "createTime": {
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis",
        "type": "date"
      }
    }
  }
}

3.1.2 测试方法

public static void searchAfter(String indexName) throws IOException {
        SearchRequest request = new SearchRequest(indexName);
        //构建搜索条件
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.from(0);
        builder.size(10);
        builder.timeout(new TimeValue(60, TimeUnit.SECONDS));
        builder.sort("createTime", SortOrder.DESC);
        request.source(builder);
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        SearchHit[] searchHits = response.getHits().getHits();
        while (null != searchHits && searchHits.length > 0) {
            for (SearchHit searchHit : searchHits) {
                System.out.println(searchHit.getSourceAsMap());
            }
            SearchHit last = searchHits[searchHits.length - 1];
            builder = builder.searchAfter(last.getSortValues());
            response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits().getHits();
        }
    }

3.2 跟踪关键代码 

看代码: SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);

说明:此处使用了ES 高级API。

3.2.1 重大发现

我发现DBug的内容中,除了返回createTime的字符串值(2022-05-02 12:55:09)之外,外层节点还有一个sort字段值数组 ~~ 一大喜讯 🎉🎉🎉。

返回内容竟然:把date字符串转化为了long类型的时间戳,好神奇。

如果这个时间戳能用起来,前面的那段ES源码 setSortValues方法不就可以用了吗?

此时,我们还不行直接

惊讶过后,我们还得一探究竟:看看ES是在什么位置做了这么友好的转化?

这个sort字段目前就是我们要重点研究的对象了。

3.2.2 按图索骥

date 字段被转为毫秒当作排序依据。于是我继续跟踪源码。

 关键代码位置: RestHighLevelClient.internalPerformRequest。

在此位置使用了Apache的HttpResponse对象,直接用工具类看看从服务端返回的内容是什么?EntityUtils.toString(response.getEntity())。

 原来是在服务器端,就对这个sort字段进行了处理 :ES服务端会对排序字段进行转化:date 字段会被转为毫秒。

为什么说是服务端呢?

我们从管理端直接查询,看返回内容是否一样?

 查询条件

GET /_search

{
    "query" : {
        "filtered" : {
            "filter" : { "term" : { "name" : "search_after1"}}
         }
    },
    "sort": { "createTime": { "order": "desc" }}
}

查询结果

{
  "hits": {
    "total": {
      "value": 1
    },
    "hits": [
      {
        "_index": "test_search_after",
        "_type": "_doc",
        "_id": "1",
        "_score": null,
        "_source": {
          "createTime": "2022-05-02 12:55:09",
          "name": "search_after1",
          "id": "1"
        },
        "sort": [
          1651496109000
        ]
      }
    ]
  }
}

果真是从服务器端返回的数据。(待后续相关ES文章老王会深究服务端的具体实现~~暂且不表🤗🤗🤗)

那从上面的分析,解决方案就不言而喻了。

4、解决方案

方案一:将排序的字符串或Date类型转化为毫秒

给大家提供一个字符串转毫秒的工具类。(针对已经是字符串的代码)

public static Long dateToStamp(String s) throws ParseException{
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss");
        Date date = simpleDateFormat.parse(s);
        return date.getTime();
}

方案二:直接从Hits返回数据中获取

 SearchHit last = searchHits[searchHits.length - 1]; 

 builder.searchAfter(last.getSortValues()); 

👏🏻👏🏻👏🏻  完毕!

如果有需要深入学习ES相关知识的朋友,可持续关注老王,后续精彩继续!

举报

相关推荐

0 条评论