Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
33039a3
Allow dynamic HNSW search threshold updates for collaborative search
krickert Feb 7, 2026
63eb03f
Introduce CollaborativeKnnCollector and Manager to core
krickert Feb 7, 2026
323c395
Clarify visibility semantics and apply formatting
krickert Feb 7, 2026
13a5960
Add CHANGES.txt entry for collaborative search
krickert Feb 7, 2026
50c6255
Add High-K and High-Dimension test scenarios for collaborative search
krickert Feb 7, 2026
2f36edd
Commit missing CollaborativeKnnCollector and Manager
krickert Feb 7, 2026
564f878
Cleanup extraneous newlines in TestCollaborativeHnswSearch
krickert Feb 7, 2026
3eb6a8e
Remove extraneous newlines and fix indentation in TestCollaborativeHn…
krickert Feb 7, 2026
5d1019d
Replace AtomicLong with AtomicInteger for global similarity threshold
krickert Feb 7, 2026
d90da2a
Add multi-segment collaborative pruning test for HNSW search
krickert Feb 7, 2026
88a5188
Add multi-index performance tests for collaborative HNSW pruning
krickert Feb 7, 2026
f7c8b63
Comprehensive multi-segment and multi-index collaborative tests
krickert Feb 7, 2026
66c7ad3
Fix forbiddenApis by adding Locale.ROOT to String.format
krickert Feb 7, 2026
f0dc67c
Mark multi-index collaborative tests as @Nightly
krickert Feb 7, 2026
c452d14
Move testHighKPruning and testHighDimensionPruning to @Nightly
krickert Feb 7, 2026
bbc0875
Idiomatic Collaborative HNSW search with LongAccumulator and DocScore…
krickert Feb 7, 2026
2e3c64c
Fix multi-index pruning bug and add recall measurement to tests
krickert Feb 7, 2026
17fba5c
Add definitive scaling and stress tests for collaborative search
krickert Feb 7, 2026
3476365
Cleanup and concurrent simulation for TestCollaborativeHnswSearch
krickert Feb 7, 2026
3c491fe
Accumulate floor score for high-recall pruning and add real-world seg…
krickert Feb 7, 2026
1f8cee1
Merge branch 'apache:main' into feature/collaborative-hnsw-search
krickert Feb 17, 2026
713325a
Feature/HNSW: Refine Collaborative Search for robust recall and distr…
krickert Feb 10, 2026
6d135a9
Feature/HNSW: Refine Collaborative Search for robust recall and distr…
krickert Feb 10, 2026
e5c894e
Lucene: implement Recall-Safe pruning using Global Floor vs Local Max…
krickert Feb 17, 2026
65b27bb
Lucene: implement topology-aware coordination with Hamming Affinity a…
krickert Feb 17, 2026
7677ac2
Restore CollaborativeKnnCollector Golden Logic (earlyTerminated, loca…
krickert Feb 17, 2026
9cc3d4c
Add 4-arg constructor for TestCollaborativeHnswSearch compatibility
krickert Feb 17, 2026
b330268
Fix trailing whitespace in CHANGES.txt
krickert Feb 18, 2026
d4adb69
Apply google-java-format via gradlew tidy
krickert Feb 18, 2026
926e5f4
Remove unused fields and imports to fix ecjLint failures
krickert Feb 18, 2026
7b77d6a
Apply google-java-format to CollaborativeKnnCollector
krickert Feb 18, 2026
56018b4
Fix forbiddenApis by using NamedThreadFactory in TestCollaborativeHns…
krickert Feb 18, 2026
2691faa
Apply google-java-format to TestCollaborativeHnswSearch
krickert Feb 18, 2026
f618bdd
Merge branch 'apache:main' into feature/collaborative-hnsw-search
krickert Feb 19, 2026
3276943
Merge branch 'main' into feature/collaborative-hnsw-search
krickert Apr 22, 2026
c7c7e07
Merge branch 'main' into feature/collaborative-hnsw-search
krickert Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lucene/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ API Changes
New Features
---------------------

* GITHUB#KNN-COLLAB: Introduce Collaborative HNSW search, allowing dynamic threshold
updates from collectors to enable cluster-wide search pruning. (ai-pipestream)

* GITHUB#15505: Upgrade snowball to 2d2e312df56f2ede014a4ffb3e91e6dea43c24be. New stemmer: PolishStemmer (and
PolishSnowballAnalyzer in the stempel package) (Justas Sakalauskas, Dawid Weiss)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.lucene.search;

