[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、解决方案
方案二:直接从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相关知识的朋友,可持续关注老王,后续精彩继续!