Browse Source

Support bulk reading doc values for byte, short and integer field types. (#134753)

Otherwise byte, short and integer field types can't bulk load like long field types. And these field types are being used.
Martijn van Groningen 1 month ago
parent
commit
987e7af02b

+ 64 - 9
server/src/main/java/org/elasticsearch/index/codec/tsdb/es819/ES819TSDBDocValuesProducer.java

@@ -390,7 +390,8 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
                 BlockLoader.Docs docs,
                 int offset,
                 boolean nullsFiltered,
-                BlockDocValuesReader.ToDouble toDouble
+                BlockDocValuesReader.ToDouble toDouble,
+                boolean toInt
             ) throws IOException {
                 assert toDouble == null;
                 if (ords instanceof BaseDenseNumericValues denseOrds) {
@@ -471,7 +472,8 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
             BlockLoader.Docs docs,
             int offset,
             boolean nullsFiltered,
-            BlockDocValuesReader.ToDouble toDouble
+            BlockDocValuesReader.ToDouble toDouble,
+            boolean toInt
         ) throws IOException {
             return null;
         }
@@ -524,7 +526,8 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
             BlockLoader.Docs docs,
             int offset,
             boolean nullsFiltered,
-            BlockDocValuesReader.ToDouble toDouble
+            BlockDocValuesReader.ToDouble toDouble,
+            boolean toInt
         ) throws IOException {
             return null;
         }
@@ -574,7 +577,8 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
             BlockLoader.Docs docs,
             int offset,
             boolean nullsFiltered,
-            BlockDocValuesReader.ToDouble toDouble
+            BlockDocValuesReader.ToDouble toDouble,
+            boolean toInt
         ) throws IOException {
             return null;
         }
@@ -1402,9 +1406,10 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
                     BlockLoader.Docs docs,
                     int offset,
                     boolean nullsFiltered,
-                    BlockDocValuesReader.ToDouble toDouble
+                    BlockDocValuesReader.ToDouble toDouble,
+                    boolean toInt
                 ) throws IOException {
-                    try (var singletonLongBuilder = singletonLongBuilder(factory, toDouble, docs.count() - offset)) {
+                    try (var singletonLongBuilder = singletonLongBuilder(factory, toDouble, docs.count() - offset, toInt)) {
                         return tryRead(singletonLongBuilder, docs, offset);
                     }
                 }
@@ -1525,7 +1530,8 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
                     BlockLoader.Docs docs,
                     int offset,
                     boolean nullsFiltered,
-                    BlockDocValuesReader.ToDouble toDouble
+                    BlockDocValuesReader.ToDouble toDouble,
+                    boolean toInt
                 ) throws IOException {
                     if (nullsFiltered == false) {
                         return null;
@@ -1566,7 +1572,7 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
                             assert disi.index() == firstIndex + i : "unexpected disi index " + (firstIndex + i) + "!=" + disi.index();
                         }
                     }
