From 41f256cead7ed3e8dd76d7a2b573689679fc15f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A0=9C=ED=98=B8?= <135206467+jeho-rpls@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:04:46 +0900 Subject: [PATCH] Check if merge is aborted during HNSW graph construction (#16368) (cherry picked from commit 2b237286b38e9b62d6e017a1d746a66c707e8462) --- lucene/CHANGES.txt | 4 + .../lucene99/Lucene99HnswVectorsWriter.java | 13 +- .../util/hnsw/ConcurrentHnswMerger.java | 20 ++- .../apache/lucene/util/hnsw/HnswBuilder.java | 13 ++ .../util/hnsw/HnswConcurrentMergeBuilder.java | 8 + .../lucene/util/hnsw/HnswGraphBuilder.java | 14 ++ .../util/hnsw/IncrementalHnswGraphMerger.java | 21 +++ .../lucene/index/TestHnswMergeAbort.java | 163 ++++++++++++++++++ .../lucene/util/hnsw/HnswGraphTestCase.java | 8 + 9 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 lucene/core/src/test/org/apache/lucene/index/TestHnswMergeAbort.java diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 245d97efe8c7..0414abf02717 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -83,6 +83,10 @@ Bug Fixes * GITHUB#16389: Fix double-counting of the underlying Automaton in AutomatonQuery#ramBytesUsed. (Sasilekha R) +* GITHUB#16367: HNSW graph construction now periodically checks whether the surrounding merge has + been aborted, so IndexWriter#rollback and abortMerges no longer block until the entire graph is + built. (Jeho Jeong) + Other --------------------- * GITHUB#16266: Remove deprecated search(Query, Collector) calls in QueryUtils by replacing diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java index ebfd3ec5470d..8beb791560eb 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java @@ -477,7 +477,8 @@ private void buildAndWriteGraph( mergeState.intraMergeTaskExecutor == null ? null : new TaskExecutor(mergeState.intraMergeTaskExecutor), - numMergeWorkers); + numMergeWorkers, + mergeState::checkAborted); for (int i = 0; i < mergeState.liveDocs.length; i++) { if (hasVectorValues(mergeState.fieldInfos[i], fieldInfo.name)) { merger.addReader( @@ -629,10 +630,11 @@ private HnswGraphMerger createGraphMerger( FieldInfo fieldInfo, RandomVectorScorerSupplier scorerSupplier, TaskExecutor parallelMergeTaskExecutor, - int numParallelMergeWorkers) { + int numParallelMergeWorkers, + IORunnable abortCheck) { if (mergeExec != null) { return new ConcurrentHnswMerger( - fieldInfo, scorerSupplier, M, beamWidth, mergeExec, numMergeWorkers); + fieldInfo, scorerSupplier, M, beamWidth, mergeExec, numMergeWorkers, abortCheck); } if (parallelMergeTaskExecutor != null && numParallelMergeWorkers > 1) { return new ConcurrentHnswMerger( @@ -641,9 +643,10 @@ private HnswGraphMerger createGraphMerger( M, beamWidth, parallelMergeTaskExecutor, - numParallelMergeWorkers); + numParallelMergeWorkers, + abortCheck); } - return new IncrementalHnswGraphMerger(fieldInfo, scorerSupplier, M, beamWidth); + return new IncrementalHnswGraphMerger(fieldInfo, scorerSupplier, M, beamWidth, abortCheck); } @Override diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/ConcurrentHnswMerger.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/ConcurrentHnswMerger.java index 9421dfb1dc87..f540d9f406e1 100644 --- a/lucene/core/src/java/org/apache/lucene/util/hnsw/ConcurrentHnswMerger.java +++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/ConcurrentHnswMerger.java @@ -29,6 +29,7 @@ import org.apache.lucene.search.TaskExecutor; import org.apache.lucene.util.BitSet; import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.IORunnable; /** This merger merges graph in a concurrent manner, by using {@link HnswConcurrentMergeBuilder} */ public class ConcurrentHnswMerger extends IncrementalHnswGraphMerger { @@ -46,7 +47,24 @@ public ConcurrentHnswMerger( int beamWidth, TaskExecutor taskExecutor, int numWorker) { - super(fieldInfo, scorerSupplier, M, beamWidth); + this(fieldInfo, scorerSupplier, M, beamWidth, taskExecutor, numWorker, null); + } + + /** + * @param fieldInfo FieldInfo for the field being merged + * @param abortCheck optional check invoked before every node insertion during graph construction; + * may throw {@link org.apache.lucene.index.MergePolicy.MergeAbortedException} to abort the + * build when the surrounding merge has been aborted, or null + */ + public ConcurrentHnswMerger( + FieldInfo fieldInfo, + RandomVectorScorerSupplier scorerSupplier, + int M, + int beamWidth, + TaskExecutor taskExecutor, + int numWorker, + IORunnable abortCheck) { + super(fieldInfo, scorerSupplier, M, beamWidth, abortCheck); this.taskExecutor = taskExecutor; this.numWorker = numWorker; } diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswBuilder.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswBuilder.java index 38109c9c95e2..43cef2cde8ee 100644 --- a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswBuilder.java +++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswBuilder.java @@ -19,6 +19,7 @@ import java.io.IOException; import org.apache.lucene.internal.hppc.IntHashSet; +import org.apache.lucene.util.IORunnable; import org.apache.lucene.util.InfoStream; /** @@ -46,6 +47,18 @@ public interface HnswBuilder { /** Set info-stream to output debugging information */ void setInfoStream(InfoStream infoStream); + /** + * Sets a check that is invoked before every node insertion during graph construction. The check + * may throw an exception to abort the build promptly, e.g. {@link + * org.apache.lucene.index.MergePolicy.MergeAbortedException} when the merge that triggered the + * build has been aborted. + * + *
The check must be non-null and can be set at most once.
+ *
+ * @throws IllegalStateException if the check was already set
+ */
+ void setAbortCheck(IORunnable abortCheck);
+
OnHeapHnswGraph getGraph();
/**
diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java
index d34a3c68bbf3..6a63ba596a64 100644
--- a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java
+++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java
@@ -31,6 +31,7 @@
import org.apache.lucene.search.TaskExecutor;
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.IORunnable;
import org.apache.lucene.util.InfoStream;
/**
@@ -136,6 +137,13 @@ public void setInfoStream(InfoStream infoStream) {
}
}
+ @Override
+ public void setAbortCheck(IORunnable abortCheck) {
+ for (HnswBuilder worker : workers) {
+ worker.setAbortCheck(abortCheck);
+ }
+ }
+
@Override
public OnHeapHnswGraph getCompletedGraph() throws IOException {
if (frozen == false) {
diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java
index 43cc3488fe1a..72ee257eaf75 100644
--- a/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java
+++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java
@@ -33,6 +33,7 @@
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.knn.KnnSearchStrategy;
import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.IORunnable;
import org.apache.lucene.util.InfoStream;
import org.apache.lucene.util.hnsw.HnswUtil.Component;
@@ -83,6 +84,7 @@ public class HnswGraphBuilder implements HnswBuilder {
protected final HnswLock hnswLock;
protected InfoStream infoStream = InfoStream.getDefault();
+ private IORunnable abortCheck;
protected boolean frozen;
/**
@@ -230,6 +232,15 @@ public void setInfoStream(InfoStream infoStream) {
this.infoStream = infoStream;
}
+ @Override
+ public void setAbortCheck(IORunnable abortCheck) {
+ Objects.requireNonNull(abortCheck);
+ if (this.abortCheck != null) {
+ throw new IllegalStateException("abort check was already set");
+ }
+ this.abortCheck = abortCheck;
+ }
+
@Override
public OnHeapHnswGraph getCompletedGraph() throws IOException {
if (!frozen) {
@@ -301,6 +312,9 @@ private void addGraphNodeInternal(int node, UpdateableRandomVectorScorer scorer,
if (frozen) {
throw new IllegalStateException("Graph builder is already frozen");
}
+ if (abortCheck != null) {
+ abortCheck.run();
+ }
final int nodeLevel = getRandomGraphLevel(ml, random);
// first add nodes to all levels
for (int level = nodeLevel; level >= 0; level--) {
diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/IncrementalHnswGraphMerger.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/IncrementalHnswGraphMerger.java
index 083c8f8c7c04..c94b1610ddc8 100644
--- a/lucene/core/src/java/org/apache/lucene/util/hnsw/IncrementalHnswGraphMerger.java
+++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/IncrementalHnswGraphMerger.java
@@ -34,6 +34,7 @@
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.IORunnable;
import org.apache.lucene.util.InfoStream;
/**
@@ -47,6 +48,7 @@ public class IncrementalHnswGraphMerger implements HnswGraphMerger {
protected final RandomVectorScorerSupplier scorerSupplier;
protected final int M;
protected final int beamWidth;
+ protected final IORunnable abortCheck;
protected List