-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a HNSW collector that exits early when nearest neighbor queue saturates #14094
Conversation
lucene/core/src/java/org/apache/lucene/search/HnswQueueSaturationCollector.java
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/HnswQueueSaturationCollector.java
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/HnswQueueSaturationCollector.java
Outdated
Show resolved
Hide resolved
lucene/core/src/test/org/apache/lucene/search/HnswQueueSaturationCollectorTest.java
Outdated
Show resolved
Hide resolved
public interface HnswKnnCollector extends KnnCollector { | ||
|
||
/** Indicates exploration of the next HNSW candidate graph node. */ | ||
void nextCandidate(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this kind of collector is OK. But it makes most sense to me to be a delegate collector. An abstract collector to KnnCollector.Delegate
.
Then, I also think that the OrdinalTranslatingKnnCollector
should inherit directly from HnswKnnCollector
always assuming that the passed in collector is a HnswKnnCollector
.
Note, the default behavior for HnswKnnCollector#nextCandidate
can simply be nothing, allowing for overriding.
This might require a new HnswGraphSearcher#search
interface to keep the old collector actions, but it can be simple to add a new one that accepts a HnswKnnCollector
and delegate to it with new HnswKnnCollector(KnnCollector delegate)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I adjusted my refactoring for the seeded queries similarly. It seems nicer IMO: #14170
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks Ben. I'll incorporate your suggestions once #14170 is in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made HnswKnnCollector
a KnnCollector.Decorator
in c6dbf7e
lucene/core/src/java/org/apache/lucene/search/HnswKnnCollector.java
Outdated
Show resolved
Hide resolved
lucene/core/src/test/org/apache/lucene/search/TestPatienceFloatVectorQuery.java
Outdated
Show resolved
Hide resolved
additional experiments with different quantization levels and filtering: No-fitleringBaseline
Candidate
FilteringBaseline
Candidate
the results are mostly good. I might see if I can improve the behavior with very selective filters ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for running those benchmarks. I think all the numbers look good.
My final concerns/questions are around the API.
Two ideas:
- can we make the API more general? Seems like it could be generally useful. Maybe we kick the can here...
- If we cannot make the API more general, or don't see the value in ever doing that, can we utilize a search strategy instead?
lucene/core/src/java/org/apache/lucene/util/hnsw/OrdinalTranslatedKnnCollector.java
Outdated
Show resolved
Hide resolved
lucene/core/src/java/org/apache/lucene/search/HnswQueueSaturationCollector.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Override | ||
public void nextCandidate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tteofili what do you think of making this more general? I think having a "nextCandidate" or "nextBlockOfVectors" is generally useful, and might be applicable to all types of kNN indices.
For example:
- Flat, you just get called once, indicating you are searching ALL vectors
- HNSW, you get called for each NSW (or in the case of filtered search, extended NSW)
- IVF, you get called for each posting list
- Vamana, you get called for each node before calling the neighbors
Do you think we can make this API general?
Maybe not, I am not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this idea Ben, I'll see if I can make up something reasonable for that ;)
* | ||
* @lucene.experimental | ||
*/ | ||
public abstract class HnswKnnCollector extends KnnCollector.Decorator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it is a little frustrating as we already have an "HNSWStrategy" and now we have an "HNSWCollector".
Could we utilize an HNSWStrategy? Or make nextCandidate
a more general API?
My thought on the strategy would be that the graph searcher to indicate through the strategy object when the next group of vectors will be searched and the strategy would have a reference to the collector to which it can forward the request.
Of course, this still requires a new HnswQueueSaturationCollector
, but it won't require these new base classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've spent some time trying to refactor this and extract a wider nextVectorsBlock
API, but it sounds like conflating too much into this PR, so I'd opt to "only" get rid of the HnswKnnCollector
class and rely on the strategy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a first step I've dropped HnswKnnCollector
in favor of adding the nextVectorsBlock
API to KnnCollector.Decorator
.
…atedKnnCollector.java Co-authored-by: Benjamin Trent <[email protected]>
@benwtrent I've reworked the design exposing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is OK for now. We really need to clean up these internal APIs, they are getting out of hand :)
@@ -139,6 +139,8 @@ New Features | |||
|
|||
* GITHUB#14412: Allow skip cache factor to be updated dynamically. (Sagar Upadhyaya) | |||
|
|||
* GITHUB#14094: New KNN query that early terminates when HNSW nearest neighbor queue saturates. (Tommaso Teofili) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10.2 is cut, so this will now be a 10.3 thing :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless this is being added to 10.2
…urates (#14094) * Add a HNSW early termination based on nearest neighbor queue saturation Co-authored-by: Benjamin Trent <[email protected]> (cherry picked from commit 525bf34)
…urates (#14094) * Add a HNSW early termination based on nearest neighbor queue saturation Co-authored-by: Benjamin Trent <[email protected]> (cherry picked from commit 525bf34)
This introduces a
HnswKnnCollector
interface, extendingKnnCollector
for HNSW, to make it possible to hook into HNSW execution for optimizations.It then adds a new collector which uses a saturation-based threshold to dynamically halt HNSW graph exploration, in order to early exit when the exploration of new candidates is unlikely to lead to addition of new neighbors.
The new collector records the number of added neighbors upon exploration of a new candidate (a HNSW node) and it compares it with the number of neighbors added while exploring the previous candidate, when the rate of added neighbors plateaus for a number of consecutive iterations, it stops graph exploration (
earlyTerminate
returnstrue
).