Skip to content

Commit 3400483

Browse files
authored
Add date and date_nanos conversion to the numeric_type sort option (#40199) (#40224)
This change adds an option to convert a `date` field to nanoseconds resolution and a `date_nanos` field to millisecond resolution when sorting. The resolution of the sort can be set using the `numeric_type` option of the field sort builder. The conversion is done at the shard level and is restricted to dates from 1970 to 2262 for the nanoseconds resolution in order to avoid numeric overflow.
1 parent 5eb33f2 commit 3400483

File tree

9 files changed

+305
-48
lines changed

9 files changed

+305
-48
lines changed

docs/reference/search/request/sort.asciidoc

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ POST /_search
112112

113113
For numeric fields it is also possible to cast the values from one type
114114
to another using the `numeric_type` option.
115-
This option accepts the following values: [`"double", "long"`] and can be useful
116-
for cross-index search if the sort field is mapped differently on some
115+
This option accepts the following values: [`"double", "long", "date", "date_nanos"`]
116+
and can be useful for cross-index search if the sort field is mapped differently on some
117117
indices.
118118

119119
Consider for instance these two indices:
@@ -175,6 +175,63 @@ but note that in this case floating points are replaced by the largest
175175
value that is less than or equal (greater than or equal if the value
176176
is negative) to the argument and is equal to a mathematical integer.
177177

178+
This option can also be used to convert a `date` field that uses millisecond
179+
resolution to a `date_nanos` field with nanosecond resolution.
180+
Consider for instance these two indices:
181+
182+
[source,js]
183+
--------------------------------------------------
184+
PUT /index_double
185+
{
186+
"mappings": {
187+
"properties": {
188+
"field": { "type": "date" }
189+
}
190+
}
191+
}
192+
--------------------------------------------------
193+
// CONSOLE
194+
195+
[source,js]
196+
--------------------------------------------------
197+
PUT /index_long
198+
{
199+
"mappings": {
200+
"properties": {
201+
"field": { "type": "date_nanos" }
202+
}
203+
}
204+
}
205+
--------------------------------------------------
206+
// CONSOLE
207+
// TEST[continued]
208+
209+
Values in these indices are stored with different resolutions so sorting on these
210+
fields will always sort the `date` before the `date_nanos` (ascending order).
211+
With the `numeric_type` type option it is possible to set a single resolution for
212+
the sort, setting to `date` will convert the `date_nanos` to the millisecond resolution
213+
while `date_nanos` will convert the values in the `date` field to the nanoseconds resolution:
214+
215+
[source,js]
216+
--------------------------------------------------
217+
POST /index_long,index_double/_search
218+
{
219+
"sort" : [
220+
{
221+
"field" : {
222+
"numeric_type" : "date_nanos"
223+
}
224+
}
225+
]
226+
}
227+
--------------------------------------------------
228+
// CONSOLE
229+
// TEST[continued]
230+
231+
[WARNING]
232+
To avoid overflow, the conversion to `date_nanos` cannot be applied on dates before
233+
1970 and after 2262 as nanoseconds are represented as longs.
234+
178235
[[nested-sorting]]
179236
==== Sorting within nested objects.
180237

server/src/main/java/org/elasticsearch/common/time/DateUtils.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public static ZoneId of(String zoneId) {
8989

9090
private static final Instant MAX_NANOSECOND_INSTANT = Instant.parse("2262-04-11T23:47:16.854775807Z");
9191

92+
static final long MAX_NANOSECOND_IN_MILLIS = MAX_NANOSECOND_INSTANT.toEpochMilli();
93+
9294
/**
9395
* convert a java time instant to a long value which is stored in lucene
9496
* the long value resembles the nanoseconds since the epoch
@@ -117,7 +119,7 @@ public static long toLong(Instant instant) {
117119
*/
118120
public static Instant toInstant(long nanoSecondsSinceEpoch) {
119121
if (nanoSecondsSinceEpoch < 0) {
120-
throw new IllegalArgumentException("nanoseconds are [" + nanoSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " +
122+
throw new IllegalArgumentException("nanoseconds [" + nanoSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " +
121123
"be processed in nanosecond resolution");
122124
}
123125
if (nanoSecondsSinceEpoch == 0) {
@@ -129,6 +131,24 @@ public static Instant toInstant(long nanoSecondsSinceEpoch) {
129131
return Instant.ofEpochSecond(seconds, nanos);
130132
}
131133

134+
/**
135+
* Convert a nanosecond timestamp in milliseconds
136+
*
137+
* @param milliSecondsSinceEpoch the millisecond since the epoch
138+
* @return the nanoseconds since the epoch
139+
*/
140+
public static long toNanoSeconds(long milliSecondsSinceEpoch) {
141+
if (milliSecondsSinceEpoch < 0) {
142+
throw new IllegalArgumentException("milliSeconds [" + milliSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " +
143+
"be converted to nanoseconds");
144+
} else if (milliSecondsSinceEpoch > MAX_NANOSECOND_IN_MILLIS) {
145+
throw new IllegalArgumentException("milliSeconds [" + milliSecondsSinceEpoch + "] are after 2262-04-11T23:47:16.854775807 " +
146+
"and cannot be converted to nanoseconds");
147+
}
148+
149+
return milliSecondsSinceEpoch * 1_000_000;
150+
}
151+
132152
/**
133153
* Convert a nanosecond timestamp in milliseconds
134154
*
@@ -137,7 +157,7 @@ public static Instant toInstant(long nanoSecondsSinceEpoch) {
137157
*/
138158
public static long toMilliSeconds(long nanoSecondsSinceEpoch) {
139159
if (nanoSecondsSinceEpoch < 0) {
140-
throw new IllegalArgumentException("nanoseconds are [" + nanoSecondsSinceEpoch + "] are before the epoch in 1970 and will " +
160+
throw new IllegalArgumentException("nanoseconds are [" + nanoSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " +
141161
"be converted to milliseconds");
142162
}
143163

server/src/main/java/org/elasticsearch/index/fielddata/fieldcomparator/LongValuesComparatorSource.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,53 @@
2626
import org.apache.lucene.search.SortField;
2727
import org.apache.lucene.util.BitSet;
2828
import org.elasticsearch.common.Nullable;
29+
import org.elasticsearch.index.fielddata.AtomicNumericFieldData;
2930
import org.elasticsearch.index.fielddata.FieldData;
3031
import org.elasticsearch.index.fielddata.IndexFieldData;
3132
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
33+
import org.elasticsearch.index.fielddata.plain.SortedNumericDVIndexFieldData;
3234
import org.elasticsearch.search.MultiValueMode;
3335

3436
import java.io.IOException;
37+
import java.util.function.Function;
3538

3639
/**
3740
* Comparator source for long values.
3841
*/
3942
public class LongValuesComparatorSource extends IndexFieldData.XFieldComparatorSource {
4043

4144
private final IndexNumericFieldData indexFieldData;
45+
private final Function<SortedNumericDocValues, SortedNumericDocValues> converter;
4246

43-
public LongValuesComparatorSource(IndexNumericFieldData indexFieldData, @Nullable Object missingValue, MultiValueMode sortMode,
44-
Nested nested) {
47+
public LongValuesComparatorSource(IndexNumericFieldData indexFieldData, @Nullable Object missingValue,
48+
MultiValueMode sortMode, Nested nested) {
49+
this(indexFieldData, missingValue, sortMode, nested, null);
50+
}
51+
52+
public LongValuesComparatorSource(IndexNumericFieldData indexFieldData, @Nullable Object missingValue,
53+
MultiValueMode sortMode, Nested nested,
54+
Function<SortedNumericDocValues, SortedNumericDocValues> converter) {
4555
super(missingValue, sortMode, nested);
4656
this.indexFieldData = indexFieldData;
57+
this.converter = converter;
4758
}
4859

4960
@Override
5061
public SortField.Type reducedType() {
5162
return SortField.Type.LONG;
5263
}
5364

65+
private SortedNumericDocValues loadDocValues(LeafReaderContext context) {
66+
final AtomicNumericFieldData data = indexFieldData.load(context);
67+
SortedNumericDocValues values;
68+
if (data instanceof SortedNumericDVIndexFieldData.NanoSecondFieldData) {
69+
values = ((SortedNumericDVIndexFieldData.NanoSecondFieldData) data).getLongValuesAsNanos();
70+
} else {
71+
values = data.getLongValues();
72+
}
73+
return converter != null ? converter.apply(values) : values;
74+
}
75+
5476
@Override
5577
public FieldComparator<?> newComparator(String fieldname, int numHits, int sortPos, boolean reversed) {
5678
assert indexFieldData == null || fieldname.equals(indexFieldData.getFieldName());
@@ -61,7 +83,7 @@ public FieldComparator<?> newComparator(String fieldname, int numHits, int sortP
6183
return new FieldComparator.LongComparator(numHits, null, null) {
6284
@Override
6385
protected NumericDocValues getNumericDocValues(LeafReaderContext context, String field) throws IOException {
64-
final SortedNumericDocValues values = indexFieldData.load(context).getLongValues();
86+
final SortedNumericDocValues values = loadDocValues(context);
6587
final NumericDocValues selectedValues;
6688
if (nested == null) {
6789
selectedValues = FieldData.replaceMissing(sortMode.select(values), dMissingValue);

server/src/main/java/org/elasticsearch/index/fielddata/plain/SortedNumericDVIndexFieldData.java

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.io.IOException;
4949
import java.util.Collection;
5050
import java.util.Collections;
51+
import java.util.function.LongUnaryOperator;
5152

5253
/**
5354
* FieldData backed by {@link LeafReader#getSortedNumericDocValues(String)}
@@ -69,8 +70,8 @@ public SortedNumericDVIndexFieldData(Index index, String fieldNames, NumericType
6970
* Values are casted to the provided <code>targetNumericType</code> type if it doesn't
7071
* match the field's <code>numericType</code>.
7172
*/
72-
public SortField sortField(NumericType targetNumericType, Object missingValue, MultiValueMode sortMode, Nested nested,
73-
boolean reverse) {
73+
public SortField sortField(NumericType targetNumericType, Object missingValue, MultiValueMode sortMode,
74+
Nested nested, boolean reverse) {
7475
final XFieldComparatorSource source;
7576
switch (targetNumericType) {
7677
case HALF_FLOAT:
@@ -82,6 +83,26 @@ public SortField sortField(NumericType targetNumericType, Object missingValue, M
8283
source = new DoubleValuesComparatorSource(this, missingValue, sortMode, nested);
8384
break;
8485

86+
case DATE:
87+
if (numericType == NumericType.DATE_NANOSECONDS) {
88+
// converts date values to nanosecond resolution
89+
source = new LongValuesComparatorSource(this, missingValue,
90+
sortMode, nested, dvs -> convertNanosToMillis(dvs));
91+
} else {
92+
source = new LongValuesComparatorSource(this, missingValue, sortMode, nested);
93+
}
94+
break;
95+
96+
case DATE_NANOSECONDS:
97+
if (numericType == NumericType.DATE) {
98+
// converts date_nanos values to millisecond resolution
99+
source = new LongValuesComparatorSource(this, missingValue,
100+
sortMode, nested, dvs -> convertMillisToNanos(dvs));
101+
} else {
102+
source = new LongValuesComparatorSource(this, missingValue, sortMode, nested);
103+
}
104+
break;
105+
85106
default:
86107
assert !targetNumericType.isFloatingPoint();
87108
source = new LongValuesComparatorSource(this, missingValue, sortMode, nested);
@@ -93,9 +114,9 @@ public SortField sortField(NumericType targetNumericType, Object missingValue, M
93114
* returns a custom sort field otherwise.
94115
*/
95116
if (nested != null
96-
|| (sortMode != MultiValueMode.MAX && sortMode != MultiValueMode.MIN)
97-
|| numericType == NumericType.HALF_FLOAT
98-
|| targetNumericType != numericType) {
117+
|| (sortMode != MultiValueMode.MAX && sortMode != MultiValueMode.MIN)
118+
|| numericType == NumericType.HALF_FLOAT
119+
|| targetNumericType != numericType) {
99120
return new SortField(fieldName, source, reverse);
100121
}
101122

@@ -171,29 +192,7 @@ public final class NanoSecondFieldData extends AtomicLongFieldData {
171192

172193
@Override
173194
public SortedNumericDocValues getLongValues() {
174-
final SortedNumericDocValues dv = getLongValuesAsNanos();
175-
return new AbstractSortedNumericDocValues() {
176-
177-
@Override
178-
public boolean advanceExact(int target) throws IOException {
179-
return dv.advanceExact(target);
180-
}
181-
182-
@Override
183-
public long nextValue() throws IOException {
184-
return DateUtils.toMilliSeconds(dv.nextValue());
185-
}
186-
187-
@Override
188-
public int docValueCount() {
189-
return dv.docValueCount();
190-
}
191-
192-
@Override
193-
public int nextDoc() throws IOException {
194-
return dv.nextDoc();
195-
}
196-
};
195+
return convertNanosToMillis(getLongValuesAsNanos());
197196
}
198197

199198
public SortedNumericDocValues getLongValuesAsNanos() {
@@ -463,4 +462,47 @@ public Collection<Accountable> getChildResources() {
463462
return Collections.emptyList();
464463
}
465464
}
465+
466+
/**
467+
* Convert the values in <code>dvs</code> from nanosecond to millisecond resolution.
468+
*/
469+
static SortedNumericDocValues convertNanosToMillis(SortedNumericDocValues dvs) {
470+
return convertNumeric(dvs, DateUtils::toMilliSeconds);
471+
}
472+
473+
/**
474+
* Convert the values in <code>dvs</code> from millisecond to nanosecond resolution.
475+
*/
476+
static SortedNumericDocValues convertMillisToNanos(SortedNumericDocValues values) {
477+
return convertNumeric(values, DateUtils::toNanoSeconds);
478+
}
479+
480+
/**
481+
* Convert the values in <code>dvs</code> using the provided <code>converter</code>.
482+
*/
483+
private static SortedNumericDocValues convertNumeric(SortedNumericDocValues values, LongUnaryOperator converter) {
484+
return new AbstractSortedNumericDocValues() {
485+
486+
@Override
487+
public boolean advanceExact(int target) throws IOException {
488+
return values.advanceExact(target);
489+
}
490+
491+
@Override
492+
public long nextValue() throws IOException {
493+
return converter.applyAsLong(values.nextValue());
494+
}
495+
496+
@Override
497+
public int docValueCount() {
498+
return values.docValueCount();
499+
}
500+
501+
@Override
502+
public int nextDoc() throws IOException {
503+
return values.nextDoc();
504+
}
505+
};
506+
}
507+
466508
}

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public Builder format(String format) {
176176
return this;
177177
}
178178

179-
Builder withResolution(Resolution resolution) {
179+
public Builder withResolution(Resolution resolution) {
180180
this.resolution = resolution;
181181
return this;
182182
}

server/src/main/java/org/elasticsearch/search/sort/FieldSortBuilder.java

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,16 +304,19 @@ public String getNumericType() {
304304
* Allowed values are <code>long</code> and <code>double</code>.
305305
*/
306306
public FieldSortBuilder setNumericType(String numericType) {
307-
String upperCase = numericType.toUpperCase(Locale.ENGLISH);
308-
switch (upperCase) {
309-
case "LONG":
310-
case "DOUBLE":
307+
String lowerCase = numericType.toLowerCase(Locale.ENGLISH);
308+
switch (lowerCase) {
309+
case "long":
310+
case "double":
311+
case "date":
312+
case "date_nanos":
311313
break;
312314

313315
default:
314-
throw new IllegalArgumentException("invalid value for [numeric_type], must be [LONG, DOUBLE], got " + numericType);
316+
throw new IllegalArgumentException("invalid value for [numeric_type], " +
317+
"must be [long, double, date, date_nanos], got " + lowerCase);
315318
}
316-
this.numericType = upperCase;
319+
this.numericType = lowerCase;
317320
return this;
318321
}
319322

@@ -348,6 +351,23 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
348351
return builder;
349352
}
350353

354+
private static NumericType resolveNumericType(String value) {
355+
switch (value) {
356+
case "long":
357+
return NumericType.LONG;
358+
case "double":
359+
return NumericType.DOUBLE;
360+
case "date":
361+
return NumericType.DATE;
362+
case "date_nanos":
363+
return NumericType.DATE_NANOSECONDS;
364+
365+
default:
366+
throw new IllegalArgumentException("invalid value for [numeric_type], " +
367+
"must be [long, double, date, date_nanos], got " + value);
368+
}
369+
}
370+
351371
@Override
352372
public SortFieldAndFormat build(QueryShardContext context) throws IOException {
353373
if (DOC_FIELD_NAME.equals(fieldName)) {
@@ -404,7 +424,7 @@ public SortFieldAndFormat build(QueryShardContext context) throws IOException {
404424
"[numeric_type] option cannot be set on a non-numeric field, got " + fieldType.typeName());
405425
}
406426
SortedNumericDVIndexFieldData numericFieldData = (SortedNumericDVIndexFieldData) fieldData;
407-
NumericType resolvedType = NumericType.valueOf(numericType);
427+
NumericType resolvedType = resolveNumericType(numericType);
408428
field = numericFieldData.sortField(resolvedType, missing, localSortMode, nested, reverse);
409429
} else {
410430
field = fieldData.sortField(missing, localSortMode, nested, reverse);

0 commit comments

Comments
 (0)