Skip to content

Commit

Permalink
SOLR-13748: Add support for mm parameter to bool query parser (#2025)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Andrey Bozhko <[email protected]>
Co-authored-by: Mikhail Khludnev <[email protected]>
  • Loading branch information
3 people authored Dec 8, 2023
1 parent a2f5e9e commit c0c2099
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 3 deletions.
2 changes: 2 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
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;
import org.apache.solr.request.SolrQueryRequest;
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}.
*
* <p>Example: <code>{!bool should=title:lucene should=title:solr must_not=id:1}</code>
*/
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,18 @@ protected BooleanQuery parseImpl() throws SyntaxError {

exclude(clauses.keySet());

BooleanQuery.Builder builder = new BooleanQuery.Builder();
BooleanQuery.Builder builder = createBuilder();
for (Map.Entry<QParser, Occur> clause : clauses.entrySet()) {
builder.add(unwrapQuery(clause.getKey().getQuery(), clause.getValue()), clause.getValue());
}
// what about empty query?
return builder.build();
}

protected BooleanQuery.Builder createBuilder() {
return new BooleanQuery.Builder();
}

protected Query unwrapQuery(Query query, BooleanClause.Occur occur) {
return query;
}
Expand Down
19 changes: 19 additions & 0 deletions solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
115 changes: 115 additions & 0 deletions solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
Original file line number Diff line number Diff line change
@@ -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}")));
}
}
16 changes: 16 additions & 0 deletions solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:

Expand Down

0 comments on commit c0c2099

Please sign in to comment.