Skip to content

Conversation

@0ctopus13prime
Copy link
Collaborator

Description

RFC : #2924

Enable optimistic search for memory optimized search and deprecate MultiLeafKnnCollector which has an early termination logic.

This PR has three big changes:

  1. Now, when memory-optimized search is enabled, all queries use NativeEngineKnnVectorQuery.
    KnnQuery, which only provides a ScorerSupplier and performs search within a single leaf segment (with the resulting Scorer being consumed by an external BulkScorer under the standard Lucene search flow). But optimistic search requires coordination across segments. It needs to run an initial (first-phase) search, then identify and revisit only the segments likely to contain promising results.
    To support this coordinated two-phase process, NativeEngineKnnVectorQuery is a more suitable entry point than KnnQuery.

  2. Backported Lucene components required for optimistic search, specifically:
    2.1. ReentrantKnnCollectorManager
    2.2. SeededMappedDISI
    2.3. SeededTopDocsDISI

Related Issues

Resolves #[Issue number to be closed when this PR is merged]
#2924

Check List

  • [O] New functionality includes testing.
  • [O] New functionality has been documented.
  • [O] API changes companion pull request created.
  • [O] Commits are signed per the DCO using --signoff.
  • Public documentation issue/PR created.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

@0ctopus13prime
Copy link
Collaborator Author

@Vikasht34 @shatejas
All CI passed! Could you take a look at this when you have time?
Thank you

@Vikasht34
Copy link
Collaborator

Will look into this PR :- Tomorrow Morning

@0ctopus13prime 0ctopus13prime force-pushed the optimistic-srch branch 2 times, most recently from 5b006af to 1e09bf0 Compare October 9, 2025 05:06
);
}

/*
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic has been moved to approximateSearch

* An immutable, empty {@link BitSet} implementation used to represent
* the absence of filter bits without incurring null checks or allocations.
*/
public static final BitSet MATCH_ALL_BIT_SET = new BitSet() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why we are not uysing Lucene's MathAllBits or MatcNoBits?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not use both, as it's sub class of Bits while we need BitSet in here. 😵‍💫
Since optimistic search will call approximateSearch twice, we need to keep BitSet for reusing.

}

@Override
public int length() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we doing any iteration on Bitset , this gooona break if we are doing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only place using this one is when we're getting siblings in nested case.
In there, we don't do iteration.

@shatejas
Copy link
Collaborator

shatejas commented Oct 13, 2025

when memory-optimized search is enabled, all queries use NativeEngineKnnVectorQuery.

There are caveats to doing this,

  1. This will completely bypass the slicing logic, consuming more CPU for concurrent segment search. The concurrency will use all threads available for search. Allowing a shard to use all cores of CPU impacts other parts of the system and is deviating from existing behavior for cases where rescoring is not used
  2. This impacts the total hits count, moving it to the shard level will always show total hits as k, where as with segment level will add up each segment results in total count. This is a behavior change and can impact cases where users rely on total hit count
  3. This now returns k results on shard level, while its not a huge concern, it can affect results of single shard - single segment case when k < size for non-rescoring cases

I think 1 is a concern which needs discussion, 2 is manageable with some extra logic to keep behavior consistent, 3 isn't a big deal

@navneet1v
Copy link
Collaborator

when memory-optimized search is enabled, all queries use NativeEngineKnnVectorQuery.

There are caveats to doing this,

  1. This will completely bypass the slicing logic, consuming more CPU for concurrent segment search. The concurrency will use all threads available for search. Allowing a shard to use all cores of CPU impacts other parts of the system and is deviating from existing behavior for cases where rescoring is not used
  2. This impacts the total hits count, moving it to the shard level will always show total hits as k, where as with segment level will add up each segment results in total count. This is a behavior change and can impact cases where users rely on total hit count
  3. This now returns k results on shard level, while its not a huge concern, it can affect results of single shard - single segment case when k < size for non-rescoring cases

I think 1 is a concern which needs discussion, 2 is manageable with some extra logic to keep behavior consistent, 3 isn't a big deal

@shatejas from a user perspective all of this is a breaking change if we are making Lucene on Faiss default. So this needs to be documented in the docs to clearly callout the behavior and how to mitigate this. Along with this, we should ensure that older indices are still on the same non memory optimized based search so that upgrades are seamless.

We have already seen GH issues in part where changes in range of cosine scores lead to issues with users. #2561

@Setter
private KnnCollectorManager optimistic2ndKnnCollectorManager;

