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 1155e12082 [core] Support full-text indexes on DV tables (#9031)
1155e12082 is described below
commit 1155e1208262b925b05bd37ef671d3c04fa042f0
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Aug 5 11:27:32 2026 +0800
[core] Support full-text indexes on DV tables (#9031)
---
.../table/source/DataEvolutionFullTextRead.java | 21 ++++-
.../table/source/DataEvolutionFullTextScan.java | 40 +++++++-
.../apache/paimon/table/source/FullTextScan.java | 10 ++
.../table/source/FullTextSearchBuilderImpl.java | 5 +-
.../paimon/table/source/RawFullTextReadImpl.java | 24 ++++-
.../table/source/FullTextSearchBuilderTest.java | 101 +++++++++++++++++++++
6 files changed, 192 insertions(+), 9 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextRead.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextRead.java
index d9100c2838..780155f837 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextRead.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextRead.java
@@ -18,6 +18,7 @@
package org.apache.paimon.table.source;
+import org.apache.paimon.Snapshot;
import org.apache.paimon.globalindex.GlobalIndexIOMeta;
import org.apache.paimon.globalindex.GlobalIndexReadThreadPool;
import org.apache.paimon.globalindex.GlobalIndexReader;
@@ -79,6 +80,16 @@ public class DataEvolutionFullTextRead implements
FullTextRead {
@Override
public GlobalIndexResult read(List<FullTextSearchSplit> splits) {
+ return read(splits, null);
+ }
+
+ @Override
+ public GlobalIndexResult read(FullTextScan.Plan plan) {
+ return read(plan.splits(), plan.snapshot());
+ }
+
+ private GlobalIndexResult read(
+ List<FullTextSearchSplit> splits, @Nullable Snapshot planSnapshot)
{
if (splits.isEmpty()) {
return GlobalIndexResult.createEmpty();
}
@@ -102,13 +113,19 @@ public class DataEvolutionFullTextRead implements
FullTextRead {
}
GlobalIndexFileReader indexFileReader = m ->
table.fileIO().newInputStream(m.filePath());
- RoaringNavigableMap64 liveRows =
GlobalIndexLiveRowFilter.liveRows(table, partitionFilter);
+ RoaringNavigableMap64 liveRows =
+ GlobalIndexLiveRowFilter.liveRows(table, planSnapshot,
partitionFilter, null);
ScoredGlobalIndexResult result =
evalQuery(splitsByColumn, indexPathFactory, indexFileReader,
executor, liveRows);
if (!rawRowRanges.isEmpty()) {
result =
new RawFullTextReadImpl(
- table, partitionFilter, limit, textColumn,
this::evalQuery)
+ table,
+ planSnapshot,
+ partitionFilter,
+ limit,
+ textColumn,
+ this::evalQuery)
.withRawSearch(result, rawRowRanges,
splitsByColumn, executor);
}
return result.topK(limit);
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextScan.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextScan.java
index 3dc1f36812..fbd1bd83d1 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextScan.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionFullTextScan.java
@@ -34,6 +34,8 @@ import org.apache.paimon.types.DataField;
import org.apache.paimon.utils.Filter;
import org.apache.paimon.utils.Range;
+import javax.annotation.Nullable;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -53,19 +55,33 @@ import static
org.apache.paimon.utils.Preconditions.checkNotNull;
public class DataEvolutionFullTextScan implements FullTextScan {
private final FileStoreTable table;
- private final PartitionPredicate partitionFilter;
+ @Nullable private final PartitionPredicate partitionFilter;
private final List<DataField> textColumns;
+ @Nullable private final Snapshot pinnedSnapshot;
public DataEvolutionFullTextScan(
- FileStoreTable table, PartitionPredicate partitionFilter,
DataField textColumn) {
+ FileStoreTable table,
+ @Nullable PartitionPredicate partitionFilter,
+ DataField textColumn) {
this(table, partitionFilter, Collections.singletonList(textColumn));
}
public DataEvolutionFullTextScan(
- FileStoreTable table, PartitionPredicate partitionFilter,
List<DataField> textColumns) {
+ FileStoreTable table,
+ @Nullable PartitionPredicate partitionFilter,
+ List<DataField> textColumns) {
+ this(table, partitionFilter, textColumns, null);
+ }
+
+ public DataEvolutionFullTextScan(
+ FileStoreTable table,
+ @Nullable PartitionPredicate partitionFilter,
+ List<DataField> textColumns,
+ @Nullable Snapshot pinnedSnapshot) {
this.table = table;
this.partitionFilter = partitionFilter;
this.textColumns = textColumns;
+ this.pinnedSnapshot = pinnedSnapshot;
}
@Override
@@ -82,7 +98,9 @@ public class DataEvolutionFullTextScan implements
FullTextScan {
idToColumn.put(textColumn.id(), textColumn.name());
}
- Snapshot snapshot = TimeTravelUtil.tryTravelOrLatest(table);
+ @Nullable
+ Snapshot snapshot =
+ pinnedSnapshot != null ? pinnedSnapshot :
TimeTravelUtil.tryTravelOrLatest(table);
IndexFileHandler indexFileHandler =
table.store().newIndexFileHandler();
Filter<IndexManifestEntry> indexFileFilter =
entry -> {
@@ -128,7 +146,19 @@ public class DataEvolutionFullTextScan implements
FullTextScan {
}
}
- return () -> splits;
+ @Nullable Snapshot planSnapshot = snapshot;
+ return new Plan() {
+ @Override
+ public List<FullTextSearchSplit> splits() {
+ return splits;
+ }
+
+ @Override
+ @Nullable
+ public Snapshot snapshot() {
+ return planSnapshot;
+ }
+ };
}
/**
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextScan.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextScan.java
index 563ffe1c9d..28e76f9fee 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextScan.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextScan.java
@@ -18,6 +18,10 @@
package org.apache.paimon.table.source;
+import org.apache.paimon.Snapshot;
+
+import javax.annotation.Nullable;
+
import java.util.List;
/** Full-text scan to scan index files. */
@@ -28,5 +32,11 @@ public interface FullTextScan {
/** Plan of full-text scan. */
interface Plan {
List<FullTextSearchSplit> splits();
+
+ /** Snapshot the plan was built against; the read pins live-row
filtering to it. */
+ @Nullable
+ default Snapshot snapshot() {
+ return null;
+ }
}
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextSearchBuilderImpl.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextSearchBuilderImpl.java
index 873e73b2f4..dccd3d7da2 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextSearchBuilderImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/FullTextSearchBuilderImpl.java
@@ -78,7 +78,10 @@ public class FullTextSearchBuilderImpl implements
FullTextSearchBuilder {
? new PrimaryKeyFullTextScan(
table, definition.get(), partitionFilter,
pinnedSnapshot)
: new DataEvolutionFullTextScan(
- table, partitionFilter,
Collections.singletonList(textColumn));
+ table,
+ partitionFilter,
+ Collections.singletonList(textColumn),
+ pinnedSnapshot);
}
@Override
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/RawFullTextReadImpl.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/RawFullTextReadImpl.java
index ca142430c3..86c6d2a1d2 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/RawFullTextReadImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/RawFullTextReadImpl.java
@@ -18,6 +18,8 @@
package org.apache.paimon.table.source;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.PositionOutputStream;
@@ -64,6 +66,7 @@ import static
org.apache.paimon.utils.Preconditions.checkNotNull;
class RawFullTextReadImpl {
private final FileStoreTable table;
+ @Nullable private final Snapshot planSnapshot;
@Nullable private final PartitionPredicate partitionFilter;
private final int limit;
private final DataField textColumn;
@@ -71,11 +74,13 @@ class RawFullTextReadImpl {
RawFullTextReadImpl(
FileStoreTable table,
+ @Nullable Snapshot planSnapshot,
@Nullable PartitionPredicate partitionFilter,
int limit,
DataField textColumn,
IndexSearch indexSearch) {
this.table = table;
+ this.planSnapshot = planSnapshot;
this.partitionFilter = partitionFilter;
this.limit = limit;
this.textColumn = textColumn;
@@ -243,13 +248,30 @@ class RawFullTextReadImpl {
}
private ReadBuilder rawReadBuilder(RowType readType) {
- ReadBuilder readBuilder =
table.newReadBuilder().withReadType(readType);
+ ReadBuilder readBuilder =
rawReadTable().newReadBuilder().withReadType(readType);
if (partitionFilter != null) {
readBuilder.withPartitionFilter(partitionFilter);
}
return readBuilder;
}
+ private FileStoreTable rawReadTable() {
+ if (planSnapshot == null) {
+ return table;
+ }
+
+ Map<String, String> pinOptions = new HashMap<>();
+ pinOptions.put(
+ CoreOptions.SCAN_MODE.key(),
CoreOptions.StartupMode.FROM_SNAPSHOT.toString());
+ pinOptions.put(CoreOptions.SCAN_SNAPSHOT_ID.key(),
String.valueOf(planSnapshot.id()));
+ pinOptions.put(CoreOptions.SCAN_VERSION.key(), null);
+ pinOptions.put(CoreOptions.SCAN_TAG_NAME.key(), null);
+ pinOptions.put(CoreOptions.SCAN_WATERMARK.key(), null);
+ pinOptions.put(CoreOptions.SCAN_TIMESTAMP.key(), null);
+ pinOptions.put(CoreOptions.SCAN_TIMESTAMP_MILLIS.key(), null);
+ return table.copyWithoutTimeTravel(pinOptions);
+ }
+
private Options rawSearchOptions() {
Options options = new
Options(table.coreOptions().toConfiguration().toMap());
options.setString("full-text.searcher-pool.max-size", "0");
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
index 16c418dc64..0cc025e040 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
@@ -19,6 +19,10 @@
package org.apache.paimon.table.source;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.append.dataevolution.DataEvolutionCompactCoordinator;
+import org.apache.paimon.append.dataevolution.DataEvolutionCompactTask;
+import
org.apache.paimon.append.dataevolution.DataEvolutionCompactionCommitPreparation;
import org.apache.paimon.catalog.Identifier;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.BinaryString;
@@ -65,7 +69,9 @@ import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import static
org.apache.paimon.table.source.DeletionVectorTestUtils.commitDeletionVectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -162,6 +168,101 @@ public class FullTextSearchBuilderTest extends
TableTestBase {
assertThat(readIds(table, result)).containsExactlyInAnyOrder(2, 3);
}
+ @Test
+ public void testFullTextSearchPinsLiveRowFilterToPlanSnapshot() throws
Exception {
+ Identifier identifier = identifier("full_text_pinned_live_rows");
+ Schema schema =
+ Schema.newBuilder()
+ .column("id", DataTypes.INT())
+ .column(TEXT_FIELD_NAME, DataTypes.STRING())
+ .option(CoreOptions.BUCKET.key(), "-1")
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ENABLED.key(),
"true")
+ .option(CoreOptions.DELETION_VECTORS_ENABLED.key(),
"true")
+ .build();
+ catalog.createTable(identifier, schema, false);
+ FileStoreTable table = getTable(identifier);
+
+ String[] documents = {
+ "paimon keyword", "paimon keyword", "paimon keyword", "paimon
keyword"
+ };
+ writeDocuments(table, documents);
+ buildAndCommitIndex(table, documents);
+
+ FullTextSearchBuilder builder =
+ table.newFullTextSearchBuilder()
+ .withQuery(TEXT_FIELD_NAME, matchQuery("keyword"))
+ .withLimit(4);
+ FullTextScan.Plan plan = builder.newFullTextScan().scan();
+ assertThat(plan.snapshot()).isNotNull();
+
+ // Row 0 was live when the index plan was created, so a later DV must
not affect this read.
+ commitDeletionVectors(table, 0L);
+
+ GlobalIndexResult result = builder.newFullTextRead().read(plan);
+ assertThat(result.results()).containsExactlyInAnyOrder(0L, 1L, 2L, 3L);
+ }
+
+ @Test
+ public void testFullTextRawFallbackPinsDataReadToPlanSnapshot() throws
Exception {
+ Identifier identifier = identifier("full_text_pinned_raw_fallback");
+ Schema schema =
+ Schema.newBuilder()
+ .column("id", DataTypes.INT())
+ .column(TEXT_FIELD_NAME, DataTypes.STRING())
+ .option(CoreOptions.BUCKET.key(), "-1")
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ENABLED.key(),
"true")
+ .option(CoreOptions.DELETION_VECTORS_ENABLED.key(),
"true")
+ .option(CoreOptions.FULL_TEXT_INDEX_SEARCH_MODE.key(),
"full")
+ .build();
+ catalog.createTable(identifier, schema, false);
+ FileStoreTable table = getTable(identifier);
+
+ String[] documents = {"indexed keyword", "raw keyword"};
+ writeDocuments(table, documents);
+ buildAndCommitIndexRange(
+ table,
+ new String[] {documents[0]},
+
Collections.singletonList(table.rowType().getField(TEXT_FIELD_NAME)),
+ 0);
+
+ FullTextSearchBuilder builder =
+ table.newFullTextSearchBuilder()
+ .withQuery(TEXT_FIELD_NAME, matchQuery("keyword"))
+ .withLimit(2);
+ FullTextScan.Plan plan = builder.newFullTextScan().scan();
+
assertThat(plan.splits()).anyMatch(RawFullTextSearchSplit.class::isInstance);
+
+ // Materialization rewrites the surviving row ids after planning. Both
the indexed and raw
+ // sides must still be evaluated against the pre-compaction snapshot
carried by the plan.
+ commitDeletionVectors(table, 0L);
+ Map<String, String> compactOptions = new HashMap<>();
+
compactOptions.put(CoreOptions.DATA_EVOLUTION_COMPACTION_REWRITE_ROW_IDS.key(),
"true");
+ FileStoreTable compactTable = table.copy(compactOptions);
+ Snapshot compactSnapshot = compactTable.latestSnapshot().get();
+ DataEvolutionCompactCoordinator coordinator =
+ new DataEvolutionCompactCoordinator(compactTable, false,
false, compactSnapshot);
+ List<DataEvolutionCompactTask> tasks = coordinator.plan();
+ assertThat(tasks)
+ .singleElement()
+ .extracting(DataEvolutionCompactTask::type)
+
.isEqualTo(DataEvolutionCompactTask.TaskType.MATERIALIZE_DELETION);
+ List<CommitMessage> messages = new ArrayList<>();
+ for (DataEvolutionCompactTask task : tasks) {
+ messages.add(task.doCompact(compactTable,
"test-full-text-snapshot-pin"));
+ }
+ messages.addAll(
+ new DataEvolutionCompactionCommitPreparation(compactTable,
compactSnapshot)
+ .prepare(messages));
+ try (BatchTableCommit commit =
compactTable.newBatchWriteBuilder().newCommit()) {
+ commit.commit(messages);
+ }
+
+ GlobalIndexResult result = builder.newFullTextRead().read(plan);
+ assertThat(result.results()).containsExactlyInAnyOrder(0L, 1L);
+ }
+
@Test
public void testFullTextSearchNonFastModesScanUnindexedData() throws
Exception {
createTableDefault();