Return a non-matching explanation for documents that are not nearest neighbors - #3480
Open
hyunwoo-kurly wants to merge 1 commit into
Open
Conversation
…neighbors KNNWeight.explain() reported every document as a match, including documents the query does not match. getKnnScore() already knew whether the scorer landed on the document but collapsed that answer to a score of 0, and every return path of explain() was Explanation.match(...), so a document that is not among the segment's nearest neighbors received Explanation.match(0.0f, ...) whose isMatch() is true. That breaks the agreement Lucene expects between Weight.explain() and the scorer produced by the same Weight, and callers such as bool, script_score and function_score branch on isMatch() before using the sub-query scorer. explain() now uses the advance result directly and returns Explanation.noMatch(...) when the scorer does not land on the document. The score value cannot serve as the match signal on its own, because KNNScorer.score() multiplies by boost and a genuine nearest neighbor queried with a boost of 0 also scores 0. Documents that do match keep their existing behavior, and callers that pass a score of their own (disk-based search and DocAndScoreQuery) are unaffected. getKnnScore() had no other caller and is removed. Signed-off-by: hyunwoo-kurly <hayden.kim@kurlycorp.com>
hyunwoo-kurly
force-pushed
the
fix/issue-3479-knnweight-explain-nomatch
branch
from
August 3, 2026 00:30
150c9a4 to
87f58df
Compare
hyunwoo-kurly
marked this pull request as ready for review
August 3, 2026 00:31
hyunwoo-kurly
requested review from
0ctopus13prime,
VijayanB,
Vikasht34,
heemin32,
kotwanikunal,
luyuncheng,
martin-gaievski,
naveentatikonda,
navneet1v,
shatejas and
vamshin
as code owners
August 3, 2026 00:31
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
KNNWeight.explain()reported every document as a match, including documents the query does not match.explain(context, doc)(the radial search path) delegates toexplain(context, doc, 0), which computed the score throughgetKnnScore():That helper already knew whether the scorer landed on the document but collapsed the answer to a score of
0, and every return path ofexplain()wasExplanation.match(...). A document that is not among the segment's nearest neighbors therefore gotExplanation.match(0.0f, ...), whoseisMatch()istrue, which breaks the agreement Lucene expects betweenWeight.explain()and the scorer of the sameWeight.This change makes
explain()use the advance result directly and returnExplanation.noMatch(...)when the scorer does not land on the document. The score value cannot be used as the match signal on its own:KNNScorer.score()multiplies byboost, so a genuine nearest neighbor queried with"boost": 0also scores0.Documents that do match are unaffected: the score is still taken from
knnScorer.score()when the caller did not supply one, and callers that pass a non-zero score (disk-based search,DocAndScoreQuery) keep their existing path.getKnnScore()had no other caller and is removed.Testing:
ExplainTests#testExplain_whenDocIsNotANearestNeighbor_thenNoMatchasserts a non-matching explanation for a document outside the segment's result set. Reverting the guard makes it fail, because the old code returnsExplanation.match(0.0f, ...)../gradlew test --tests "*ExplainTests*"passes.Related Issues
Resolves #3479
Related: opensearch-project/OpenSearch#22619 and opensearch-project/OpenSearch#22624 cover the core side of the same disagreement, where a sub-query weight that explains a match while producing no scorer makes
ScriptScoreQuery.explain()throw aNullPointerException. This change fixes the k-NN side, which that core guard does not address.Check List
--signoff.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.