You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by mk...@apache.org on 2023/12/08 07:39:58 UTC

(solr) branch main updated: SOLR-13748: Add support for mm parameter to bool query parser (#2025)

This is an automated email from the ASF dual-hosted git repository.

mkhl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new c0c20998991 SOLR-13748: Add support for mm parameter to bool query parser (#2025)
c0c20998991 is described below

commit c0c2099899180b9d77002f3749a664b931a72698
Author: Andrey Bozhko <an...@gmail.com>
AuthorDate: Fri Dec 8 01:39:53 2023 -0600

    SOLR-13748: Add support for mm parameter to bool query parser (#2025)
    
    
    ---------
    
    Co-authored-by: Andrey Bozhko <ab...@apple.com>
    Co-authored-by: Mikhail Khludnev <mk...@apache.org>
---
 solr/CHANGES.txt                                   |   2 +
 .../org/apache/solr/search/BoolQParserPlugin.java  |  12 ++-
 .../apache/solr/search/join/FiltersQParser.java    |   6 +-
 .../org/apache/solr/search/QueryEqualityTest.java  |  19 ++++
 .../solr/search/TestMmBoolQParserPlugin.java       | 115 +++++++++++++++++++++
 .../modules/query-guide/pages/other-parsers.adoc   |  16 +++
 6 files changed, 167 insertions(+), 3 deletions(-)

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 @@ package org.apache.solr.search;
 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.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>
  */
@@ -44,6 +45,13 @@ public class BoolQParserPlugin extends QParserPlugin {
         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 @@ public class FiltersQParser extends QParser {
 
     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());
     }
@@ -65,6 +65,10 @@ public class FiltersQParser extends QParser {
     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 class QueryEqualityTest extends SolrTestCaseJ4 {
     }
   }
 
+  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: