Skip to content

Fix NPE explaining function_score when the sub-query produces no scorer - #22635

Open
hyunwoo-kurly wants to merge 1 commit into
opensearch-project:mainfrom
hyunwoo-kurly:fix/issue-22634-function-score-explain-npe
Open

Fix NPE explaining function_score when the sub-query produces no scorer#22635
hyunwoo-kurly wants to merge 1 commit into
opensearch-project:mainfrom
hyunwoo-kurly:fix/issue-22634-function-score-explain-npe

Conversation

@hyunwoo-kurly

Copy link
Copy Markdown

Description

FunctionScoreQuery.CustomBoostFactorWeight.explain() dereferenced the scorer from functionScorer(context) without a null check:

FunctionFactorScorer scorer = functionScorer(context);
int actualDoc = scorer.iterator().advance(doc);

functionScorer() returns null when subQueryWeight.scorer(context) is null, so a sub-query whose weight explains a document as a match while producing no scorer for that segment made explain() throw a NullPointerException. Since explain() runs in the fetch phase, one such clause failed the whole search request for requests that succeed fine with explain disabled.

scorerSupplier() in the same weight returns null for the equivalent state, which means "no matches on this segment". This change makes explain() agree with it: a null scorer now yields Explanation.noMatch(...) wrapping the sub-query explanation instead of dereferencing the null scorer.

The assert (actualDoc == doc) below the new guard is deliberately left alone. A sub-query that returns a scorer which does not position on the document is the same class of disagreement, but handling it changes the explanation for a different state and is better done as its own change once this one lands.

Testing:

  • FunctionScoreTests#testExplainFunctionScoreQueryWhenSubQueryHasNoScorer covers the scorer/explain mismatch with a sub-query whose weight always explains a match and never returns a scorer. Reverting the guard makes it fail with the NullPointerException described above.
  • ./gradlew :server:test --tests "org.opensearch.index.query.functionscore.FunctionScoreTests" passes.

The CHANGELOG is not updated, since as of 3.6 it is no longer used to generate release notes (#21071).

Related Issues

Resolves #22634

Same defect class as #18446, fixed by #19650 in scorerSupplier(), and #22619, which #22624 addresses in ScriptScoreQuery.explain(). FunctionScoreQuery.explain() is the remaining site, which neither change touches, so this PR does not overlap with #22624.

Check List

  • Functionality includes testing.
  • API changes companion pull request created, if applicable.
  • Public documentation issue/PR created, if applicable.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

FunctionScoreQuery.CustomBoostFactorWeight.explain() dereferenced the scorer
returned by functionScorer(context) without a null check. functionScorer()
returns null when subQueryWeight.scorer(context) is null, so a sub-query whose
weight explains a document as a match while producing no scorer for that
segment made explain() throw a NullPointerException. Because explain() runs in
the fetch phase, one such clause failed the whole search request for requests
that succeed fine with explain disabled.

scorerSupplier() in the same weight returns null for the equivalent state,
which means "no matches on this segment". explain() now agrees with it and
returns Explanation.noMatch(...) wrapping the sub-query explanation instead of
dereferencing the null scorer.

The assert below the new guard is left alone. A sub-query that returns a scorer
which does not position on the document is the same class of disagreement, but
it changes the explanation for a different state and is better handled on its
own.

Signed-off-by: hyunwoo-kurly <hayden.kim@kurlycorp.com>
@github-actions github-actions Bot added the Search Search query, autocomplete ...etc label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against scorer not matching document

After advancing the scorer's iterator to doc, verify that actualDoc == doc before
computing the score. If the scorer exists but does not actually match this document
on the segment, advance may return a different doc id (or NO_MORE_DOCS), and calling
computeScore(doc, ...) would produce incorrect results. Return Explanation.noMatch
in that case as well, rather than relying only on an assert which is disabled in
production.

server/src/main/java/org/opensearch/common/lucene/search/function/FunctionScoreQuery.java [475-483]

 FunctionFactorScorer scorer = functionScorer(context);
 if (scorer == null) {
-    // The sub-query explains this document as a match but produces no scorer for this segment.
-    // functionScorer() returns null there, and scorerSupplier() treats that state as "no matches
-    // on this segment", so report no match instead of dereferencing a null scorer.
     return Explanation.noMatch("sub-query produced no scorer for this segment", expl);
 }
 int actualDoc = scorer.iterator().advance(doc);
-assert (actualDoc == doc);
+if (actualDoc != doc) {
+    return Explanation.noMatch("sub-query scorer did not match this document", expl);
+}
Suggestion importance[1-10]: 4

__

Why: The suggestion identifies a valid concern about replacing an assert with runtime checking, but this is pre-existing behavior unrelated to the PR's fix and the assert may be intentional given the caller's contract. Moderate impact at best.

Low

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 3e2134c: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

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

Labels

duplicate This issue or pull request already exists Search Search query, autocomplete ...etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] NullPointerException in FunctionScoreQuery.explain() when the sub-query produces no scorer

1 participant