import java.util.concurrent.atomic.AtomicInteger;
import org.apache.lucene.search.knn.KnnSearchStrategy;

/**
* A {@link KnnCollector} that allows for collaborative search by sharing a global minimum
* competitive similarity across multiple threads or nodes.
*
* <p>This collector wraps a {@link TopKnnCollector} and an {@link AtomicInteger} (storing float
* bits). It ensures that the search can be pruned by scores found in other concurrent search
* processes (e.g., other shards in a cluster).
*
* @lucene.experimental
*/
public class CollaborativeKnnCollector extends KnnCollector.Decorator {

private final AtomicInteger globalMinSimBits;

/**
* Create a new CollaborativeKnnCollector
*
* @param k number of neighbors to collect
* @param visitLimit maximum number of nodes to visit
* @param globalMinSimBits shared atomic float bits for global pruning
*/
public CollaborativeKnnCollector(int k, int visitLimit, AtomicInteger globalMinSimBits) {
this(new TopKnnCollector(k, visitLimit), globalMinSimBits);
}

/**
* Create a new CollaborativeKnnCollector with a search strategy
*
* @param k number of neighbors to collect
* @param visitLimit maximum number of nodes to visit
* @param searchStrategy search strategy to use
* @param globalMinSimBits shared atomic float bits for global pruning
*/
public CollaborativeKnnCollector(
int k, int visitLimit, KnnSearchStrategy searchStrategy, AtomicInteger globalMinSimBits) {
this(new TopKnnCollector(k, visitLimit, searchStrategy), globalMinSimBits);
}

private CollaborativeKnnCollector(KnnCollector delegate, AtomicInteger globalMinSimBits) {
super(delegate);
this.globalMinSimBits = globalMinSimBits;
}

@Override
public float minCompetitiveSimilarity() {
float localMin = super.minCompetitiveSimilarity();
float globalMin = Float.intBitsToFloat(globalMinSimBits.get());
return Math.max(localMin, globalMin);
}

/**
* Update the global minimum similarity if the provided score is higher.
*
* @param score the new potential global minimum
*/
public void updateGlobalMinSimilarity(float score) {
int newBits = Float.floatToRawIntBits(score);
while (true) {
int currentBits = globalMinSimBits.get();
if (score <= Float.intBitsToFloat(currentBits)) {
break;
}
if (globalMinSimBits.compareAndSet(currentBits, newBits)) {
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.lucene.search.knn;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.CollaborativeKnnCollector;
import org.apache.lucene.search.KnnCollector;

/**
* A {@link KnnCollectorManager} that creates {@link CollaborativeKnnCollector} instances sharing a
* single {@link AtomicInteger} for global pruning.
*
* @lucene.experimental
*/
public class CollaborativeKnnCollectorManager implements KnnCollectorManager {

private final int k;
private final AtomicInteger globalMinSimBits;

/**
* Create a new CollaborativeKnnCollectorManager
*
* @param k number of neighbors to collect
* @param globalMinSimBits shared atomic float bits for global pruning
*/
public CollaborativeKnnCollectorManager(int k, AtomicInteger globalMinSimBits) {
this.k = k;
this.globalMinSimBits = globalMinSimBits;
}

@Override
public KnnCollector newCollector(
int visitedLimit, KnnSearchStrategy searchStrategy, LeafReaderContext context)
throws IOException {
return new CollaborativeKnnCollector(k, visitedLimit, searchStrategy, globalMinSimBits);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,17 @@ void searchLevel(
// We should allow exploring equivalent minAcceptedSimilarity values at least once
boolean shouldExploreMinSim = true;
while (candidates.size() > 0 && results.earlyTerminated() == false) {
// Update the threshold dynamically from the collector to allow external pruning.
// This enables "Parallel-Collaborative" search where multiple shards/threads
// share a global high-score bar, typically via a bi-directional gRPC stream.
// Note: Visibility is guaranteed because the collector's minCompetitiveSimilarity()
// performs a volatile read (via AtomicLong) of the global bar.
float liveMinSimilarity = Math.nextUp(results.minCompetitiveSimilarity());
if (liveMinSimilarity > minAcceptedSimilarity) {
minAcceptedSimilarity = liveMinSimilarity;
shouldExploreMinSim = true;
}

// get the best candidate (closest or best scoring)
float topCandidateSimilarity = candidates.topScore();
if (topCandidateSimilarity < minAcceptedSimilarity) {
Expand Down
Loading
Loading