diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 077c334c186..e24ccafae2f 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -97,6 +97,8 @@ Improvements
* SOLR-16907: Fail when parsing an invalid custom permission definition from security.json (janhoy, Uwe Schindler)
+* SOLR-13748: Add support for mm (min should match) parameter to bool query parser (Andrey Bozhko)
+
* SOLR-17046: SchemaCodecFactory is now the implicit default codec factory. (hossman)
* SOLR-16397: Swap core v2 endpoints have been updated to be more REST-ful.
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 d0f36d762d3..9d37fa590c8 100644
--- a/solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java
@@ -20,6 +20,7 @@
import java.util.IdentityHashMap;
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.SolrParams;
import org.apache.solr.query.FilterQuery;
@@ -27,8 +28,8 @@
import org.apache.solr.search.join.FiltersQParser;
/**
- * Create a boolean query from sub queries. Sub queries can be marked as must, must_not, filter or
- * should
+ * Create a boolean query from sub queries. Sub queries can be marked as {@code must}, {@code
+ * must_not}, {@code filter}, or {@code should}.
*
*
Example: {!bool should=title:lucene should=title:solr must_not=id:1}
*/
@@ -44,6 +45,13 @@ public Query parse() throws SyntaxError {
return parseImpl();
}
+ @Override
+ protected BooleanQuery.Builder createBuilder() {
+ BooleanQuery.Builder builder = super.createBuilder();
+ builder.setMinimumNumberShouldMatch(localParams.getInt("mm", 0));
+ return builder;
+ }
+
@Override
protected Query unwrapQuery(Query query, BooleanClause.Occur occur) {
if (occur == BooleanClause.Occur.FILTER) {
diff --git a/solr/core/src/java/org/apache/solr/search/join/FiltersQParser.java b/solr/core/src/java/org/apache/solr/search/join/FiltersQParser.java
index c2397cd7061..0766a8f46d5 100644
--- a/solr/core/src/java/org/apache/solr/search/join/FiltersQParser.java
+++ b/solr/core/src/java/org/apache/solr/search/join/FiltersQParser.java
@@ -57,7 +57,7 @@ protected BooleanQuery parseImpl() throws SyntaxError {
exclude(clauses.keySet());
- BooleanQuery.Builder builder = new BooleanQuery.Builder();
+ BooleanQuery.Builder builder = createBuilder();
for (Map.Entry clause : clauses.entrySet()) {
builder.add(unwrapQuery(clause.getKey().getQuery(), clause.getValue()), clause.getValue());
}
@@ -65,6 +65,10 @@ protected BooleanQuery parseImpl() throws SyntaxError {
return builder.build();
}
+ protected BooleanQuery.Builder createBuilder() {
+ return new BooleanQuery.Builder();
+ }
+
protected Query unwrapQuery(Query query, BooleanClause.Occur occur) {
return query;
}
diff --git a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
index bbdad73b4d0..0a03521c87b 100644
--- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
+++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
@@ -1524,6 +1524,25 @@ public void testPayloadFunction() throws Exception {
}
}
+ public void testBoolMmQuery() throws Exception {
+ assertQueryEquals(
+ "lucene",
+ "{!bool should=foo_s:a should=foo_s:b}",
+ "{!bool should=foo_s:a should=foo_s:b mm=0}");
+ assertQueryEquals(
+ "lucene",
+ "{!bool should=foo_s:a should=foo_s:b mm=1}",
+ "{!bool should=foo_s:a should=foo_s:b mm=1}");
+ expectThrows(
+ AssertionError.class,
+ "queries should not have been equal",
+ () ->
+ assertQueryEquals(
+ "lucene",
+ "{!bool should=foo_s:a should=foo_s:b mm=1}",
+ "{!bool should=foo_s:a should=foo_s:b}"));
+ }
+
public void testBoolQuery() throws Exception {
assertQueryEquals(
"bool",
diff --git a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
new file mode 100644
index 00000000000..57790c330d7
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.search;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.request.SolrQueryRequest;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestMmBoolQParserPlugin extends SolrTestCaseJ4 {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig.xml", "schema.xml");
+ }
+
+ private static Query parseQuery(SolrQueryRequest req) throws Exception {
+ QParser parser = QParser.getParser(req.getParams().get("q"), req);
+ return parser.getQuery();
+ }
+
+ @Test
+ public void testBooleanQuery() throws Exception {
+ Query actual = parseQuery(req("q", "{!bool must=name:foo should=name:bar should=name:qux}"));
+
+ BooleanQuery expected =
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.MUST)
+ .add(new TermQuery(new Term("name", "bar")), BooleanClause.Occur.SHOULD)
+ .add(new TermQuery(new Term("name", "qux")), BooleanClause.Occur.SHOULD)
+ .setMinimumNumberShouldMatch(0)
+ .build();
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testMinShouldMatch() throws Exception {
+ Query actual =
+ 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();
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testNoClauses() throws Exception {
+ Query actual = parseQuery(req("q", "{!bool}"));
+
+ BooleanQuery expected = new BooleanQuery.Builder().build();
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testExcludeTags() throws Exception {
+ Query actual =
+ parseQuery(
+ req(
+ "q",
+ "{!bool must=$ref excludeTags=t2}",
+ "ref",
+ "{!tag=t1}foo",
+ "ref",
+ "{!tag=t2}bar",
+ "df",
+ "name"));
+
+ BooleanQuery expected =
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("name", "foo")), BooleanClause.Occur.MUST)
+ .build();
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testInvalidMinShouldMatchThrowsException() {
+ expectThrows(
+ SolrException.class,
+ NumberFormatException.class,
+ () -> parseQuery(req("q", "{!bool should=name:foo mm=20%}")));
+
+ expectThrows(
+ SolrException.class,
+ NumberFormatException.class,
+ () -> parseQuery(req("q", "{!bool should=name:foo mm=2.9}")));
+ }
+}
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
index a69fd3e9e97..56023635026 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
@@ -75,6 +75,17 @@ However, unlike `must`, the score of filter queries is ignored.
Also, these queries are cached in filter cache.
To avoid caching add either `cache=false` as local parameter, or `"cache":"false"` property to underneath Query DLS Object.
+`mm`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `0`
+|===
++
+The number of optional clauses that must match. By default, no optional clauses are necessary for a match
+(unless there are no required clauses). If this parameter is set, then the specified number of `should` clauses is required.
+If this parameter is not set, the usual rules about boolean queries still apply at search time - that is, a boolean query containing no required clauses must still match at least one optional clause.
+
`excludeTags`::
+
[%autowidth,frame=none]
@@ -97,6 +108,11 @@ See explanation below.
{!bool filter=foo should=bar}
----
+[source,text]
+----
+{!bool should=foo should=bar should=qux mm=2}
+----
+
Parameters might also be multivalue references.
The former example above is equivalent to: