This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit a511387c55149a1e02f03a3bfca0f20f91f23628 Author: Renato Haeberli <[email protected]> AuthorDate: Thu May 14 15:05:03 2026 +0200 BoolQParserPlugin: add percentage and threshold based minimum match functionality (#4406) (cherry picked from commit 3056ce9a8b2b65a947f6cb7e82a6f6b4399131a9) --- ...enhance-minimum-match-for-BoolQParserPlugin.yml | 7 +++ .../org/apache/solr/search/BoolQParserPlugin.java | 12 ++-- .../java/org/apache/solr/search/DisMaxQParser.java | 16 +---- .../apache/solr/search/ExtendedDismaxQParser.java | 2 +- .../java/org/apache/solr/util/SolrPluginUtils.java | 14 +++++ .../solr/search/TestMmBoolQParserPlugin.java | 68 ++++++++++++++++++---- 6 files changed, 88 insertions(+), 31 deletions(-) diff --git a/changelog/unreleased/PR#4406-enhance-minimum-match-for-BoolQParserPlugin.yml b/changelog/unreleased/PR#4406-enhance-minimum-match-for-BoolQParserPlugin.yml new file mode 100644 index 00000000000..52c118049cd --- /dev/null +++ b/changelog/unreleased/PR#4406-enhance-minimum-match-for-BoolQParserPlugin.yml @@ -0,0 +1,7 @@ +title: add percentage and threshold based minimum match functionality, as we know it from ExtendedDismaxQParser, to BoolQParserPlugin +type: changed +authors: +- name: Renato Haeberli +links: +- name: PR#4406 + url: https://github.com/apache/solr/pull/4406 diff --git a/solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java index 9d37fa590c8..8a0a8eb96db 100644 --- a/solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java +++ b/solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java @@ -22,10 +22,12 @@ import java.util.Map; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; +import org.apache.solr.common.params.DisMaxParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.query.FilterQuery; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.search.join.FiltersQParser; +import org.apache.solr.util.SolrPluginUtils; /** * Create a boolean query from sub queries. Sub queries can be marked as {@code must}, {@code @@ -46,10 +48,12 @@ public class BoolQParserPlugin extends QParserPlugin { } @Override - protected BooleanQuery.Builder createBuilder() { - BooleanQuery.Builder builder = super.createBuilder(); - builder.setMinimumNumberShouldMatch(localParams.getInt("mm", 0)); - return builder; + protected BooleanQuery parseImpl() throws SyntaxError { + BooleanQuery query = super.parseImpl(); + SolrParams solrParams = SolrParams.wrapDefaults(localParams, params); + String minShouldMatch = SolrPluginUtils.parseMinShouldMatch(req.getSchema(), solrParams); + boolean mmAutoRelax = params.getBool(DisMaxParams.MM_AUTORELAX, false); + return SolrPluginUtils.setMinShouldMatch(query, minShouldMatch, mmAutoRelax); } @Override diff --git a/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java b/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java index 79756bc1090..25e93592d35 100644 --- a/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java +++ b/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java @@ -28,7 +28,6 @@ import org.apache.solr.common.params.DisMaxParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.StrUtils; -import org.apache.solr.parser.QueryParser; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.schema.IndexSchema; import org.apache.solr.util.SolrPluginUtils; @@ -47,19 +46,6 @@ public class DisMaxQParser extends QParser { */ private static String IMPOSSIBLE_FIELD_NAME = "\uFFFC\uFFFC\uFFFC"; - /** - * Applies the appropriate default rules for the "mm" param based on the effective value of the - * "q.op" param - * - * @see QueryParsing#OP - * @see DisMaxParams#MM - */ - public static String parseMinShouldMatch(final IndexSchema schema, final SolrParams params) { - QueryParser.Operator op = QueryParsing.parseOP(params.get(QueryParsing.OP)); - - return params.get(DisMaxParams.MM, op.equals(QueryParser.Operator.AND) ? "100%" : "0%"); - } - /** * Uses {@link SolrPluginUtils#parseFieldBoosts(String)} with the 'qf' parameter. Falls back to * the 'df' parameter @@ -248,7 +234,7 @@ public class DisMaxQParser extends QParser { String userQuery, SolrPluginUtils.DisjunctionMaxQueryParser up, SolrParams solrParams) throws SyntaxError { - String minShouldMatch = parseMinShouldMatch(req.getSchema(), solrParams); + String minShouldMatch = SolrPluginUtils.parseMinShouldMatch(req.getSchema(), solrParams); Query dis = up.parse(userQuery); Query query = dis; diff --git a/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java b/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java index b1dcb910a9d..4dd2745bf1e 100644 --- a/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java +++ b/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java @@ -1723,7 +1723,7 @@ public class ExtendedDismaxQParser extends QParser { solrParams = SolrParams.wrapDefaults(localParams, params); schema = req.getSchema(); // req.getSearcher() here causes searcher refcount imbalance - minShouldMatch = DisMaxQParser.parseMinShouldMatch(schema, solrParams); + minShouldMatch = SolrPluginUtils.parseMinShouldMatch(schema, solrParams); userFields = new UserFields(U.parseFieldBoosts(solrParams.getParams(DMP.UF))); try { // req.getSearcher() here causes searcher refcount imbalance diff --git a/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java b/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java index b32b77846d2..ed6178be4f9 100644 --- a/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java +++ b/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java @@ -50,6 +50,7 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.DisMaxParams; import org.apache.solr.common.params.MapSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.CollectionUtil; @@ -605,6 +606,19 @@ public class SolrPluginUtils { } } + /** + * Applies the appropriate default rules for the "mm" param based on the effective value of the + * "q.op" param + * + * @see QueryParsing#OP + * @see DisMaxParams#MM + */ + public static String parseMinShouldMatch(final IndexSchema schema, final SolrParams params) { + QueryParser.Operator op = QueryParsing.parseOP(params.get(QueryParsing.OP)); + + return params.get(DisMaxParams.MM, op.equals(QueryParser.Operator.AND) ? "100%" : "0%"); + } + public static void setMinShouldMatch(BooleanQuery.Builder q, String spec) { setMinShouldMatch(q, spec, false); } diff --git a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java index 57790c330d7..988fa6767d4 100644 --- a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java +++ b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java @@ -30,6 +30,14 @@ import org.junit.Test; public class TestMmBoolQParserPlugin extends SolrTestCaseJ4 { + private static BooleanQuery.Builder shouldBuilder(String... terms) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String term : terms) { + builder.add(new TermQuery(new Term("name", term)), BooleanClause.Occur.SHOULD); + } + return builder; + } + @BeforeClass public static void beforeClass() throws Exception { initCore("solrconfig.xml", "schema.xml"); @@ -61,12 +69,55 @@ public class TestMmBoolQParserPlugin extends SolrTestCaseJ4 { parseQuery(req("q", "{!bool should=name:foo should=name:bar should=name:qux mm=2}")); BooleanQuery expected = - new BooleanQuery.Builder() - .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.SHOULD) - .add(new TermQuery(new Term("name", "bar")), BooleanClause.Occur.SHOULD) - .add(new TermQuery(new Term("name", "qux")), BooleanClause.Occur.SHOULD) - .setMinimumNumberShouldMatch(2) - .build(); + shouldBuilder("foo", "bar", "qux").setMinimumNumberShouldMatch(2).build(); + + assertEquals(expected, actual); + } + + @Test + public void testMinShouldMatchPercentage75() throws Exception { + Query actual = + parseQuery(req("q", "{!bool should=name:foo should=name:bar should=name:qux mm=75%}")); + + BooleanQuery expected = + shouldBuilder("foo", "bar", "qux").setMinimumNumberShouldMatch(2).build(); + + assertEquals(expected, actual); + } + + @Test + public void testMinShouldMatchPercentage50() throws Exception { + Query actual = + parseQuery(req("q", "{!bool should=name:foo should=name:bar should=name:qux mm=50%}")); + + BooleanQuery expected = + shouldBuilder("foo", "bar", "qux").setMinimumNumberShouldMatch(1).build(); + + assertEquals(expected, actual); + } + + @Test + public void testMinShouldMatchThresholdsLower() throws Exception { + Query actual = + parseQuery( + req("q", "{!bool should=name:foo should=name:bar should=name:qux mm='2<-1 5<-2'}")); + + BooleanQuery expected = + shouldBuilder("foo", "bar", "qux").setMinimumNumberShouldMatch(2).build(); + + assertEquals(expected, actual); + } + + @Test + public void testMinShouldMatchThresholdsUpper() throws Exception { + Query actual = + parseQuery( + req( + "q", + "{!bool should=name:foo should=name:bar should=name:qux should=name:n1 should=name:n2 should=name:n3 mm='2<-1 5<-2'}")); + + BooleanQuery expected = + shouldBuilder("foo", "bar", "qux", "n1", "n2", "n3").setMinimumNumberShouldMatch(4).build(); assertEquals(expected, actual); } @@ -102,11 +153,6 @@ public class TestMmBoolQParserPlugin extends SolrTestCaseJ4 { @Test public void testInvalidMinShouldMatchThrowsException() { - expectThrows( - SolrException.class, - NumberFormatException.class, - () -> parseQuery(req("q", "{!bool should=name:foo mm=20%}"))); - expectThrows( SolrException.class, NumberFormatException.class,
