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

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


我们知道elasticsearch可以通过SearchAfter的方式解决深分页问题。SearchAfter原理就是通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景。

目录

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相关知识的朋友,可持续关注老王,后续精彩继续!

31戒烟网