-                    try (var singletonLongBuilder = singletonLongBuilder(factory, toDouble, valueCount)) {
+                    try (var singletonLongBuilder = singletonLongBuilder(factory, toDouble, valueCount, toInt)) {
                         for (int i = 0; i < valueCount;) {
                             final int index = firstIndex + i;
                             final int blockIndex = index >>> ES819TSDBDocValuesFormat.NUMERIC_BLOCK_SHIFT;
@@ -1884,10 +1890,15 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
     static BlockLoader.SingletonLongBuilder singletonLongBuilder(
         BlockLoader.BlockFactory factory,
         BlockDocValuesReader.ToDouble toDouble,
-        int valueCount
+        int valueCount,
+        boolean toInt
     ) {
+        assert (toInt && toDouble != null) == false;
+
         if (toDouble != null) {
             return new SingletonLongToDoubleDelegate(factory.singletonDoubles(valueCount), toDouble);
+        } else if (toInt) {
+            return new SingletonLongtoIntDelegate(factory.singletonInts(valueCount));
         } else {
             return factory.singletonLongs(valueCount);
         }
@@ -1941,4 +1952,48 @@ final class ES819TSDBDocValuesProducer extends DocValuesProducer {
         }
     }
 
+    static final class SingletonLongtoIntDelegate implements BlockLoader.SingletonLongBuilder {
+        private final BlockLoader.SingletonIntBuilder builder;
+
+        SingletonLongtoIntDelegate(BlockLoader.SingletonIntBuilder builder) {
+            this.builder = builder;
+        }
+
+        @Override
+        public BlockLoader.SingletonLongBuilder appendLong(long value) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public BlockLoader.SingletonLongBuilder appendLongs(long[] values, int from, int length) {
+            builder.appendLongs(values, from, length);
+            return this;
+        }
+
+        @Override
+        public BlockLoader.Block build() {
+            return builder.build();
+        }
+
+        @Override
+        public BlockLoader.Builder appendNull() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public BlockLoader.Builder beginPositionEntry() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public BlockLoader.Builder endPositionEntry() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void close() {
+            builder.close();
+        }
+    }
+
 }

+ 16 - 5
server/src/main/java/org/elasticsearch/index/mapper/BlockDocValuesReader.java

@@ -137,7 +137,7 @@ public abstract class BlockDocValuesReader implements BlockLoader.AllReader {
         @Override
         public BlockLoader.Block read(BlockFactory factory, Docs docs, int offset, boolean nullsFiltered) throws IOException {
             if (numericDocValues instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
-                BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, null);
+                BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, null, false);
                 if (result != null) {
                     return result;
                 }
@@ -262,7 +262,7 @@ public abstract class BlockDocValuesReader implements BlockLoader.AllReader {
         }
     }
 
-    private static class SingletonInts extends BlockDocValuesReader {
+    static class SingletonInts extends BlockDocValuesReader implements NumericDocValuesAccessor {
         private final NumericDocValues numericDocValues;
 
         SingletonInts(NumericDocValues numericDocValues) {
@@ -271,6 +271,12 @@ public abstract class BlockDocValuesReader implements BlockLoader.AllReader {
 
         @Override
         public BlockLoader.Block read(BlockFactory factory, Docs docs, int offset, boolean nullsFiltered) throws IOException {
+            if (numericDocValues instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
+                BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, null, true);
+                if (result != null) {
+                    return result;
+                }
+            }
             try (BlockLoader.IntBuilder builder = factory.intsFromDocValues(docs.count() - offset)) {
                 for (int i = offset; i < docs.count(); i++) {
                     int doc = docs.get(i);
@@ -303,9 +309,14 @@ public abstract class BlockDocValuesReader implements BlockLoader.AllReader {
         public String toString() {
             return "BlockDocValuesReader.SingletonInts";
         }
+
+        @Override
+        public NumericDocValues numericDocValues() {
+            return numericDocValues;
+        }
     }
 
-    private static class Ints extends BlockDocValuesReader {
+    static class Ints extends BlockDocValuesReader {
         private final SortedNumericDocValues numericDocValues;
 
         Ints(SortedNumericDocValues numericDocValues) {
@@ -409,7 +420,7 @@ public abstract class BlockDocValuesReader implements BlockLoader.AllReader {
         @Override
         public BlockLoader.Block read(BlockFactory factory, Docs docs, int offset, boolean nullsFiltered) throws IOException {
             if (docValues instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
-                BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, toDouble);
+                BlockLoader.Block result = direct.tryRead(factory, docs, offset, nullsFiltered, toDouble, false);
                 if (result != null) {
                     return result;
                 }
@@ -764,7 +775,7 @@ public abstract class BlockDocValuesReader implements BlockLoader.AllReader {
                 return readSingleDoc(factory, docs.get(offset));
             }
             if (ordinals instanceof BlockLoader.OptionalColumnAtATimeReader direct) {
-                BlockLoader.Block block = direct.tryRead(factory, docs, offset, nullsFiltered, null);
+                BlockLoader.Block block = direct.tryRead(factory, docs, offset, nullsFiltered, null, false);
                 if (block != null) {
                     return block;
                 }

+ 22 - 2
server/src/main/java/org/elasticsearch/index/mapper/BlockLoader.java

@@ -68,7 +68,8 @@ public interface BlockLoader {
          *
          * @param nullsFiltered if {@code true}, then target docs are guaranteed to have a value for the field.
          *                      see {@link ColumnAtATimeReader#read(BlockFactory, Docs, int, boolean)}
-         * @param toDouble a function to convert long values to double, or null if no conversion is needed/supported
+         * @param toDouble      a function to convert long values to double, or null if no conversion is needed/supported
+         * @param toInt         whether to convert to int in case int block / vector is needed
          */
         @Nullable
         BlockLoader.Block tryRead(
@@ -76,7 +77,8 @@ public interface BlockLoader {
             Docs docs,
             int offset,
             boolean nullsFiltered,
-            BlockDocValuesReader.ToDouble toDouble
+            BlockDocValuesReader.ToDouble toDouble,
+            boolean toInt
         ) throws IOException;
     }
 
@@ -445,6 +447,17 @@ public interface BlockLoader {
          */
         SingletonLongBuilder singletonLongs(int expectedCount);
 
+        /**
+         * Build a specialized builder for singleton dense int based fields with the following constraints:
+         * <ul>
+         *     <li>Only one value per document can be collected</li>
+         *     <li>No more than expectedCount values can be collected</li>
+         * </ul>
+         *
+         * @param expectedCount The maximum number of values to be collected.
+         */
+        SingletonIntBuilder singletonInts(int expectedCount);
+
         /**
          * Build a specialized builder for singleton dense double based fields with the following constraints:
          * <ul>
@@ -570,6 +583,13 @@ public interface BlockLoader {
         SingletonDoubleBuilder appendLongs(BlockDocValuesReader.ToDouble toDouble, long[] values, int from, int length);
     }
 
+    /**
+     * Specialized builder for collecting dense arrays of double values.
+     */
+    interface SingletonIntBuilder extends Builder {
+        SingletonIntBuilder appendLongs(long[] values, int from, int length);
+    }
+
     interface LongBuilder extends Builder {
         /**
          * Appends a long to the current entry.

+ 105 - 20
server/src/test/java/org/elasticsearch/index/codec/tsdb/es819/ES819TSDBDocValuesFormatTests.java

@@ -774,7 +774,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
 
                         {
                             // bulk loading timestamp:
-                            var block = (TestBlock) timestampDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                            var block = (TestBlock) timestampDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                             assertNotNull(block);
                             assertEquals(size, block.size());
                             for (int j = 0; j < block.size(); j++) {
@@ -786,10 +786,10 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                         }
                         {
                             // bulk loading counter field:
-                            var block = (TestBlock) counterDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                            var block = (TestBlock) counterDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                             assertNotNull(block);
                             assertEquals(size, block.size());
-                            var stringBlock = (TestBlock) stringCounterDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                            var stringBlock = (TestBlock) stringCounterDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                             assertNotNull(stringBlock);
                             assertEquals(size, stringBlock.size());
                             for (int j = 0; j < block.size(); j++) {
@@ -806,7 +806,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                         }
                         {
                             // bulk loading gauge field:
-                            var block = (TestBlock) gaugeDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                            var block = (TestBlock) gaugeDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                             assertNotNull(block);
                             assertEquals(size, block.size());
                             for (int j = 0; j < block.size(); j++) {
@@ -844,7 +844,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
 
                 {
                     // bulk loading timestamp:
-                    var block = (TestBlock) timestampDV.tryRead(blockFactory, docs, randomOffset, false, null);
+                    var block = (TestBlock) timestampDV.tryRead(blockFactory, docs, randomOffset, false, null, false);
                     assertNotNull(block);
                     assertEquals(size, block.size());
                     for (int j = 0; j < block.size(); j++) {
@@ -856,11 +856,11 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                 }
                 {
                     // bulk loading counter field:
-                    var block = (TestBlock) counterDV.tryRead(factory, docs, randomOffset, false, null);
+                    var block = (TestBlock) counterDV.tryRead(factory, docs, randomOffset, false, null, false);
                     assertNotNull(block);
                     assertEquals(size, block.size());
 
-                    var stringBlock = (TestBlock) stringCounterDV.tryRead(factory, docs, randomOffset, false, null);
+                    var stringBlock = (TestBlock) stringCounterDV.tryRead(factory, docs, randomOffset, false, null, false);
                     assertNotNull(stringBlock);
                     assertEquals(size, stringBlock.size());
 
@@ -878,7 +878,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                 }
                 {
                     // bulk loading gauge field:
-                    var block = (TestBlock) gaugeDV.tryRead(factory, docs, randomOffset, false, null);
+                    var block = (TestBlock) gaugeDV.tryRead(factory, docs, randomOffset, false, null, false);
                     assertNotNull(block);
                     assertEquals(size, block.size());
                     for (int j = 0; j < block.size(); j++) {
@@ -903,11 +903,11 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                 stringCounterDV = getBaseSortedDocValues(leafReader, counterFieldAsString);
                 {
                     // bulk loading counter field:
-                    var block = (TestBlock) counterDV.tryRead(factory, docs, 0, false, null);
+                    var block = (TestBlock) counterDV.tryRead(factory, docs, 0, false, null, false);
                     assertNotNull(block);
                     assertEquals(size, block.size());
 
-                    var stringBlock = (TestBlock) stringCounterDV.tryRead(factory, docs, 0, false, null);
+                    var stringBlock = (TestBlock) stringCounterDV.tryRead(factory, docs, 0, false, null, false);
                     assertNotNull(stringBlock);
                     assertEquals(size, stringBlock.size());
 
@@ -925,6 +925,91 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
         }
     }
 
+    public void testOptionalColumnAtATimeReaderReadAsInt() throws Exception {
+        final String counterField = "counter";
+        final String timestampField = "@timestamp";
+        final String gaugeField = "gauge";
+        int currentTimestamp = 17040672;
+        int currentCounter = 10_000_000;
+
+        var config = getTimeSeriesIndexWriterConfig(null, timestampField);
+        try (var dir = newDirectory(); var iw = new IndexWriter(dir, config)) {
+            int[] gauge1Values = new int[] { 2, 4, 6, 8, 10, 12, 14, 16 };
+            int numDocs = 256 + random().nextInt(8096);
+
+            for (int i = 0; i < numDocs; i++) {
+                var d = new Document();
+                long timestamp = currentTimestamp;
+                // Index sorting doesn't work with NumericDocValuesField:
+                d.add(SortedNumericDocValuesField.indexedField(timestampField, timestamp));
+                d.add(new SortedNumericDocValuesField(counterField, currentCounter));
+                d.add(new SortedNumericDocValuesField(gaugeField, gauge1Values[i % gauge1Values.length]));
+
+                iw.addDocument(d);
+                if (i % 100 == 0) {
+                    iw.commit();
+                }
+                if (i < numDocs - 1) {
+                    currentTimestamp += 1000;
+                    currentCounter++;
+                }
+            }
+            iw.commit();
+            var factory = TestBlock.factory();
+            try (var reader = DirectoryReader.open(iw)) {
+                int gaugeIndex = numDocs;
+                for (var leaf : reader.leaves()) {
+                    var timestampDV = getBaseDenseNumericValues(leaf.reader(), timestampField);
+                    var counterDV = getBaseDenseNumericValues(leaf.reader(), counterField);
+                    var gaugeDV = getBaseDenseNumericValues(leaf.reader(), gaugeField);
+                    int maxDoc = leaf.reader().maxDoc();
+                    for (int i = 0; i < maxDoc;) {
+                        int size = Math.max(1, random().nextInt(0, maxDoc - i));
+                        var docs = TestBlock.docs(IntStream.range(i, i + size).toArray());
+
+                        {
+                            // bulk loading timestamp:
+                            var block = (TestBlock) timestampDV.tryRead(factory, docs, 0, random().nextBoolean(), null, true);
+                            assertNotNull(block);
+                            assertEquals(size, block.size());
+                            for (int j = 0; j < block.size(); j++) {
+                                int actualTimestamp = (int) block.get(j);
+                                int expectedTimestamp = currentTimestamp;
+                                assertEquals(expectedTimestamp, actualTimestamp);
+                                currentTimestamp -= 1000;
+                            }
+                        }
+                        {
+                            // bulk loading counter field:
+                            var block = (TestBlock) counterDV.tryRead(factory, docs, 0, random().nextBoolean(), null, true);
+                            assertNotNull(block);
+                            assertEquals(size, block.size());
+                            for (int j = 0; j < block.size(); j++) {
+                                int expectedCounter = currentCounter;
+                                int actualCounter = (int) block.get(j);
+                                assertEquals(expectedCounter, actualCounter);
+                                currentCounter--;
+                            }
+                        }
+                        {
+                            // bulk loading gauge field:
+                            var block = (TestBlock) gaugeDV.tryRead(factory, docs, 0, random().nextBoolean(), null, true);
+                            assertNotNull(block);
+                            assertEquals(size, block.size());
+                            for (int j = 0; j < block.size(); j++) {
+                                int actualGauge = (int) block.get(j);
+                                int expectedGauge = gauge1Values[--gaugeIndex % gauge1Values.length];
+                                assertEquals(expectedGauge, actualGauge);
+                            }
+                        }
+
+                        i += size;
+                    }
+                }
+            }
+        }
+    }
+
     public void testOptionalColumnAtATimeReaderWithSparseDocs() throws Exception {
         final String counterField = "counter";
         final String counterAsStringField = "counter_as_string";
@@ -1006,7 +1091,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                     var docs = TestBlock.docs(docIds);
                     {
                         timestampDV = getBaseDenseNumericValues(leafReader, timestampField);
-                        var block = (TestBlock) timestampDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                        var block = (TestBlock) timestampDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                         assertNotNull(block);
                         assertEquals(numDocsPerQValue, block.size());
                         for (int j = 0; j < block.size(); j++) {
@@ -1017,7 +1102,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                     }
                     {
                         counterDV = getBaseDenseNumericValues(leafReader, counterField);
-                        var block = (TestBlock) counterDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                        var block = (TestBlock) counterDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                         assertNotNull(block);
                         assertEquals(numDocsPerQValue, block.size());
                         for (int j = 0; j < block.size(); j++) {
@@ -1028,7 +1113,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                     }
                     {
                         counterAsStringDV = getBaseSortedDocValues(leafReader, counterAsStringField);
-                        var block = (TestBlock) counterAsStringDV.tryRead(factory, docs, 0, random().nextBoolean(), null);
+                        var block = (TestBlock) counterAsStringDV.tryRead(factory, docs, 0, random().nextBoolean(), null, false);
                         assertNotNull(block);
                         assertEquals(numDocsPerQValue, block.size());
                         for (int j = 0; j < block.size(); j++) {
@@ -1051,8 +1136,8 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                             assertThat(dv, instanceOf(OptionalColumnAtATimeReader.class));
                             OptionalColumnAtATimeReader directReader = (OptionalColumnAtATimeReader) dv;
                             docs = TestBlock.docs(testDocs.stream().mapToInt(n -> n).toArray());
-                            assertNull(directReader.tryRead(factory, docs, 0, false, null));
-                            TestBlock block = (TestBlock) directReader.tryRead(factory, docs, 0, true, null);
+                            assertNull(directReader.tryRead(factory, docs, 0, false, null, false));
+                            TestBlock block = (TestBlock) directReader.tryRead(factory, docs, 0, true, null, false);
                             assertNotNull(block);
                             for (int i = 0; i < testDocs.size(); i++) {
                                 assertThat(block.get(i), equalTo(temperatureValues[testDocs.get(i)]));
@@ -1064,8 +1149,8 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                             docs = TestBlock.docs(testDocs.stream().mapToInt(n -> n).toArray());
                             NumericDocValues dv = leafReader.getNumericDocValues(temperatureField);
                             OptionalColumnAtATimeReader directReader = (OptionalColumnAtATimeReader) dv;
-                            assertNull(directReader.tryRead(factory, docs, 0, false, null));
-                            assertNull(directReader.tryRead(factory, docs, 0, true, null));
+                            assertNull(directReader.tryRead(factory, docs, 0, false, null, false));
+                            assertNull(directReader.tryRead(factory, docs, 0, true, null, false));
                         }
                     }
                 }
@@ -1122,7 +1207,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                             }
                         };
                         var idReader = ESTestCase.asInstanceOf(OptionalColumnAtATimeReader.class, leaf.reader().getNumericDocValues("id"));
-                        TestBlock idBlock = (TestBlock) idReader.tryRead(factory, docs, 0, false, null);
+                        TestBlock idBlock = (TestBlock) idReader.tryRead(factory, docs, 0, false, null, false);
                         assertNotNull(idBlock);
 
                         {
@@ -1136,7 +1221,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                                 block = (TestBlock) reader2.tryReadAHead(factory, docs, randomOffset);
                             } else {
                                 assertNull(reader2.tryReadAHead(factory, docs, randomOffset));
-                                block = (TestBlock) reader2.tryRead(factory, docs, randomOffset, false, null);
+                                block = (TestBlock) reader2.tryRead(factory, docs, randomOffset, false, null, false);
                             }
                             assertNotNull(block);
                             assertThat(block.size(), equalTo(docs.count() - randomOffset));
@@ -1158,7 +1243,7 @@ public class ES819TSDBDocValuesFormatTests extends ES87TSDBDocValuesFormatTests
                                 block = (TestBlock) reader3.tryReadAHead(factory, docs, randomOffset);
                             } else {
                                 assertNull(reader3.tryReadAHead(factory, docs, randomOffset));
-                                block = (TestBlock) reader3.tryRead(factory, docs, randomOffset, false, null);
+                                block = (TestBlock) reader3.tryRead(factory, docs, randomOffset, false, null, false);
                             }
                             assertNotNull(reader3);
                             assertNotNull(block);

+ 9 - 0
server/src/test/java/org/elasticsearch/index/mapper/ByteFieldMapperTests.java

@@ -52,4 +52,13 @@ public class ByteFieldMapperTests extends WholeNumberFieldMapperTests {
     protected IngestScriptSupport ingestScriptSupport() {
         throw new AssumptionViolatedException("not supported");
     }
+
+    protected boolean supportsBulkIntBlockReading() {
+        return true;
+    }
+
+    @Override
+    protected Object[] getThreeSampleValues() {
+        return new Object[] { 1, 2, 3 };
+    }
 }

+ 9 - 0
server/src/test/java/org/elasticsearch/index/mapper/IntegerFieldMapperTests.java

@@ -53,4 +53,13 @@ public class IntegerFieldMapperTests extends WholeNumberFieldMapperTests {
     protected IngestScriptSupport ingestScriptSupport() {
         throw new AssumptionViolatedException("not supported");
     }
+
+    protected boolean supportsBulkIntBlockReading() {
+        return true;
+    }
+
+    @Override
+    protected Object[] getThreeSampleValues() {
+        return new Object[] { 1, 2, 3 };
+    }
 }

+ 9 - 0
server/src/test/java/org/elasticsearch/index/mapper/ShortFieldMapperTests.java

@@ -53,4 +53,13 @@ public class ShortFieldMapperTests extends WholeNumberFieldMapperTests {
     protected IngestScriptSupport ingestScriptSupport() {
         throw new AssumptionViolatedException("not supported");
     }
+
+    protected boolean supportsBulkIntBlockReading() {
+        return true;
+    }
+
+    @Override
+    protected Object[] getThreeSampleValues() {
+        return new Object[] { 1, 2, 3 };
+    }
 }

+ 17 - 3
test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java

@@ -1503,6 +1503,10 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
         return false;
     }
 
+    protected boolean supportsBulkIntBlockReading() {
+        return false;
+    }
+
     protected boolean supportsBulkDoubleBlockReading() {
         return false;
     }
@@ -1515,6 +1519,11 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
         return getThreeSampleValues();
     }
 
+    public void testSingletonIntBulkBlockReading() throws IOException {
+        assumeTrue("field type supports bulk singleton int reading", supportsBulkIntBlockReading());
+        testSingletonBulkBlockReading(columnAtATimeReader -> (BlockDocValuesReader.SingletonInts) columnAtATimeReader);
+    }
+
     public void testSingletonLongBulkBlockReading() throws IOException {
         assumeTrue("field type supports bulk singleton long reading", supportsBulkLongBlockReading());
         testSingletonBulkBlockReading(columnAtATimeReader -> (BlockDocValuesReader.SingletonLongs) columnAtATimeReader);
@@ -1593,8 +1602,9 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
                 var numeric = ((BlockDocValuesReader.NumericDocValuesAccessor) columnReader).numericDocValues();
                 assertThat(numeric, instanceOf(BlockLoader.OptionalColumnAtATimeReader.class));
                 var directReader = (BlockLoader.OptionalColumnAtATimeReader) numeric;
-                assertNull(directReader.tryRead(TestBlock.factory(), docBlock, 0, false, null));
-                block = (TestBlock) directReader.tryRead(TestBlock.factory(), docBlock, 0, true, null);
+                boolean toInt = supportsBulkIntBlockReading();
+                assertNull(directReader.tryRead(TestBlock.factory(), docBlock, 0, false, null, toInt));
+                block = (TestBlock) directReader.tryRead(TestBlock.factory(), docBlock, 0, true, null, toInt);
                 assertNotNull(block);
                 assertThat(block.get(0), equalTo(expectedSampleValues[0]));
                 assertThat(block.get(1), equalTo(expectedSampleValues[2]));
@@ -1624,7 +1634,11 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
                 var columnReader = blockLoader.columnAtATimeReader(context);
                 assertThat(
                     columnReader,
-                    anyOf(instanceOf(BlockDocValuesReader.Longs.class), instanceOf(BlockDocValuesReader.Doubles.class))
+                    anyOf(
+                        instanceOf(BlockDocValuesReader.Longs.class),
+                        instanceOf(BlockDocValuesReader.Doubles.class),
+                        instanceOf(BlockDocValuesReader.Ints.class)
+                    )
                 );
                 var docBlock = TestBlock.docs(IntStream.range(0, 3).toArray());
                 var block = (TestBlock) columnReader.read(TestBlock.factory(), docBlock, 0, false);

+ 42 - 0
test/framework/src/main/java/org/elasticsearch/index/mapper/TestBlock.java

@@ -251,6 +251,48 @@ public class TestBlock implements BlockLoader.Block {
                 };
             }
 
+            @Override
+            public BlockLoader.SingletonIntBuilder singletonInts(int expectedCount) {
+                final int[] values = new int[expectedCount];
+
+                return new BlockLoader.SingletonIntBuilder() {
+
+                    private int count;
+
+                    @Override
+                    public BlockLoader.Block build() {
+                        return new TestBlock(Arrays.stream(values).boxed().collect(Collectors.toUnmodifiableList()));
+                    }
+
+                    @Override
+                    public BlockLoader.SingletonIntBuilder appendLongs(long[] newValues, int from, int length) {
+                        for (int i = 0; i < length; i++) {
+                            values[count + i] = Math.toIntExact(newValues[from + i]);
+                        }
+                        this.count += length;
+                        return this;
+                    }
+
+                    @Override
+                    public BlockLoader.Builder appendNull() {
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    public BlockLoader.Builder beginPositionEntry() {
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    public BlockLoader.Builder endPositionEntry() {
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    public void close() {}
+                };
+            }
+
             @Override
             public BlockLoader.SingletonDoubleBuilder singletonDoubles(int expectedCount) {
                 final double[] values = new double[expectedCount];

+ 5 - 0
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/DelegatingBlockLoaderFactory.java

@@ -108,6 +108,11 @@ public abstract class DelegatingBlockLoaderFactory implements BlockLoader.BlockF
         return new SingletonLongBuilder(expectedCount, factory);
     }
 
+    @Override
+    public BlockLoader.SingletonIntBuilder singletonInts(int expectedCount) {
+        return new SingletonIntBuilder(expectedCount, factory);
+    }
+
     @Override
     public BlockLoader.SingletonDoubleBuilder singletonDoubles(int expectedCount) {
         return new SingletonDoubleBuilder(expectedCount, factory);

+ 1 - 1
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/SingletonDoubleBuilder.java

@@ -61,7 +61,7 @@ public final class SingletonDoubleBuilder implements BlockLoader.SingletonDouble
 
     @Override
     public long estimatedBytes() {
-        return (long) values.length * Double.BYTES;
+        return valuesSize(values.length);
     }
 
     @Override

+ 91 - 0
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/SingletonIntBuilder.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.compute.lucene.read;
+
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BlockFactory;
+import org.elasticsearch.core.Releasable;
+import org.elasticsearch.index.mapper.BlockLoader;
+
+/**
+ * Like {@link org.elasticsearch.compute.data.IntBlockBuilder} but optimized for collecting dense single valued values.
+ * Additionally, this builder doesn't grow its array.
+ */
+public final class SingletonIntBuilder implements BlockLoader.SingletonIntBuilder, Releasable, Block.Builder {
+
+    private final int[] values;
+    private final BlockFactory blockFactory;
+
+    private int count;
+
+    public SingletonIntBuilder(int expectedCount, BlockFactory blockFactory) {
+        this.blockFactory = blockFactory;
+        blockFactory.adjustBreaker(valuesSize(expectedCount));
+        this.values = new int[expectedCount];
+    }
+
+    @Override
+    public Block.Builder appendNull() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Block.Builder beginPositionEntry() {
+        throw new UnsupportedOperationException();
+
+    }
+
+    @Override
+    public Block.Builder endPositionEntry() {
+        throw new UnsupportedOperationException();
+
+    }
+
+    @Override
+    public Block.Builder copyFrom(Block block, int beginInclusive, int endExclusive) {
+        throw new UnsupportedOperationException();
+
+    }
+
+    @Override
+    public Block.Builder mvOrdering(Block.MvOrdering mvOrdering) {
+        throw new UnsupportedOperationException();
+
+    }
+
+    @Override
+    public long estimatedBytes() {
+        return valuesSize(values.length);
+    }
+
+    @Override
+    public Block build() {
+        if (values.length != count) {
+            throw new IllegalStateException("expected " + values.length + " values but got " + count);
+        }
+        return blockFactory.newIntArrayVector(values, count).asBlock();
+    }
+
+    @Override
+    public BlockLoader.SingletonIntBuilder appendLongs(long[] longValues, int from, int length) {
+        for (int i = 0; i < length; i++) {
+            values[count + i] = Math.toIntExact(longValues[from + i]);
+        }
+        this.count += length;
+        return this;
+    }
+
+    @Override
+    public void close() {
+        blockFactory.adjustBreaker(-valuesSize(values.length));
+    }
+
+    static long valuesSize(int count) {
+        return (long) count * Integer.BYTES;
+    }
+}

+ 1 - 1
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/read/SingletonLongBuilder.java

@@ -60,7 +60,7 @@ public final class SingletonLongBuilder implements BlockLoader.SingletonLongBuil
 
     @Override
     public long estimatedBytes() {
-        return (long) values.length * Long.BYTES;
+        return valuesSize(values.length);
     }
 
     @Override

+ 130 - 0
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/read/SingletonIntBuilderTests.java

@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.compute.lucene.read;
+
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.tests.analysis.MockAnalyzer;
+import org.apache.lucene.tests.util.TestUtil;
+import org.elasticsearch.common.breaker.CircuitBreakingException;
+import org.elasticsearch.compute.data.BlockFactory;
+import org.elasticsearch.compute.data.IntVector;
+import org.elasticsearch.compute.test.ComputeTestCase;
+import org.elasticsearch.index.codec.tsdb.es819.ES819TSDBDocValuesFormat;
+import org.elasticsearch.indices.CrankyCircuitBreakerService;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.test.MapMatcher.assertMap;
+import static org.elasticsearch.test.MapMatcher.matchesMap;
+import static org.hamcrest.Matchers.equalTo;
+
+public class SingletonIntBuilderTests extends ComputeTestCase {
+
+    public void testReader() throws IOException {
+        testRead(blockFactory());
+    }
+
+    public void testReadWithCranky() throws IOException {
+        var factory = crankyBlockFactory();
+        try {
+            testRead(factory);
+            // If we made it this far cranky didn't fail us!
+        } catch (CircuitBreakingException e) {
+            logger.info("cranky", e);
+            assertThat(e.getMessage(), equalTo(CrankyCircuitBreakerService.ERROR_MESSAGE));
+        }
+        assertThat(factory.breaker().getUsed(), equalTo(0L));
+    }
+
+    private void testRead(BlockFactory factory) throws IOException {
+        Integer[] values = new Integer[] { 10, 20, 30, 40 };
+
+        int count = 1000;
+        try (Directory directory = newDirectory()) {
+            try (IndexWriter indexWriter = createIndexWriter(directory)) {
+                for (int i = 0; i < count; i++) {
+                    Integer v = values[i % values.length];
+                    indexWriter.addDocument(List.of(new NumericDocValuesField("field", v)));
+                }
+            }
+            Map<Integer, Integer> counts = new HashMap<>();
+            try (IndexReader reader = DirectoryReader.open(directory)) {
+                for (LeafReaderContext ctx : reader.leaves()) {
+                    var docValues = ctx.reader().getNumericDocValues("field");
+                    try (SingletonIntBuilder builder = new SingletonIntBuilder(ctx.reader().numDocs(), factory)) {
+                        for (int i = 0; i < ctx.reader().maxDoc(); i++) {
+                            assertThat(docValues.advanceExact(i), equalTo(true));
+                            long value = docValues.longValue();
+                            builder.appendLongs(new long[] { value }, 0, 1);
+                        }
+                        try (IntVector build = (IntVector) builder.build().asVector()) {
+                            for (int i = 0; i < build.getPositionCount(); i++) {
+                                int key = build.getInt(i);
+                                counts.merge(key, 1, Integer::sum);
+                            }
+                        }
+                    }
+                }
+            }
+            int expectedCount = count / values.length;
+            assertMap(
+                counts,
+                matchesMap().entry(10, expectedCount).entry(20, expectedCount).entry(30, expectedCount).entry(40, expectedCount)
+            );
+        }
+    }
+
+    public void testMoreValues() throws IOException {
+        int count = 1_000;
+        try (Directory directory = newDirectory()) {
+            try (IndexWriter indexWriter = createIndexWriter(directory)) {
+                for (int i = 0; i < count; i++) {
+                    indexWriter.addDocument(List.of(new NumericDocValuesField("field", i)));
+                }
+                indexWriter.forceMerge(1);
+            }
+            try (IndexReader reader = DirectoryReader.open(directory)) {
+                assertThat(reader.leaves().size(), equalTo(1));
+                LeafReader leafReader = reader.leaves().get(0).reader();
+                var docValues = leafReader.getNumericDocValues("field");
+                int offset = 850;
+                try (SingletonIntBuilder builder = new SingletonIntBuilder(count - offset, blockFactory())) {
+                    for (int i = offset; i < leafReader.maxDoc(); i++) {
+                        assertThat(docValues.advanceExact(i), equalTo(true));
+                        long value = docValues.longValue();
+                        builder.appendLongs(new long[] { value }, 0, 1);
+                    }
+                    try (IntVector build = (IntVector) builder.build().asVector()) {
+                        assertThat(build.getPositionCount(), equalTo(count - offset));
+                        for (int i = 0; i < build.getPositionCount(); i++) {
+                            Integer key = build.getInt(i);
+                            assertThat(key, equalTo(offset + i));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    static IndexWriter createIndexWriter(Directory directory) throws IOException {
+        IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
+        iwc.setCodec(TestUtil.alwaysDocValuesFormat(new ES819TSDBDocValuesFormat()));
+        return new IndexWriter(directory, iwc);
+    }
+
+}