diff --git a/docs/changelog/135899.yaml b/docs/changelog/135899.yaml new file mode 100644 index 0000000000000..680a98a5d6b5e --- /dev/null +++ b/docs/changelog/135899.yaml @@ -0,0 +1,6 @@ +pr: 135899 +summary: Fix date fields sort formatting with missing values +area: Search +type: bug +issues: + - 81960 diff --git a/server/src/main/java/org/elasticsearch/search/DocValueFormat.java b/server/src/main/java/org/elasticsearch/search/DocValueFormat.java index 13256a2481867..31c2659f9d840 100644 --- a/server/src/main/java/org/elasticsearch/search/DocValueFormat.java +++ b/server/src/main/java/org/elasticsearch/search/DocValueFormat.java @@ -35,7 +35,12 @@ import java.text.NumberFormat; import java.text.ParseException; import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Base64; import java.util.Locale; @@ -305,6 +310,14 @@ public DateMathParser getDateMathParser() { @Override public String format(long value) { + // Handle missing values, represented as Long.MIN_VALUE or Long.MAX_VALUE depending on the sort order + if (value == Long.MIN_VALUE) { + return formatter.withZone(ZoneOffset.ofHours(0)).format(Instant.EPOCH); + } else if (value == Long.MAX_VALUE) { + // On descending values, use the maximum possible value for strict_date_time format as the safest option + return formatter.withZone(ZoneOffset.ofHours(0)).format(LocalDateTime.of(LocalDate.of(9999, 12, 31), LocalTime.MAX)); + } + try { return formatter.format(resolution.toInstant(value).atZone(timeZone)); } catch (DateTimeException dte) { diff --git a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java index 7c9a68cbc91f1..208d837ed7f3b 100644 --- a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java +++ b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.time.FormatNames; import org.elasticsearch.index.mapper.DateFieldMapper.Resolution; import org.elasticsearch.index.mapper.RoutingPathFields; import org.elasticsearch.index.mapper.TimeSeriesRoutingHashFieldMapper; @@ -238,6 +239,28 @@ public void testFormatSortFieldOutput() { assertThat(dateFormat.formatSortValue(1415580798601L), equalTo("2014-11-10 01:53:18")); } + public void testFormatSortFieldOutputForMissingValues() { + DateFormatter formatter = DateFormatter.forPattern(FormatNames.STRICT_DATE_TIME.getName()); + DocValueFormat.DateTime dateFormat = new DocValueFormat.DateTime( + formatter, + ZoneOffset.ofHours(randomInt(6)), + randomBoolean() ? Resolution.MILLISECONDS : Resolution.NANOSECONDS + ); + dateFormat = (DocValueFormat.DateTime) DocValueFormat.enableFormatSortValues(dateFormat); + assertThat(dateFormat.formatSortValue(Long.MIN_VALUE), equalTo("1970-01-01T00:00:00.000Z")); + assertThat(dateFormat.formatSortValue(Long.MAX_VALUE), equalTo("9999-12-31T23:59:59.999999999Z")); + + formatter = DateFormatter.forPattern(FormatNames.EPOCH_MILLIS.getName()); + dateFormat = new DocValueFormat.DateTime( + formatter, + ZoneOffset.ofHours(randomInt(6)), + randomBoolean() ? Resolution.MILLISECONDS : Resolution.NANOSECONDS + ); + dateFormat = (DocValueFormat.DateTime) DocValueFormat.enableFormatSortValues(dateFormat); + assertThat(dateFormat.formatSortValue(Long.MIN_VALUE), equalTo("0")); + assertThat(dateFormat.formatSortValue(Long.MAX_VALUE), equalTo("253402300799999.999999")); + } + public void testBadUtf8() { IllegalArgumentException e = expectThrows( IllegalArgumentException.class,