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