This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 80dbb7d02df Read MAP keys selectively from sealed forward indexes
(#19273)
80dbb7d02df is described below
commit 80dbb7d02dfd92ccc20ae2606ac712ea4f73d3cf
Author: Xiang Fu <[email protected]>
AuthorDate: Sun Aug 30 12:05:32 2026 -0700
Read MAP keys selectively from sealed forward indexes (#19273)
Override selective MAP-key reads for legacy V2/V3 and V4-V6 chunk readers
so projections avoid materializing the full map. Add sealed benchmark pairs and
coverage across reader versions and compression types.
---
.../apache/pinot/perf/BenchmarkMapKeyAccess.java | 61 ++++++++++++++++-
.../forward/VarByteChunkForwardIndexReaderV4.java | 14 ++++
.../forward/VarByteChunkSVForwardIndexReader.java | 13 ++++
.../segment/index/creator/VarByteChunkV4Test.java | 61 +++++++++++++++++
.../forward/VarByteChunkSVForwardIndexTest.java | 78 ++++++++++++++++++++++
5 files changed, 225 insertions(+), 2 deletions(-)
diff --git
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
index cf26f711085..96fd4026363 100644
--- a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
+++ b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkMapKeyAccess.java
@@ -18,12 +18,19 @@
*/
package org.apache.pinot.perf;
+import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import org.apache.commons.io.FileUtils;
import org.apache.pinot.segment.local.io.writer.impl.DirectMemoryManager;
+import
org.apache.pinot.segment.local.io.writer.impl.VarByteChunkForwardIndexWriterV4;
import
org.apache.pinot.segment.local.realtime.impl.forward.VarByteSVMutableForwardIndex;
+import
org.apache.pinot.segment.local.segment.index.readers.forward.VarByteChunkForwardIndexReaderV4;
+import org.apache.pinot.segment.spi.compression.ChunkCompressionType;
+import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.segment.spi.memory.PinotDataBufferMemoryManager;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.utils.MapUtils;
@@ -87,9 +94,14 @@ public class BenchmarkMapKeyAccess {
private PreparedMapKey _targetMapKey;
private PinotDataBufferMemoryManager _memoryManager;
private VarByteSVMutableForwardIndex _forwardIndex;
+ private File _sealedIndexDir;
+ private PinotDataBuffer _sealedDataBuffer;
+ private VarByteChunkForwardIndexReaderV4 _sealedForwardIndex;
+ private VarByteChunkForwardIndexReaderV4.ReaderContext _sealedContext;
@Setup(Level.Trial)
- public void setUp() {
+ public void setUp()
+ throws IOException {
Map<String, Object> map = new LinkedHashMap<>();
for (int i = 0; i < _numEntries; i++) {
String value = "value-with-enough-bytes-to-exercise-json-parsing-" + i;
@@ -116,15 +128,34 @@ public class BenchmarkMapKeyAccess {
_forwardIndex =
new VarByteSVMutableForwardIndex(DataType.MAP, _memoryManager,
"mapColumn", 1, serialized.length);
_forwardIndex.setBytes(0, serialized);
+
+ // The sealed counterpart of the same frame. A completed segment stores
the MAP column in a chunked raw forward
+ // index, so key access there goes through a different reader than the
consuming path above.
+ _sealedIndexDir =
Files.createTempDirectory(BenchmarkMapKeyAccess.class.getSimpleName()).toFile();
+ File indexFile = new File(_sealedIndexDir, "map.fwd");
+ try (VarByteChunkForwardIndexWriterV4 writer = new
VarByteChunkForwardIndexWriterV4(indexFile,
+ ChunkCompressionType.LZ4, Math.max(1024, serialized.length * 2))) {
+ writer.putBytes(serialized);
+ }
+ _sealedDataBuffer = PinotDataBuffer.mapReadOnlyBigEndianFile(indexFile);
+ _sealedForwardIndex = new
VarByteChunkForwardIndexReaderV4(_sealedDataBuffer, DataType.MAP, true);
+ _sealedContext = _sealedForwardIndex.createContext();
}
@TearDown(Level.Trial)
public void tearDown()
throws IOException {
try {
+ _sealedContext.close();
+ _sealedForwardIndex.close();
+ _sealedDataBuffer.close();
_forwardIndex.close();
} finally {
- _memoryManager.close();
+ try {
+ _memoryManager.close();
+ } finally {
+ FileUtils.deleteQuietly(_sealedIndexDir);
+ }
}
}
@@ -160,4 +191,30 @@ public class BenchmarkMapKeyAccess {
public Object selectiveMapValueAsString() {
return _forwardIndex.getMapEntryValueAsString(0, null, _targetMapKey);
}
+
+ /// The selective object lookup against a completed segment's chunked
forward index.
+ @Benchmark
+ public Object sealedSelectiveMapValue() {
+ return _sealedForwardIndex.getMapEntryValue(0, _sealedContext,
_targetMapKey);
+ }
+
+ /// The selective string lookup against a completed segment's chunked
forward index.
+ @Benchmark
+ public Object sealedSelectiveMapValueAsString() {
+ return _sealedForwardIndex.getMapEntryValueAsString(0, _sealedContext,
_targetMapKey);
+ }
+
+ /// The sealed object baseline: deserialize every entry of the chunk value,
then select the requested key.
+ @Benchmark
+ public Object sealedFullMapValue() {
+ return _sealedForwardIndex.getMap(0, _sealedContext).get(_targetKey);
+ }
+
+ /// The sealed string baseline: deserialize every entry, select the key,
then apply the default accessor's
+ /// null-safe `toString()` conversion.
+ @Benchmark
+ public Object sealedFullMapValueAsString() {
+ Object value = _sealedForwardIndex.getMap(0,
_sealedContext).get(_targetKey);
+ return value == null ? null : value.toString();
+ }
}
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkForwardIndexReaderV4.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkForwardIndexReaderV4.java
index 0d65fba1b26..a7b6bb5b4d7 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkForwardIndexReaderV4.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkForwardIndexReaderV4.java
@@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
import org.apache.pinot.segment.local.io.compression.ChunkCompressorFactory;
import
org.apache.pinot.segment.local.io.writer.impl.VarByteChunkForwardIndexWriterV4;
import org.apache.pinot.segment.local.utils.ArraySerDeUtils;
@@ -39,6 +40,7 @@ import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.utils.BigDecimalUtils;
import org.apache.pinot.spi.utils.MapUtils;
+import org.apache.pinot.spi.utils.MapUtils.PreparedMapKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -143,6 +145,18 @@ public class VarByteChunkForwardIndexReaderV4
return MapUtils.frameToJsonString(context.getValue(docId));
}
+ @Nullable
+ @Override
+ public Object getMapEntryValue(int docId, ReaderContext context,
PreparedMapKey key) {
+ return MapUtils.deserializeMapEntryValue(context.getValue(docId), key);
+ }
+
+ @Nullable
+ @Override
+ public String getMapEntryValueAsString(int docId, ReaderContext context,
PreparedMapKey key) {
+ return
MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(context.getValue(docId)),
key);
+ }
+
@Override
public int getIntMV(int docId, int[] valueBuffer,
VarByteChunkForwardIndexReaderV4.ReaderContext context) {
return
ArraySerDeUtils.deserializeIntArrayWithLength(context.getValue(docId),
valueBuffer);
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkSVForwardIndexReader.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkSVForwardIndexReader.java
index b732e483243..4a881b7b723 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkSVForwardIndexReader.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/forward/VarByteChunkSVForwardIndexReader.java
@@ -28,6 +28,7 @@ import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.utils.BigDecimalUtils;
import org.apache.pinot.spi.utils.MapUtils;
+import org.apache.pinot.spi.utils.MapUtils.PreparedMapKey;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -135,6 +136,18 @@ public final class VarByteChunkSVForwardIndexReader
extends BaseChunkForwardInde
return MapUtils.frameToJsonString(getBytes(docId, context));
}
+ @Nullable
+ @Override
+ public Object getMapEntryValue(int docId, ChunkReaderContext context,
PreparedMapKey key) {
+ return MapUtils.deserializeMapEntryValue(getBytes(docId, context), key);
+ }
+
+ @Nullable
+ @Override
+ public String getMapEntryValueAsString(int docId, ChunkReaderContext
context, PreparedMapKey key) {
+ return
MapUtils.deserializeMapEntryValueAsString(ByteBuffer.wrap(getBytes(docId,
context)), key);
+ }
+
/// Helper method to read BYTES value from the compressed index.
private byte[] getBytesCompressed(int docId, ChunkReaderContext context) {
int chunkRowId = docId % _numDocsPerChunk;
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/VarByteChunkV4Test.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/VarByteChunkV4Test.java
index 359bf41fb19..6b6ed9336d2 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/VarByteChunkV4Test.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/VarByteChunkV4Test.java
@@ -23,7 +23,9 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiConsumer;
@@ -39,12 +41,16 @@ import
org.apache.pinot.segment.local.segment.index.readers.forward.VarByteChunk
import org.apache.pinot.segment.spi.compression.ChunkCompressionType;
import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.utils.MapUtils;
+import org.apache.pinot.spi.utils.MapUtils.PreparedMapKey;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
public class VarByteChunkV4Test implements PinotBuffersAfterClassCheckRule {
@@ -132,6 +138,61 @@ public class VarByteChunkV4Test implements
PinotBuffersAfterClassCheckRule {
FileUtils.deleteQuietly(bytesMVFile);
}
+ /// A sealed MAP column is a chunked raw index over serialized frames, and a
projected `attributes['key']` resolves
+ /// to a selective read against it. Pins that the V4 implementation, also
inherited by V5 and V6, agrees with
+ /// deserializing the whole frame for every compression type.
+ @Test(dataProvider = "params")
+ public void testMapSV(File file, ChunkCompressionType compressionType, int
longestEntry, int chunkSize)
+ throws Exception {
+ File mapSVFile = new File(file, "testMapSV");
+ int numDocs = 1000;
+ List<Map<String, Object>> maps = new ArrayList<>(numDocs);
+ try (VarByteChunkWriter writer = createWriter(mapSVFile, compressionType,
chunkSize)) {
+ for (int i = 0; i < numDocs; i++) {
+ // Dotted OpenTelemetry-style keys, the first two of equal length so
the scan has to compare their bytes
+ // rather than skip on a length mismatch.
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("k8s.workload.name", "workload-" + i);
+ map.put("k8s.workload.kind", "Deployment");
+ map.put("k8s.namespace.name", "namespace-" + i % 7);
+ map.put("host_logical_cpus", i);
+ maps.add(map);
+ writer.putBytes(MapUtils.serializeMap(map));
+ }
+ }
+
+ try (PinotDataBuffer buffer =
PinotDataBuffer.mapReadOnlyBigEndianFile(mapSVFile);
+ VarByteChunkForwardIndexReaderV4 reader = createReader(buffer,
FieldSpec.DataType.MAP, true);
+ VarByteChunkForwardIndexReaderV4.ReaderContext context =
reader.createContext()) {
+ // A value-only assertion would also pass through ForwardIndexReader's
full-map fallback. Pin the actual
+ // dispatch target so removing either optimized override makes this
regression test fail.
+ assertSame(reader.getClass()
+ .getMethod("getMapEntryValue", int.class,
VarByteChunkForwardIndexReaderV4.ReaderContext.class,
+ PreparedMapKey.class)
+ .getDeclaringClass(),
+ VarByteChunkForwardIndexReaderV4.class);
+ assertSame(reader.getClass()
+ .getMethod("getMapEntryValueAsString", int.class,
VarByteChunkForwardIndexReaderV4.ReaderContext.class,
+ PreparedMapKey.class)
+ .getDeclaringClass(),
+ VarByteChunkForwardIndexReaderV4.class);
+ PreparedMapKey workloadName = new PreparedMapKey("k8s.workload.name");
+ PreparedMapKey cpus = new PreparedMapKey("host_logical_cpus");
+ PreparedMapKey absent = new PreparedMapKey("k8s.workload.namf");
+ for (int i = 0; i < numDocs; i++) {
+ Map<String, Object> expected = maps.get(i);
+ assertEquals(reader.getMap(i, context), expected);
+ assertEquals(reader.getMapEntryValue(i, context, workloadName),
expected.get("k8s.workload.name"));
+ assertEquals(reader.getMapEntryValueAsString(i, context,
workloadName), expected.get("k8s.workload.name"));
+ assertEquals(reader.getMapEntryValue(i, context, cpus),
expected.get("host_logical_cpus"));
+ assertEquals(reader.getMapEntryValueAsString(i, context, cpus),
String.valueOf(i));
+ assertNull(reader.getMapEntryValue(i, context, absent));
+ assertNull(reader.getMapEntryValueAsString(i, context, absent));
+ }
+ }
+ FileUtils.deleteQuietly(mapSVFile);
+ }
+
static class StringSplitterMV implements Function<String, String[]> {
@Override
public String[] apply(String input) {
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/forward/VarByteChunkSVForwardIndexTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/forward/VarByteChunkSVForwardIndexTest.java
index 920fd3789bc..70ddf5f7ae4 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/forward/VarByteChunkSVForwardIndexTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/forward/VarByteChunkSVForwardIndexTest.java
@@ -23,6 +23,10 @@ import java.io.IOException;
import java.net.URL;
import java.nio.ByteOrder;
import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Random;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
@@ -36,10 +40,13 @@ import
org.apache.pinot.segment.spi.compression.ChunkCompressionType;
import org.apache.pinot.segment.spi.memory.PinotByteBuffer;
import org.apache.pinot.segment.spi.memory.PinotDataBuffer;
import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.utils.MapUtils;
+import org.apache.pinot.spi.utils.MapUtils.PreparedMapKey;
import org.testng.Assert;
import org.testng.annotations.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.assertSame;
import static org.testng.Assert.fail;
@@ -80,6 +87,77 @@ public class VarByteChunkSVForwardIndexTest implements
PinotBuffersAfterMethodCh
test(ChunkCompressionType.GZIP);
}
+ /// Covers selective MAP reads through the legacy V2/V3 reader for both
compressed and pass-through chunks. These
+ /// formats are still selected for existing segments and do not share the
V4/V5/V6 reader implementation.
+ @Test
+ public void testMapSelectiveRead()
+ throws Exception {
+ for (ChunkCompressionType compressionType
+ : new ChunkCompressionType[]{ChunkCompressionType.SNAPPY,
ChunkCompressionType.PASS_THROUGH}) {
+ for (int writerVersion : new int[]{2, 3}) {
+ testMapSelectiveRead(compressionType, writerVersion);
+ }
+ }
+ }
+
+ private void testMapSelectiveRead(ChunkCompressionType compressionType, int
writerVersion)
+ throws Exception {
+ int numDocs = 5;
+ List<Map<String, Object>> maps = new ArrayList<>(numDocs);
+ byte[][] frames = new byte[numDocs][];
+ int longestEntry = 0;
+ for (int i = 0; i < numDocs; i++) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("k8s.workload.name", "workload-" + i);
+ map.put("k8s.workload.kind", "Deployment");
+ map.put("host_logical_cpus", i);
+ maps.add(map);
+ frames[i] = MapUtils.serializeMap(map);
+ longestEntry = Math.max(longestEntry, frames[i].length);
+ }
+
+ File outFile = Files.createTempFile(getClass().getSimpleName() + "-map-v"
+ writerVersion, ".fwd").toFile();
+ try {
+ try (VarByteChunkForwardIndexWriter writer = new
VarByteChunkForwardIndexWriter(outFile, compressionType,
+ numDocs, 2, longestEntry, writerVersion)) {
+ for (byte[] frame : frames) {
+ writer.putBytes(frame);
+ }
+ }
+
+ try (PinotDataBuffer buffer =
PinotDataBuffer.mapReadOnlyBigEndianFile(outFile);
+ VarByteChunkSVForwardIndexReader reader = new
VarByteChunkSVForwardIndexReader(buffer, DataType.MAP);
+ ChunkReaderContext context = reader.createContext()) {
+ // The inherited fallback returns the same values after materializing
the whole map, so also pin the methods
+ // that must own the selective implementation.
+ assertSame(reader.getClass()
+ .getMethod("getMapEntryValue", int.class,
ChunkReaderContext.class, PreparedMapKey.class)
+ .getDeclaringClass(),
+ VarByteChunkSVForwardIndexReader.class);
+ assertSame(reader.getClass()
+ .getMethod("getMapEntryValueAsString", int.class,
ChunkReaderContext.class, PreparedMapKey.class)
+ .getDeclaringClass(),
+ VarByteChunkSVForwardIndexReader.class);
+ PreparedMapKey workloadName = new PreparedMapKey("k8s.workload.name");
+ PreparedMapKey cpus = new PreparedMapKey("host_logical_cpus");
+ PreparedMapKey absent = new PreparedMapKey("k8s.workload.namf");
+ for (int i = 0; i < numDocs; i++) {
+ Map<String, Object> expected = maps.get(i);
+ Assert.assertEquals(reader.getMap(i, context), expected);
+ Assert.assertEquals(reader.getMapEntryValue(i, context,
workloadName), expected.get("k8s.workload.name"));
+ Assert.assertEquals(reader.getMapEntryValueAsString(i, context,
workloadName),
+ expected.get("k8s.workload.name"));
+ Assert.assertEquals(reader.getMapEntryValue(i, context, cpus),
expected.get("host_logical_cpus"));
+ Assert.assertEquals(reader.getMapEntryValueAsString(i, context,
cpus), String.valueOf(i));
+ Assert.assertNull(reader.getMapEntryValue(i, context, absent));
+ Assert.assertNull(reader.getMapEntryValueAsString(i, context,
absent));
+ }
+ }
+ } finally {
+ FileUtils.deleteQuietly(outFile);
+ }
+ }
+
/// This test writes [#NUM_ENTRIES] using [VarByteChunkForwardIndexWriter].
It then reads
/// the strings & bytes using [VarByteChunkSVForwardIndexReader], and
asserts that what was written is the
/// same as
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]