public static class OptimisticKnnCollectorManager implements KnnCollectorManager {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to a separate file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, will update in the next rev.

public PerLeafResult(final Bits filterBits, final TopDocs result) {
this.filterBits = filterBits == null ? new Bits.MatchAllBits(0) : filterBits;
// Indicates whether this result was produced via exact or approximate search.
private final SearchMode searchMode;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the use of this parameter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimistic search would do deep dive HNSW search with acquired top k results as seeds. And this will not be needed if the results acquired via exact search. Hence having search mode here, and let it bypass the second search if possible.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cases when results will be acquired from exact search is the filters case right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Whenever running exact search for whatever reason, it will set the mode as EXACT_SEARCH.
If that's the case, then we should not run 2nd search as it's pointless.

* An immutable, empty {@link BitSet} implementation used to represent
* the absence of filter bits without incurring null checks or allocations.
*/
public static final BitSet MATCH_ALL_BIT_SET = new BitSet() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

* @return a {@link TopDocs} object containing the top {@code k} approximate search results
* @throws IOException if an error occurs while reading index data or accessing vector fields
*/
public TopDocs approximateSearch(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we are making this function public?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this particular function in optimistic second search. Otherwise, if using searchLeaf, then we will end up building filter bitset twice.

@navneet1v
Copy link
Collaborator

navneet1v commented Oct 13, 2025

We are brining in a lot of code from Lucene. Please mention the source of the code for better maintainability.

One way I would think is to move the class to org.opensearch.lucene to ensure that we know these classes are merely copie/inspired from Lucene.

Vikasht34
Vikasht34 previously approved these changes Oct 14, 2025
Copy link
Collaborator

@Vikasht34 Vikasht34 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks Good !! Clean Code and Very Concise !! Thanks

@0ctopus13prime 0ctopus13prime force-pushed the optimistic-srch branch 3 times, most recently from e625f04 to 0cc0b54 Compare October 15, 2025 07:00
Comment on lines +67 to +76
/**
* A special flag used for testing purposes that forces execution of the second (exact) search
* in optimistic search mode, regardless of the results returned by the first approximate search.
* <p>
* This flag should never be enabled in production; it is intended for testing and debugging only.
*/
private static final boolean FORCE_REENTER_TESTING;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets remove this in next iteration of PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for testing the second reentrance in optimistic search.
It's bit tricky to make the data to ensure there's at least one segment whose min topk score is greater than the minimum score in the merged top-k results

Copy link
Collaborator

@navneet1v navneet1v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me.

I am not seeing how this new memory optimized search is getting integrated with the explain api.

Also let put the destination branch as main branch.

public PerLeafResult(final Bits filterBits, final TopDocs result) {
this.filterBits = filterBits == null ? new Bits.MatchAllBits(0) : filterBits;
// Indicates whether this result was produced via exact or approximate search.
private final SearchMode searchMode;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cases when results will be acquired from exact search is the filters case right?

Comment on lines +568 to +556
// Forcing optimistic search for testing
systemProperty 'mem_opt_srch.force_reenter', 'true'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to run search for memory optimized cases then lets create another gradle task and also a new CI that runs that task.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this to force it to run 2nd search in optimistic.
Since the 2nd search will kick off only if there's segment whose min score > the min score in merged results, it was tricky for me to make the data.

@0ctopus13prime 0ctopus13prime changed the base branch from feature/fp16-faiss-bulk to main October 15, 2025 17:22
@0ctopus13prime 0ctopus13prime dismissed Vikasht34’s stale review October 15, 2025 17:22

The base branch was changed.

@0ctopus13prime 0ctopus13prime force-pushed the optimistic-srch branch 3 times, most recently from ea03c31 to 21aa301 Compare October 16, 2025 03:05
@0ctopus13prime 0ctopus13prime force-pushed the optimistic-srch branch 4 times, most recently from 23325ed to 0f44ba7 Compare October 21, 2025 18:24
shatejas
shatejas previously approved these changes Oct 22, 2025
Copy link
Collaborator

@shatejas shatejas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, some minor comments

final PerLeafResult perLeafResult = perLeafResults.get(i);
final TopDocs perLeaf = perLeafResults.get(i).getResult();
if (perLeaf.scoreDocs.length > 0 && perLeafResult.getSearchMode() == PerLeafResult.SearchMode.APPROXIMATE_SEARCH) {
if (FORCE_REENTER_TESTING || perLeaf.scoreDocs[perLeaf.scoreDocs.length - 1].score >= minTopKScore) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Its not ideal to have a testing flag in final code, can't we create a test case where we mock scores?

Copy link
Collaborator Author

@0ctopus13prime 0ctopus13prime Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since whether if it kicks off the second search will be entirely depending on data. Which is hard to be made, and this was the only solution I found so far to always enforce it to enter 2nd search in optimistic.

@navneet1v
Copy link
Collaborator

@0ctopus13prime any reason why MacOS builds are failing?

@0ctopus13prime
Copy link
Collaborator Author

@navneet1v
It's timed out

2025-10-22T19:33:55.9624590Z REPRODUCE WITH: ./gradlew ':integTest' --tests 'org.opensearch.knn.index.FaissIT.testEndToEnd_whenDoRadiusSearch_whenDistanceThreshold_whenMethodIsHNSWFlat_thenSucceed' -Dtests.seed=FD2D022924B4F513 -Dtests.security.manager=false -Dtests.locale=jmc-Latn-TZ -Dtests.timezone=Etc/Zulu -Druntime.java=21
2025-10-22T19:33:55.9640570Z FaissIT > classMethod FAILED
2025-10-22T19:33:55.9643660Z 
2025-10-22T19:33:55.9644380Z     java.lang.Exception: Suite timeout exceeded (>= 1200000 msec).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants