diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 25084663bdf..9016bcfd727 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -293,6 +293,8 @@ Improvements * GITHUB#12966: Move most of the responsibility from TaxonomyFacets implementations to TaxonomyFacets itself. This reduces code duplication and enables future development. (Stefan Vodita) +* GITHUB#13362: Add sub query explanations to DisjunctionMaxQuery, if the overall query didn't match. (Tim Grein) + Optimizations --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java index 47e8b8f25e9..d011105f22d 100644 --- a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMaxQuery.java @@ -213,12 +213,13 @@ public final class DisjunctionMaxQuery extends Query implements Iterable boolean match = false; double max = 0; double otherSum = 0; - List subs = new ArrayList<>(); + List subsOnMatch = new ArrayList<>(); + List subsOnNoMatch = new ArrayList<>(); for (Weight wt : weights) { Explanation e = wt.explain(context, doc); if (e.isMatch()) { match = true; - subs.add(e); + subsOnMatch.add(e); double score = e.getValue().doubleValue(); if (score >= max) { otherSum += max; @@ -226,6 +227,8 @@ public final class DisjunctionMaxQuery extends Query implements Iterable } else { otherSum += score; } + } else if (match == false) { + subsOnNoMatch.add(e); } } if (match) { @@ -234,9 +237,9 @@ public final class DisjunctionMaxQuery extends Query implements Iterable tieBreakerMultiplier == 0.0f ? "max of:" : "max plus " + tieBreakerMultiplier + " times others of:"; - return Explanation.match(score, desc, subs); + return Explanation.match(score, desc, subsOnMatch); } else { - return Explanation.noMatch("No matching clause"); + return Explanation.noMatch("No matching clause", subsOnNoMatch); } } } // end of DisjunctionMaxWeight inner class diff --git a/lucene/core/src/test/org/apache/lucene/search/TestDisjunctionMaxQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestDisjunctionMaxQuery.java index e7385131d5f..12dc0ffc96b 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestDisjunctionMaxQuery.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestDisjunctionMaxQuery.java @@ -497,6 +497,57 @@ public class TestDisjunctionMaxQuery extends LuceneTestCase { doTestRandomTopDocs(4, 1.0f, 0.5f, 0.05f, 0f); } + public void testExplainMatch() throws IOException { + // Both match + Query sub1 = tq("hed", "elephant"); + Query sub2 = tq("dek", "elephant"); + + final DisjunctionMaxQuery dq = new DisjunctionMaxQuery(Arrays.asList(sub1, sub2), 0.0f); + + final Weight dw = s.createWeight(s.rewrite(dq), ScoreMode.COMPLETE, 1); + LeafReaderContext context = (LeafReaderContext) s.getTopReaderContext(); + Explanation explanation = dw.explain(context, 1); + + assertEquals("max of:", explanation.getDescription()); + // Two matching sub queries should be included in the explanation details + assertEquals(2, explanation.getDetails().length); + } + + public void testExplainNoMatch() throws IOException { + // No match + Query sub1 = tq("abc", "elephant"); + Query sub2 = tq("def", "elephant"); + + final DisjunctionMaxQuery dq = new DisjunctionMaxQuery(Arrays.asList(sub1, sub2), 0.0f); + + final Weight dw = s.createWeight(s.rewrite(dq), ScoreMode.COMPLETE, 1); + LeafReaderContext context = (LeafReaderContext) s.getTopReaderContext(); + Explanation explanation = dw.explain(context, 1); + + assertEquals("No matching clause", explanation.getDescription()); + // Two non-matching sub queries should be included in the explanation details + assertEquals(2, explanation.getDetails().length); + } + + public void testExplainMatch_OneNonMatchingSubQuery_NotIncludedInExplanation() + throws IOException { + // Matches + Query sub1 = tq("hed", "elephant"); + + // Doesn't match + Query sub2 = tq("def", "elephant"); + + final DisjunctionMaxQuery dq = new DisjunctionMaxQuery(Arrays.asList(sub1, sub2), 0.0f); + + final Weight dw = s.createWeight(s.rewrite(dq), ScoreMode.COMPLETE, 1); + LeafReaderContext context = (LeafReaderContext) s.getTopReaderContext(); + Explanation explanation = dw.explain(context, 1); + + assertEquals("max of:", explanation.getDescription()); + // Only the matching sub query (sub1) should be included in the explanation details + assertEquals(1, explanation.getDetails().length); + } + private void doTestRandomTopDocs(int numFields, double... freqs) throws IOException { assert numFields == freqs.length; Directory dir = newDirectory(); diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java b/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java index 3c6bb891c92..d422511dc0b 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/search/CheckHits.java @@ -417,7 +417,7 @@ public class CheckHits { if (descr.startsWith("score based on ") && descr.contains("child docs in range")) { assertTrue("Child doc explanations are missing", detail.length > 0); } - if (detail.length > 0) { + if (detail.length > 0 && expl.isMatch()) { if (detail.length == 1 && COMPUTED_FROM_PATTERN.matcher(descr).matches() == false) { // simple containment, unless it's a freq of: (which lets a query explain how the freq is // calculated),