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: