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 000000000000..52c118049cd9 --- /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 9d37fa590c87..8a0a8eb96dba 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 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 Query parse() throws SyntaxError { } @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 79756bc1090a..25e93592d35d 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.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 @@ protected Query getUserQuery( 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 b1dcb910a9d0..4dd2745bf1eb 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 ExtendedDismaxConfiguration( 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 b32b77846d27..ed6178be4f9b 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.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 static void setMinShouldMatch(BooleanQuery.Builder q, String spec, boolea } } + /** + * 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 57790c330d71..988fa6767d40 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 @@ 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 void testMinShouldMatch() throws Exception { 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 void testExcludeTags() throws Exception { @Test public void testInvalidMinShouldMatchThrowsException() { - expectThrows( - SolrException.class, - NumberFormatException.class, - () -> parseQuery(req("q", "{!bool should=name:foo mm=20%}"))); - expectThrows( SolrException.class, NumberFormatException.class,