Skip to content

Commit d8ea894

Browse files
authored
Skip iterating DISI when reading metric values (elastic#133365)
Avoids iterating the DISI when reading a sparse metric field if its nulls were already filtered by a WHERE clause (e.g., metrics != NULL). Iterating the DISI on a sparse field can be expensive. From my local benchmark, the query time of ``` TS my* | WHERE `metrics.system.memory.utilization` IS NOT NULL AND @timestamp >= \"2025-07-25T14:55:59.000Z\" AND @timestamp <= \"2025-07-25T16:25:59.000Z\" | STATS AVG(AVG_OVER_TIME(`metrics.system.memory.utilization`)) BY host.name, BUCKET(@timestamp, 1h) | LIMIT 10000" ``` decreased from 35ms to 27ms (20%).
1 parent a130929 commit d8ea894

File tree

10 files changed

+213
-76
lines changed

10 files changed

+213
-76
lines changed

docs/changelog/133365.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 133365
2+
summary: Skip iterating DISI when reading metric values
3+
area: Codec
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/index/codec/tsdb/es819/ES819TSDBDocValuesProducer.java

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.lucene.util.compress.LZ4;
4444
import org.apache.lucene.util.packed.DirectMonotonicReader;
4545
import org.apache.lucene.util.packed.PackedInts;
46+
import org.elasticsearch.core.Assertions;
4647
import org.elasticsearch.core.IOUtils;
4748
import org.elasticsearch.index.codec.tsdb.TSDBDocValuesEncoder;
4849
import org.elasticsearch.index.mapper.BlockDocValuesReader;
@@ -388,6 +389,7 @@ public BlockLoader.Block tryRead(
388389
BlockLoader.BlockFactory factory,
389390
BlockLoader.Docs docs,
390391
int offset,
392+
boolean nullsFiltered,
391393
BlockDocValuesReader.ToDouble toDouble
392394
) throws IOException {
393395
assert toDouble == null;
@@ -468,6 +470,7 @@ public BlockLoader.Block tryRead(
468470
BlockLoader.BlockFactory factory,
469471
BlockLoader.Docs docs,
470472
int offset,
473+
boolean nullsFiltered,
471474
BlockDocValuesReader.ToDouble toDouble
472475
) throws IOException {
473476
return null;
@@ -520,6 +523,7 @@ public BlockLoader.Block tryRead(
520523
BlockLoader.BlockFactory factory,
521524
BlockLoader.Docs docs,
522525
int offset,
526+
boolean nullsFiltered,
523527
BlockDocValuesReader.ToDouble toDouble
524528
) throws IOException {
525529
return null;
@@ -532,7 +536,7 @@ BlockLoader.Block tryRead(BlockLoader.SingletonLongBuilder builder, BlockLoader.
532536
}
533537
}
534538

535-
abstract static class BaseSparseNumericValues extends NumericDocValues {
539+
abstract static class BaseSparseNumericValues extends NumericDocValues implements BlockLoader.OptionalColumnAtATimeReader {
536540
protected final IndexedDISI disi;
537541

538542
BaseSparseNumericValues(IndexedDISI disi) {
@@ -563,6 +567,17 @@ public final int docID() {
563567
public final long cost() {
564568
return disi.cost();
565569
}
570+
571+
@Override
572+
public BlockLoader.Block tryRead(
573+
BlockLoader.BlockFactory factory,
574+
BlockLoader.Docs docs,
575+
int offset,
576+
boolean nullsFiltered,
577+
BlockDocValuesReader.ToDouble toDouble
578+
) throws IOException {
579+
return null;
580+
}
566581
}
567582

568583
abstract static class BaseSortedSetDocValues extends SortedSetDocValues {
@@ -1386,16 +1401,11 @@ public BlockLoader.Block tryRead(
13861401
BlockLoader.BlockFactory factory,
13871402
BlockLoader.Docs docs,
13881403
int offset,
1404+
boolean nullsFiltered,
13891405
BlockDocValuesReader.ToDouble toDouble
13901406
) throws IOException {
1391-
if (toDouble != null) {
1392-
try (BlockLoader.SingletonDoubleBuilder builder = factory.singletonDoubles(docs.count() - offset)) {
1393-
SingletonLongToDoubleDelegate delegate = new SingletonLongToDoubleDelegate(builder, toDouble);
1394-
return tryRead(delegate, docs, offset);
1395-
}
1396-
}
1397-
try (BlockLoader.SingletonLongBuilder builder = factory.singletonLongs(docs.count() - offset)) {
1398-
return tryRead(builder, docs, offset);
1407+
try (var singletonLongBuilder = singletonLongBuilder(factory, toDouble, docs.count() - offset)) {
1408+
return tryRead(singletonLongBuilder, docs, offset);
13991409
}
14001410
}
14011411

@@ -1484,6 +1494,7 @@ static boolean isDense(int firstDocId, int lastDocId, int length) {
14841494
);
14851495
return new BaseSparseNumericValues(disi) {
14861496
private final TSDBDocValuesEncoder decoder = new TSDBDocValuesEncoder(ES819TSDBDocValuesFormat.NUMERIC_BLOCK_SIZE);
1497+
private IndexedDISI lookAheadDISI;
14871498
private long currentBlockIndex = -1;
14881499
private final long[] currentBlock = new long[ES819TSDBDocValuesFormat.NUMERIC_BLOCK_SIZE];
14891500

@@ -1507,6 +1518,74 @@ public long longValue() throws IOException {
15071518
}
15081519
return currentBlock[blockInIndex];
15091520
}
1521+
1522+
@Override
1523+
public BlockLoader.Block tryRead(
1524+
BlockLoader.BlockFactory factory,
1525+
BlockLoader.Docs docs,
1526+
int offset,
1527+
boolean nullsFiltered,
1528+
BlockDocValuesReader.ToDouble toDouble
1529+
) throws IOException {
1530+
if (nullsFiltered == false) {
1531+
return null;
1532+
}
1533+
final int firstDoc = docs.get(offset);
1534+
if (disi.advanceExact(firstDoc) == false) {
1535+
assert false : "nullsFiltered is true, but doc [" + firstDoc + "] has no value";
1536+
throw new IllegalStateException("nullsFiltered is true, but doc [" + firstDoc + "] has no value");
1537+
}
1538+
if (lookAheadDISI == null) {
1539+
lookAheadDISI = new IndexedDISI(
1540+
data,
1541+
entry.docsWithFieldOffset,
1542+
entry.docsWithFieldLength,
1543+
entry.jumpTableEntryCount,
1544+
entry.denseRankPower,
1545+
entry.numValues
1546+
);
1547+
}
1548+
final int lastDoc = docs.get(docs.count() - 1);
1549+
if (lookAheadDISI.advanceExact(lastDoc) == false) {
1550+
assert false : "nullsFiltered is true, but doc [" + lastDoc + "] has no value";
1551+
throw new IllegalStateException("nullsFiltered is true, but doc [" + lastDoc + "] has no value");
1552+
}
1553+
// Assumes docIds are unique - if the number of value indices between the first
1554+
// and last doc equals the doc count, all values can be read and converted in bulk
1555+
// TODO: Pass docCount attr for enrich and lookup.
1556+
final int firstIndex = disi.index();
1557+
final int lastIndex = lookAheadDISI.index();
1558+
final int valueCount = lastIndex - firstIndex + 1;
1559+
if (valueCount != docs.count()) {
1560+
return null;
1561+
}
1562+
if (Assertions.ENABLED) {
1563+
for (int i = 0; i < docs.count(); i++) {
1564+
final int doc = docs.get(i + offset);
1565+
assert disi.advanceExact(doc) : "nullsFiltered is true, but doc [" + doc + "] has no value";
1566+
assert disi.index() == firstIndex + i : "unexpected disi index " + (firstIndex + i) + "!=" + disi.index();
1567+
}
1568+
}
1569+
try (var singletonLongBuilder = singletonLongBuilder(factory, toDouble, valueCount)) {
1570+
for (int i = 0; i < valueCount;) {
1571+
final int index = firstIndex + i;
1572+
final int blockIndex = index >>> ES819TSDBDocValuesFormat.NUMERIC_BLOCK_SHIFT;
1573+
final int blockStartIndex = index & ES819TSDBDocValuesFormat.NUMERIC_BLOCK_MASK;
1574+
if (blockIndex != currentBlockIndex) {
1575+
assert blockIndex > currentBlockIndex : blockIndex + "<=" + currentBlockIndex;
1576+
if (currentBlockIndex + 1 != blockIndex) {
1577+
valuesData.seek(indexReader.get(blockIndex));
1578+
}
1579+
currentBlockIndex = blockIndex;
1580+
decoder.decode(valuesData, currentBlock);
1581+
}
1582+
final int count = Math.min(ES819TSDBDocValuesFormat.NUMERIC_BLOCK_SIZE - blockStartIndex, valueCount - i);
1583+
singletonLongBuilder.appendLongs(currentBlock, blockStartIndex, count);
1584+
i += count;
1585+
}
1586+
return singletonLongBuilder.build();
1587+
}
1588+
}
15101589
};
15111590
}
15121591
}
@@ -1802,11 +1881,22 @@ public BlockLoader.Builder endPositionEntry() {
18021881
public void close() {}
18031882
}
18041883

1884+
static BlockLoader.SingletonLongBuilder singletonLongBuilder(
1885+
BlockLoader.BlockFactory factory,
1886+
BlockDocValuesReader.ToDouble toDouble,
1887+
int valueCount
1888+
) {
1889+
if (toDouble != null) {
1890+
return new SingletonLongToDoubleDelegate(factory.singletonDoubles(valueCount), toDouble);
1891+
} else {
1892+
return factory.singletonLongs(valueCount);
1893+
}
1894+
}
1895+
18051896
// Block builder that consumes long values and converts them to double using the provided converter function.
18061897
static final class SingletonLongToDoubleDelegate implements BlockLoader.SingletonLongBuilder {
18071898
private final BlockLoader.SingletonDoubleBuilder doubleBuilder;
18081899
private final BlockDocValuesReader.ToDouble toDouble;
1809-
private final double[] buffer = new double[ES819TSDBDocValuesFormat.NUMERIC_BLOCK_SIZE];
18101900

18111901
// The passed builder is used to store the converted double values and produce the final block containing them.
18121902
SingletonLongToDoubleDelegate(BlockLoader.SingletonDoubleBuilder doubleBuilder, BlockDocValuesReader.ToDouble toDouble) {
@@ -1821,11 +1911,7 @@ public BlockLoader.SingletonLongBuilder appendLong(long value) {
18211911

18221912
@Override
18231913
public BlockLoader.SingletonLongBuilder appendLongs(long[] values, int from, int length) {
1824-
assert length <= buffer.length : "length " + length + " > " + buffer.length;
1825-
for (int i = 0; i < length; i++) {
1826-
buffer[i] = toDouble.convert(values[from + i]);
1827-
}
1828-
doubleBuilder.appendDoubles(buffer, 0, length);
1914+
doubleBuilder.appendLongs(toDouble, values, from, length);
18291915
return this;
18301916
}
18311917

@@ -1850,7 +1936,9 @@ public BlockLoader.Builder endPositionEntry() {
18501936
}
18511937

18521938
@Override
1853-
public void close() {}
1939+
public void close() {
1940+
doubleBuilder.close();
1941+
}
18541942
}
18551943

18561944
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ static class SingletonLongs extends BlockDocValuesReader implements NumericDocVa
137137
@Override
138138
public BlockLoader.Block read(BlockFactory factory, Docs docs, int offset, boolean nullsFiltered) throws IOException {
139139
if (numericDocValues instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
140-
BlockLoader.Block result = direct.tryRead(factory, docs, offset, null);
140+
BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, null);
141141
if (result != null) {
142142
return result;
143143
}
@@ -409,7 +409,7 @@ static class SingletonDoubles extends BlockDocValuesReader implements NumericDoc
409409
@Override
410410
public BlockLoader.Block read(BlockFactory factory, Docs docs, int offset, boolean nullsFiltered) throws IOException {
411411
if (docValues instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
412-
BlockLoader.Block result = direct.tryRead(factory, docs, offset, toDouble);
412+
BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, toDouble);
413413
if (result != null) {
414414
return result;
415415
}
@@ -736,7 +736,7 @@ public BlockLoader.Block read(BlockFactory factory, Docs docs, int offset, boole
736736
return readSingleDoc(factory, docs.get(offset));
737737
}
738738
if (ordinals instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
739-
BlockLoader.Block block = direct.tryRead(factory, docs, offset, null);
739+
BlockLoader.Block block = direct.tryRead(factory, docs, offset, nullsFiltered, null);
740740
if (block != null) {
741741
return block;
742742
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,18 @@ interface OptionalColumnAtATimeReader {
6666
* Attempts to read the values of all documents in {@code docs}
6767
* Returns {@code null} if unable to load the values.
6868
*
69+
* @param nullsFiltered if {@code true}, then target docs are guaranteed to have a value for the field.
70+
* see {@link ColumnAtATimeReader#read(BlockFactory, Docs, int, boolean)}
6971
* @param toDouble a function to convert long values to double, or null if no conversion is needed/supported
7072
*/
7173
@Nullable
72-
BlockLoader.Block tryRead(BlockFactory factory, Docs docs, int offset, BlockDocValuesReader.ToDouble toDouble) throws IOException;
74+
BlockLoader.Block tryRead(
75+
BlockFactory factory,
76+
Docs docs,
77+
int offset,
78+
boolean nullsFiltered,
79+
BlockDocValuesReader.ToDouble toDouble
80+
) throws IOException;
7381
}
7482

7583
interface RowStrideReader extends Reader {
@@ -559,9 +567,7 @@ interface SingletonLongBuilder extends Builder {
559567
* Specialized builder for collecting dense arrays of double values.
560568
*/
561569
interface SingletonDoubleBuilder extends Builder {
562-
SingletonDoubleBuilder appendDouble(double value);
563-
564-
SingletonDoubleBuilder appendDoubles(double[] values, int from, int length);
570+
SingletonDoubleBuilder appendLongs(BlockDocValuesReader.ToDouble toDouble, long[] values, int from, int length);
565571
}
566572

567573
interface LongBuilder extends Builder {

0 commit comments

Comments
 (0)