From f46d076c647e98fcb352a89d5db0bb8e64f4fc02 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:10:37 -0500 Subject: [PATCH 01/10] Utilize bulk scoring interface during HNSW graph builder diversity check --- .../lucene/util/hnsw/HnswGraphBuilder.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 2e55e3e06649..0f4c4870e293 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 @@ -67,6 +67,8 @@ public class HnswGraphBuilder implements HnswBuilder { protected final int M; // max number of connections on upper layers private final double ml; + private int[] bulkScoreNodes; // for bulk scoring + private float[] bulkScores; // for bulk scoring private final SplittableRandom random; protected final UpdateableRandomVectorScorer scorer; protected final HnswGraphSearcher graphSearcher; @@ -156,6 +158,8 @@ protected HnswGraphBuilder( this.hnsw = hnsw; this.hnswLock = hnswLock; this.graphSearcher = graphSearcher; + this.bulkScoreNodes = new int[this.M]; + this.bulkScores = new float[this.M]; entryCandidates = new GraphBuilderKnnCollector(1); beamCandidates = new GraphBuilderKnnCollector(beamWidth); beamCandidates0 = new GraphBuilderKnnCollector(Math.min(beamWidth / 2, M * 3)); @@ -470,12 +474,18 @@ static void popToScratch(GraphBuilderKnnCollector candidates, NeighborArray scra */ private boolean diversityCheck(float score, NeighborArray neighbors, RandomVectorScorer scorer) throws IOException { + // bulk score all neighbors, bulkScoreNodes is at most `M` need to handle paging over it + int bulkCount = 0; for (int i = 0; i < neighbors.size(); i++) { - float neighborSimilarity = scorer.score(neighbors.nodes()[i]); - if (neighborSimilarity >= score) { - return false; + bulkScoreNodes[bulkCount++] = neighbors.nodes()[i]; + if (bulkCount == bulkScoreNodes.length || i == neighbors.size() - 1) { + if (scorer.bulkScore(bulkScoreNodes, bulkScores, bulkCount) >= score) { + return false; + } + bulkCount = 0; } } + assert bulkCount == 0; return true; } From cb0dff2b40c407491c4eadec13915f449c4241d1 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:33:00 -0500 Subject: [PATCH 02/10] iter --- .../java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0f4c4870e293..110c2cda18b0 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 @@ -158,8 +158,8 @@ protected HnswGraphBuilder( this.hnsw = hnsw; this.hnswLock = hnswLock; this.graphSearcher = graphSearcher; - this.bulkScoreNodes = new int[this.M]; - this.bulkScores = new float[this.M]; + this.bulkScoreNodes = new int[8]; + this.bulkScores = new float[8]; entryCandidates = new GraphBuilderKnnCollector(1); beamCandidates = new GraphBuilderKnnCollector(beamWidth); beamCandidates0 = new GraphBuilderKnnCollector(Math.min(beamWidth / 2, M * 3)); From f7b2d29f44d609b69d4ec9c1766967fdf5350a91 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:05:03 -0500 Subject: [PATCH 03/10] iter --- .../src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 110c2cda18b0..2f4b8900d602 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 @@ -158,6 +158,8 @@ protected HnswGraphBuilder( this.hnsw = hnsw; this.hnswLock = hnswLock; this.graphSearcher = graphSearcher; + // pick a number that keeps us from scoring TOO much for diversity checking + // but enough to take advantage of bulk scoring this.bulkScoreNodes = new int[8]; this.bulkScores = new float[8]; entryCandidates = new GraphBuilderKnnCollector(1); @@ -474,7 +476,6 @@ static void popToScratch(GraphBuilderKnnCollector candidates, NeighborArray scra */ private boolean diversityCheck(float score, NeighborArray neighbors, RandomVectorScorer scorer) throws IOException { - // bulk score all neighbors, bulkScoreNodes is at most `M` need to handle paging over it int bulkCount = 0; for (int i = 0; i < neighbors.size(); i++) { bulkScoreNodes[bulkCount++] = neighbors.nodes()[i]; From b37d52f1828ea022a187b1bf02c4f78fd95494f1 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:07:39 -0500 Subject: [PATCH 04/10] iter --- .../java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2f4b8900d602..2b487ac063fc 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 @@ -67,8 +67,8 @@ public class HnswGraphBuilder implements HnswBuilder { protected final int M; // max number of connections on upper layers private final double ml; - private int[] bulkScoreNodes; // for bulk scoring - private float[] bulkScores; // for bulk scoring + private final int[] bulkScoreNodes; // for bulk scoring + private final float[] bulkScores; // for bulk scoring private final SplittableRandom random; protected final UpdateableRandomVectorScorer scorer; protected final HnswGraphSearcher graphSearcher; From cb92c93cee3a97d91d74e53c7b1310adba0790b2 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:22:50 -0500 Subject: [PATCH 05/10] adding changes, adjusting bulk chunk size --- lucene/CHANGES.txt | 3 +++ .../src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 174b14812a48..59e9898730e7 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -323,6 +323,9 @@ Optimizations * GITHUB#15592: Optimize DocIdSetIteratorAcceptDocs#cost. (Shubham Chaudhary) +* GITHUB#15607: Utilize bulk scoring for diversity checking when building HNSW vector indices. This results + in some performance improvements during indexing and segment merges. (Ben Trent) + Bug Fixes --------------------- * GITHUB#14161: PointInSetQuery's constructor now throws IllegalArgumentException 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 2b487ac063fc..5a9e7dc82091 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 @@ -477,9 +477,10 @@ static void popToScratch(GraphBuilderKnnCollector candidates, NeighborArray scra private boolean diversityCheck(float score, NeighborArray neighbors, RandomVectorScorer scorer) throws IOException { int bulkCount = 0; + final int bulkScoreChunk = Math.min((neighbors.nodes().length + 1) / 2, bulkScoreNodes.length); for (int i = 0; i < neighbors.size(); i++) { bulkScoreNodes[bulkCount++] = neighbors.nodes()[i]; - if (bulkCount == bulkScoreNodes.length || i == neighbors.size() - 1) { + if (bulkCount == bulkScoreChunk || i == neighbors.size() - 1) { if (scorer.bulkScore(bulkScoreNodes, bulkScores, bulkCount) >= score) { return false; } From 6e71ac1f8ae035ca04892d381be90acb784bf772 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:44:25 -0500 Subject: [PATCH 06/10] iter --- .../lucene/util/hnsw/HnswGraphBuilder.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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 5a9e7dc82091..ecb215a7d715 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 @@ -476,18 +476,23 @@ static void popToScratch(GraphBuilderKnnCollector candidates, NeighborArray scra */ private boolean diversityCheck(float score, NeighborArray neighbors, RandomVectorScorer scorer) throws IOException { - int bulkCount = 0; final int bulkScoreChunk = Math.min((neighbors.nodes().length + 1) / 2, bulkScoreNodes.length); - for (int i = 0; i < neighbors.size(); i++) { - bulkScoreNodes[bulkCount++] = neighbors.nodes()[i]; - if (bulkCount == bulkScoreChunk || i == neighbors.size() - 1) { - if (scorer.bulkScore(bulkScoreNodes, bulkScores, bulkCount) >= score) { - return false; - } - bulkCount = 0; + int scored = 0; + for (scored = 0; scored < neighbors.size(); scored += bulkScoreChunk) { + int chunkSize = Math.min(bulkScoreChunk, neighbors.size() - scored); + System.arraycopy(neighbors.nodes(), scored, bulkScoreNodes, 0, chunkSize); + if (scorer.bulkScore(bulkScoreNodes, bulkScores, chunkSize) >= score) { + return false; + } + } + // handle a tail + if (scored < neighbors.size()) { + int chunkSize = neighbors.size() - scored; + System.arraycopy(neighbors.nodes(), scored, bulkScoreNodes, 0, chunkSize); + if (scorer.bulkScore(bulkScoreNodes, bulkScores, chunkSize) >= score) { + return false; } } - assert bulkCount == 0; return true; } From aca7509e6d74fbdbc2b01c16978facdd7ffb4c07 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:02:07 -0500 Subject: [PATCH 07/10] fixing chunk size --- .../src/java/org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ecb215a7d715..ee7a73eb5d4d 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 @@ -476,7 +476,7 @@ static void popToScratch(GraphBuilderKnnCollector candidates, NeighborArray scra */ private boolean diversityCheck(float score, NeighborArray neighbors, RandomVectorScorer scorer) throws IOException { - final int bulkScoreChunk = Math.min((neighbors.nodes().length + 1) / 2, bulkScoreNodes.length); + final int bulkScoreChunk = Math.min((neighbors.size() + 1) / 2, bulkScoreNodes.length); int scored = 0; for (scored = 0; scored < neighbors.size(); scored += bulkScoreChunk) { int chunkSize = Math.min(bulkScoreChunk, neighbors.size() - scored); From 2ce3ace095e227ea5de83c7b83c006a2805eadb3 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:38:06 -0500 Subject: [PATCH 08/10] no tail needed, :facepalm: --- .../org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 8 -------- 1 file changed, 8 deletions(-) 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 ee7a73eb5d4d..0f2d8de9f297 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 @@ -485,14 +485,6 @@ private boolean diversityCheck(float score, NeighborArray neighbors, RandomVecto return false; } } - // handle a tail - if (scored < neighbors.size()) { - int chunkSize = neighbors.size() - scored; - System.arraycopy(neighbors.nodes(), scored, bulkScoreNodes, 0, chunkSize); - if (scorer.bulkScore(bulkScoreNodes, bulkScores, chunkSize) >= score) { - return false; - } - } return true; } From 1a602af98b9f3ecddeff571a01b6286929ae2f4a Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:44:53 -0500 Subject: [PATCH 09/10] iter --- .../org/apache/lucene/util/hnsw/HnswGraphBuilder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 0f2d8de9f297..c5fba5c88d44 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 @@ -64,6 +64,8 @@ public class HnswGraphBuilder implements HnswBuilder { @SuppressWarnings("NonFinalStaticField") public static long randSeed = DEFAULT_RAND_SEED; + private static final int MAX_BULK_SCORE_NODES = 8; + protected final int M; // max number of connections on upper layers private final double ml; @@ -160,8 +162,8 @@ protected HnswGraphBuilder( this.graphSearcher = graphSearcher; // pick a number that keeps us from scoring TOO much for diversity checking // but enough to take advantage of bulk scoring - this.bulkScoreNodes = new int[8]; - this.bulkScores = new float[8]; + this.bulkScoreNodes = new int[MAX_BULK_SCORE_NODES]; + this.bulkScores = new float[MAX_BULK_SCORE_NODES]; entryCandidates = new GraphBuilderKnnCollector(1); beamCandidates = new GraphBuilderKnnCollector(beamWidth); beamCandidates0 = new GraphBuilderKnnCollector(Math.min(beamWidth / 2, M * 3)); @@ -477,8 +479,7 @@ static void popToScratch(GraphBuilderKnnCollector candidates, NeighborArray scra private boolean diversityCheck(float score, NeighborArray neighbors, RandomVectorScorer scorer) throws IOException { final int bulkScoreChunk = Math.min((neighbors.size() + 1) / 2, bulkScoreNodes.length); - int scored = 0; - for (scored = 0; scored < neighbors.size(); scored += bulkScoreChunk) { + for (int scored = 0; scored < neighbors.size(); scored += bulkScoreChunk) { int chunkSize = Math.min(bulkScoreChunk, neighbors.size() - scored); System.arraycopy(neighbors.nodes(), scored, bulkScoreNodes, 0, chunkSize); if (scorer.bulkScore(bulkScoreNodes, bulkScores, chunkSize) >= score) { From 929fb6ca29305f155f9bd29d2a4d8b4f99908842 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:01:04 -0500 Subject: [PATCH 10/10] iter --- lucene/CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 7812ecfc704f..5e9ac8631264 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -338,7 +338,7 @@ Optimizations * GITHUB#15632: Use a coarser-grained competitive iterator with lower construction costs for numeric sorts against fields with DocValuesSkippers. (Alan Woodward) - + * GITHUB#15607: Utilize bulk scoring for diversity checking when building HNSW vector indices. This results in some performance improvements during indexing and segment merges. (Ben Trent)