This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 4926d60aba [common] Preserve empty scored results during serialization
(#8924)
4926d60aba is described below
commit 4926d60aba22587f243f57126cf224e333ae3cda
Author: QuakeWang <[email protected]>
AuthorDate: Thu Jul 30 19:18:59 2026 +0800
[common] Preserve empty scored results during serialization (#8924)
---
.../globalindex/GlobalIndexResultSerializer.java | 58 ++++++++++++---
.../globalindex/GlobalIndexSerDeUtilsTest.java | 85 ++++++++++++++++++++++
2 files changed, 134 insertions(+), 9 deletions(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexResultSerializer.java
b/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexResultSerializer.java
index 5559c59c4f..56fdb0c68f 100644
---
a/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexResultSerializer.java
+++
b/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexResultSerializer.java
@@ -35,7 +35,11 @@ import static
org.apache.paimon.utils.Preconditions.checkArgument;
/** GlobalIndexResultSerializer to serialize and deserialize
GlobalIndexResult. */
public class GlobalIndexResultSerializer implements
Serializer<GlobalIndexResult> {
- private static final int VERSION = 1;
+ private static final int VERSION_1 = 1;
+ private static final int VERSION_2 = 2;
+
+ private static final byte UNSCORED_RESULT_KIND = 0;
+ private static final byte SCORED_RESULT_KIND = 1;
@Override
public Serializer<GlobalIndexResult> duplicate() {
@@ -59,7 +63,9 @@ public class GlobalIndexResultSerializer implements
Serializer<GlobalIndexResult
@Override
public void serialize(GlobalIndexResult globalIndexResult, DataOutputView
dataOutput)
throws IOException {
- dataOutput.writeInt(VERSION);
+ boolean isScored = globalIndexResult instanceof
ScoredGlobalIndexResult;
+ dataOutput.writeInt(VERSION_2);
+ dataOutput.writeByte(isScored ? SCORED_RESULT_KIND :
UNSCORED_RESULT_KIND);
RoaringNavigableMap64 roaringNavigableMap64 =
globalIndexResult.results();
byte[] bytes = roaringNavigableMap64.serialize();
@@ -67,7 +73,7 @@ public class GlobalIndexResultSerializer implements
Serializer<GlobalIndexResult
dataOutput.writeInt(bytes.length);
dataOutput.write(bytes);
- if (globalIndexResult instanceof ScoredGlobalIndexResult) {
+ if (isScored) {
ScoredGlobalIndexResult scored = (ScoredGlobalIndexResult)
globalIndexResult;
dataOutput.writeInt(roaringNavigableMap64.getIntCardinality());
ScoreGetter scoreGetter = scored.scoreGetter();
@@ -82,21 +88,55 @@ public class GlobalIndexResultSerializer implements
Serializer<GlobalIndexResult
@Override
public GlobalIndexResult deserialize(DataInputView dataInput) throws
IOException {
int version = dataInput.readInt();
- if (version != VERSION) {
- throw new IllegalStateException("Invalid version: " + version);
+ if (version == VERSION_1) {
+ return deserializeV1(dataInput);
+ }
+ if (version == VERSION_2) {
+ return deserializeV2(dataInput);
+ }
+ throw new IllegalStateException("Invalid version: " + version);
+ }
+
+ private GlobalIndexResult deserializeV1(DataInputView dataInput) throws
IOException {
+ RoaringNavigableMap64 roaringNavigableMap64 =
deserializeBitmap(dataInput);
+ int scoreSize = dataInput.readInt();
+ // V1 inferred the result kind from score count, so empty scored
results are
+ // indistinguishable.
+ if (scoreSize == 0) {
+ return GlobalIndexResult.create(roaringNavigableMap64);
+ }
+ return deserializeScoredResult(dataInput, roaringNavigableMap64,
scoreSize);
+ }
+
+ private GlobalIndexResult deserializeV2(DataInputView dataInput) throws
IOException {
+ byte resultKind = dataInput.readByte();
+ if (resultKind != UNSCORED_RESULT_KIND && resultKind !=
SCORED_RESULT_KIND) {
+ throw new IllegalStateException("Invalid result kind: " +
resultKind);
}
+ RoaringNavigableMap64 roaringNavigableMap64 =
deserializeBitmap(dataInput);
+ int scoreSize = dataInput.readInt();
+ if (resultKind == UNSCORED_RESULT_KIND) {
+ checkArgument(
+ scoreSize == 0, "Unexpected score size for unscored
result: %s", scoreSize);
+ return GlobalIndexResult.create(roaringNavigableMap64);
+ }
+ return deserializeScoredResult(dataInput, roaringNavigableMap64,
scoreSize);
+ }
+
+ private RoaringNavigableMap64 deserializeBitmap(DataInputView dataInput)
throws IOException {
int size = dataInput.readInt();
byte[] bytes = new byte[size];
dataInput.readFully(bytes);
RoaringNavigableMap64 roaringNavigableMap64 = new
RoaringNavigableMap64();
roaringNavigableMap64.deserialize(bytes);
- int scoreSize = dataInput.readInt();
+ return roaringNavigableMap64;
+ }
- if (scoreSize == 0) {
- return GlobalIndexResult.create(roaringNavigableMap64);
- }
+ private ScoredGlobalIndexResult deserializeScoredResult(
+ DataInputView dataInput, RoaringNavigableMap64
roaringNavigableMap64, int scoreSize)
+ throws IOException {
checkArgument(
scoreSize == roaringNavigableMap64.getIntCardinality(),
"Error size of score: "
diff --git
a/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexSerDeUtilsTest.java
b/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexSerDeUtilsTest.java
index 7729620d29..148c64f324 100644
---
a/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexSerDeUtilsTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexSerDeUtilsTest.java
@@ -33,6 +33,8 @@ import static org.assertj.core.api.Assertions.assertThat;
/** Tests for {@link GlobalIndexResultSerializer}. */
public class GlobalIndexSerDeUtilsTest {
+ private static final int VERSION_1 = 1;
+
@Test
public void testSerializeAndDeserializeGlobalIndexResult() throws
IOException {
RoaringNavigableMap64 bitmap = RoaringNavigableMap64.bitmapOf(1, 5,
10, 100, 1000);
@@ -56,6 +58,32 @@ public class GlobalIndexSerDeUtilsTest {
assertThat(deserialized.results().isEmpty()).isTrue();
}
+ @Test
+ public void testSerializeAndDeserializeEmptyScoredGlobalIndexResult()
throws IOException {
+ ScoredGlobalIndexResult original =
ScoredGlobalIndexResult.createEmpty();
+
+ byte[] serialized = serialize(original);
+ GlobalIndexResult deserialized = deserialize(serialized);
+
+ assertThat(deserialized).isInstanceOf(ScoredGlobalIndexResult.class);
+ assertThat(deserialized.results().isEmpty()).isTrue();
+
assertThat(serialized).isNotEqualTo(serialize(GlobalIndexResult.createEmpty()));
+ }
+
+ @Test
+ public void testEmptyScoredGlobalIndexResultConvenienceMethods() throws
IOException {
+ GlobalIndexResultSerializer serializer = new
GlobalIndexResultSerializer();
+ ScoredGlobalIndexResult original =
ScoredGlobalIndexResult.createEmpty();
+
+ ScoredGlobalIndexResult deserialized =
+ serializer.deserialize(serializer.serialize(original));
+ GlobalIndexResult copied = serializer.copy(original);
+
+ assertThat(deserialized.results().isEmpty()).isTrue();
+ assertThat(copied).isInstanceOf(ScoredGlobalIndexResult.class);
+ assertThat(copied.results().isEmpty()).isTrue();
+ }
+
@Test
public void testSerializeAndDeserializeTopkGlobalIndexResult() throws
IOException {
RoaringNavigableMap64 bitmap = RoaringNavigableMap64.bitmapOf(1, 5,
10, 100);
@@ -106,6 +134,42 @@ public class GlobalIndexSerDeUtilsTest {
assertThat(scoreGetter.score(Long.MAX_VALUE - 1)).isEqualTo(0.1f);
}
+ @Test
+ public void testDeserializeV1GlobalIndexResult() throws IOException {
+ RoaringNavigableMap64 bitmap = RoaringNavigableMap64.bitmapOf(1, 5,
10);
+
+ GlobalIndexResult deserialized =
deserialize(serializeV1(GlobalIndexResult.create(bitmap)));
+
+
assertThat(deserialized).isNotInstanceOf(ScoredGlobalIndexResult.class);
+ assertThat(deserialized.results()).isEqualTo(bitmap);
+ }
+
+ @Test
+ public void testDeserializeV1ScoredGlobalIndexResult() throws IOException {
+ RoaringNavigableMap64 bitmap = RoaringNavigableMap64.bitmapOf(1, 5);
+ Map<Long, Float> scoreMap = new HashMap<>();
+ scoreMap.put(1L, 0.9f);
+ scoreMap.put(5L, 0.8f);
+
+ GlobalIndexResult deserialized =
+ deserialize(serializeV1(ScoredGlobalIndexResult.create(bitmap,
scoreMap::get)));
+
+ assertThat(deserialized).isInstanceOf(ScoredGlobalIndexResult.class);
+ ScoredGlobalIndexResult scored = (ScoredGlobalIndexResult)
deserialized;
+ assertThat(scored.results()).isEqualTo(bitmap);
+ assertThat(scored.scoreGetter().score(1L)).isEqualTo(0.9f);
+ assertThat(scored.scoreGetter().score(5L)).isEqualTo(0.8f);
+ }
+
+ @Test
+ public void testDeserializeV1EmptyScoredGlobalIndexResult() throws
IOException {
+ GlobalIndexResult deserialized =
+
deserialize(serializeV1(ScoredGlobalIndexResult.createEmpty()));
+
+
assertThat(deserialized).isNotInstanceOf(ScoredGlobalIndexResult.class);
+ assertThat(deserialized.results().isEmpty()).isTrue();
+ }
+
private byte[] serialize(GlobalIndexResult result) throws IOException {
GlobalIndexResultSerializer globalIndexResultSerializer = new
GlobalIndexResultSerializer();
DataOutputSerializer dataOutputSerializer = new
DataOutputSerializer(1024);
@@ -118,4 +182,25 @@ public class GlobalIndexSerDeUtilsTest {
DataInputDeserializer dataInputDeserializer = new
DataInputDeserializer(data);
return globalIndexResultSerializer.deserialize(dataInputDeserializer);
}
+
+ private byte[] serializeV1(GlobalIndexResult result) throws IOException {
+ DataOutputSerializer dataOutputSerializer = new
DataOutputSerializer(1024);
+ dataOutputSerializer.writeInt(VERSION_1);
+
+ RoaringNavigableMap64 bitmap = result.results();
+ byte[] bitmapBytes = bitmap.serialize();
+ dataOutputSerializer.writeInt(bitmapBytes.length);
+ dataOutputSerializer.write(bitmapBytes);
+
+ if (result instanceof ScoredGlobalIndexResult) {
+ dataOutputSerializer.writeInt(bitmap.getIntCardinality());
+ ScoreGetter scoreGetter = ((ScoredGlobalIndexResult)
result).scoreGetter();
+ for (long rowId : bitmap) {
+ dataOutputSerializer.writeFloat(scoreGetter.score(rowId));
+ }
+ } else {
+ dataOutputSerializer.writeInt(0);
+ }
+ return dataOutputSerializer.getCopyOfBuffer();
+ }
}