Copilot commented on code in PR #4475:
URL: https://github.com/apache/solr/pull/4475#discussion_r3455530007


##########
solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java:
##########
@@ -73,11 +72,10 @@ protected Query unwrapQuery(Query query, 
BooleanClause.Occur occur) {
       @Override
       protected Map<QParser, BooleanClause.Occur> clauses() throws SyntaxError 
{
         IdentityHashMap<QParser, BooleanClause.Occur> clauses = new 
IdentityHashMap<>();
-        SolrParams solrParams = SolrParams.wrapDefaults(localParams, params);
-        addQueries(clauses, solrParams.getParams("must"), 
BooleanClause.Occur.MUST);
-        addQueries(clauses, solrParams.getParams("must_not"), 
BooleanClause.Occur.MUST_NOT);
-        addQueries(clauses, solrParams.getParams("filter"), 
BooleanClause.Occur.FILTER);
-        addQueries(clauses, solrParams.getParams("should"), 
BooleanClause.Occur.SHOULD);
+        addQueries(clauses, localParams.getParams("must"), 
BooleanClause.Occur.MUST);
+        addQueries(clauses, localParams.getParams("must_not"), 
BooleanClause.Occur.MUST_NOT);
+        addQueries(clauses, localParams.getParams("filter"), 
BooleanClause.Occur.FILTER);
+        addQueries(clauses, localParams.getParams("should"), 
BooleanClause.Occur.SHOULD);
         return clauses;

Review Comment:
   `clauses()` now pulls `must`/`filter`/`should` exclusively from 
`localParams`, which can be null and also prevents callers from specifying 
these as normal request params when using `defType=bool` or `{!bool}` with 
defaults. Restore the wrapped-default params lookup so local params override 
request params but still allow request-level configuration.



##########
solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java:
##########
@@ -109,6 +110,40 @@ public void testMinShouldMatchThresholdsLower() throws 
Exception {
     assertEquals(expected, actual);
   }
 
+  @Test
+  public void testEmptyStopWordClauseShouldCountForMMWithAutoRelaxFalse() 
throws Exception {
+
+    Query actual =
+        parseQuery(
+            req(
+                "q",
+                "{!bool should=name:foo should=name:bar should=teststop:to 
mm=-1 mm.autoRelax=false}"));
+
+    BooleanQuery expected =
+        shouldBuilder("foo", "bar")
+            .setMinimumNumberShouldMatch(2)
+            .add(new MatchNoDocsQuery(""), BooleanClause.Occur.SHOULD)

Review Comment:
   The parser now creates `new MatchNoDocsQuery("empty query")` for 
stopword-only clauses (see `SolrQueryParserBase#createFieldQuery`), so this 
expected query should use the same reason string (or the no-arg ctor) to keep 
`Query.equals` stable.



##########
solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java:
##########
@@ -109,6 +110,40 @@ public void testMinShouldMatchThresholdsLower() throws 
Exception {
     assertEquals(expected, actual);
   }
 
+  @Test
+  public void testEmptyStopWordClauseShouldCountForMMWithAutoRelaxFalse() 
throws Exception {
+
+    Query actual =
+        parseQuery(
+            req(
+                "q",
+                "{!bool should=name:foo should=name:bar should=teststop:to 
mm=-1 mm.autoRelax=false}"));
+
+    BooleanQuery expected =
+        shouldBuilder("foo", "bar")
+            .setMinimumNumberShouldMatch(2)
+            .add(new MatchNoDocsQuery(""), BooleanClause.Occur.SHOULD)
+            .build();
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void testEmptyStopWordClauseShouldNotCountForMMWithAutoRelaxTrue() 
throws Exception {
+
+    Query actual =
+        parseQuery(
+            req(
+                "q",
+                "{!bool should=name:foo should=name:bar should=teststop:to 
mm=-1 mm.autoRelax=true}"));
+
+    BooleanQuery expected =
+        shouldBuilder("foo", "bar")
+            .setMinimumNumberShouldMatch(1)
+            .add(new MatchNoDocsQuery(""), BooleanClause.Occur.SHOULD)

Review Comment:
   Same issue as above: `MatchNoDocsQuery` equality includes the reason string, 
so the expected query should match the production reason (or use the no-arg 
ctor consistently).



##########
solr/core/src/java/org/apache/solr/search/BoolQParserPlugin.java:
##########
@@ -50,9 +50,8 @@ public Query parse() throws SyntaxError {
       @Override
       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);
+        String minShouldMatch = 
SolrPluginUtils.parseMinShouldMatch(req.getSchema(), localParams);
+        boolean mmAutoRelax = localParams.getBool(DisMaxParams.MM_AUTORELAX, 
false);
         return SolrPluginUtils.setMinShouldMatch(query, minShouldMatch, 
mmAutoRelax);

Review Comment:
   `BoolQParserPlugin` now reads `mm` / `mm.autoRelax` only from `localParams`. 
This breaks existing behavior where these can be supplied as request params 
(e.g., `q.op=AND` affecting the default `mm`), and it can also throw an NPE if 
the parser is used without local params (e.g., `defType=bool` where 
`localParams` may be null). Use `SolrParams.wrapDefaults(localParams, params)` 
like before so local params override request params but still fall back 
correctly.



##########
solr/core/src/test/org/apache/solr/util/SolrPluginUtilsTest.java:
##########
@@ -124,119 +124,149 @@ public void testParseFieldBoosts() {
     assertEquals("spacey e2", e2, SolrPluginUtils.parseFieldBoosts("   \t   
"));
   }
 
-  @Test
-  public void testDisjunctionMaxQueryParser() throws Exception {
-
-    Query out;
-    String t;
-
-    SolrQueryRequest req = req("df", "text");
-    QParser qparser = QParser.getParser("hi", "dismax", req);
-
-    DisjunctionMaxQueryParser qp =
-        new SolrPluginUtils.DisjunctionMaxQueryParser(qparser, 
req.getParams().get("df"));
-
-    qp.addAlias(
-        "hoss",
-        0.01f,
-        SolrPluginUtils.parseFieldBoosts("title^2.0 title_stemmed name^1.2 
subject^0.5"));
-    qp.addAlias("test", 0.01f, SolrPluginUtils.parseFieldBoosts("text^2.0"));
-    qp.addAlias("unused", 1.0f, SolrPluginUtils.parseFieldBoosts("subject^0.5 
sind^1.5"));
-
-    /* first some sanity tests that don't use aliasing at all */
 
-    t = "XXXXXXXX";
-    out = qp.parse(t);
+  /* sanity: bare term with no field and no alias → TermQuery on default field 
*/
+  @Test
+  public void testDismaxParser_bareTermUsesDefaultField() throws Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "XXXXXXXX";
+    Query out = qp.parse(t);
     assertNotNull(t + " sanity test gave back null", out);
     assertTrue(t + " sanity test isn't TermQuery: " + out.getClass(), out 
instanceof TermQuery);
     assertEquals(
         t + " sanity test is wrong field",
         qp.getDefaultField(),
         ((TermQuery) out).getTerm().field());
+  }
 
-    t = "subject:XXXXXXXX";
-    out = qp.parse(t);
+  /* sanity: explicit non-aliased field → TermQuery on that field */
+  @Test
+  public void testDismaxParser_explicitFieldNoAlias() throws Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "subject:XXXXXXXX";
+    Query out = qp.parse(t);
     assertNotNull(t + " sanity test gave back null", out);
     assertTrue(t + " sanity test isn't TermQuery: " + out.getClass(), out 
instanceof TermQuery);
     assertEquals(t + " sanity test is wrong field", "subject", ((TermQuery) 
out).getTerm().field());
+  }
 
-    /* field has untokenized type, so this should be a term anyway */
-    t = "sind:\"simple phrase\"";
-    out = qp.parse(t);
+  /* sanity: untokenized field type → still a TermQuery even with quoted 
phrase */
+  @Test
+  public void testDismaxParser_quotedPhraseOnUntokenizedFieldIsTermQuery() 
throws Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "sind:\"simple phrase\"";
+    Query out = qp.parse(t);
     assertNotNull(t + " sanity test gave back null", out);
     assertTrue(t + " sanity test isn't TermQuery: " + out.getClass(), out 
instanceof TermQuery);
     assertEquals(t + " sanity test is wrong field", "sind", ((TermQuery) 
out).getTerm().field());
+  }
 
-    t = "subject:\"simple phrase\"";
-    out = qp.parse(t);
+  /* sanity: tokenized field with quoted phrase → PhraseQuery */
+  @Test
+  public void testDismaxParser_quotedPhraseOnTokenizedFieldIsPhraseQuery() 
throws Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "subject:\"simple phrase\"";
+    Query out = qp.parse(t);
     assertNotNull(t + " sanity test gave back null", out);
     assertTrue(t + " sanity test isn't PhraseQuery: " + out.getClass(), out 
instanceof PhraseQuery);
     assertEquals(
         t + " sanity test is wrong field", "subject", ((PhraseQuery) 
out).getTerms()[0].field());
+  }
 
-    /* now some tests that use aliasing */
-
-    /* basic usage of single "term" */
-    t = "hoss:XXXXXXXX";
-    out = qp.parse(t);
+  /* basic alias use: single term on an alias mapping to 4 fields → DMQ with 4 
clauses */
+  @Test
+  public void testDismaxParser_aliasedTermProducesDMQClausePerField() throws 
Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "hoss:XXXXXXXX";
+    Query out = qp.parse(t);
     assertNotNull(t + " was null", out);
     assertTrue(t + " wasn't a DMQ:" + out.getClass(), out instanceof 
DisjunctionMaxQuery);
     assertEquals(
         t + " wrong number of clauses", 4, countItems(((DisjunctionMaxQuery) 
out).iterator()));
+  }
 
-    /* odd case, but should still work, DMQ of one clause */
-    t = "test:YYYYY";
-    out = qp.parse(t);
+  /* edge case: alias points to a single field → DMQ with 1 clause */
+  @Test
+  public void testDismaxParser_singleFieldAliasIsDMQWithOneClause() throws 
Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "test:YYYYY";
+    Query out = qp.parse(t);
     assertNotNull(t + " was null", out);
     assertTrue(t + " wasn't a DMQ:" + out.getClass(), out instanceof 
DisjunctionMaxQuery);
     assertEquals(
         t + " wrong number of clauses", 1, countItems(((DisjunctionMaxQuery) 
out).iterator()));
+  }
 
-    /* basic usage of multiple "terms" */
-    t = "hoss:XXXXXXXX test:YYYYY";
-    out = qp.parse(t);
+  /* multiple aliased terms → BooleanQuery whose clauses are DMQs */
+  @Test
+  public void testDismaxParser_multipleAliasedTermsProduceBooleanOfDMQs() 
throws Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "hoss:XXXXXXXX test:YYYYY";
+    Query out = qp.parse(t);
     assertNotNull(t + " was null", out);
     assertTrue(t + " wasn't a boolean:" + out.getClass(), out instanceof 
BooleanQuery);
-    {
-      BooleanQuery bq = (BooleanQuery) out;
-      List<BooleanClause> clauses = new ArrayList<>(bq.clauses());
-      assertEquals(t + " wrong number of clauses", 2, clauses.size());
-      Query sub = clauses.get(0).query();
-      assertTrue(t + " first wasn't a DMQ:" + sub.getClass(), sub instanceof 
DisjunctionMaxQuery);
-      assertEquals(
-          t + " first had wrong number of clauses",
-          4,
-          countItems(((DisjunctionMaxQuery) sub).iterator()));
-      sub = clauses.get(1).query();
-      assertTrue(t + " second wasn't a DMQ:" + sub.getClass(), sub instanceof 
DisjunctionMaxQuery);
-      assertEquals(
-          t + " second had wrong number of clauses",
-          1,
-          countItems(((DisjunctionMaxQuery) sub).iterator()));
-    }
 
-    /* a phrase and a term that is a stop word for some fields */
-    t = "hoss:\"XXXXXX YYYYY\" hoss:the";
-    out = qp.parse(t);
+    BooleanQuery bq = (BooleanQuery) out;
+    List<BooleanClause> clauses = new ArrayList<>(bq.clauses());
+    assertEquals(t + " wrong number of clauses", 2, clauses.size());
+
+    Query first = clauses.get(0).query();
+    assertTrue(t + " first wasn't a DMQ:" + first.getClass(), first instanceof 
DisjunctionMaxQuery);
+    assertEquals(
+        t + " first had wrong number of clauses",
+        4,
+        countItems(((DisjunctionMaxQuery) first).iterator()));
+
+    Query second = clauses.get(1).query();
+    assertTrue(
+        t + " second wasn't a DMQ:" + second.getClass(), second instanceof 
DisjunctionMaxQuery);
+    assertEquals(
+        t + " second had wrong number of clauses",
+        1,
+        countItems(((DisjunctionMaxQuery) second).iterator()));
+  }
+
+  /* phrase + a term that is a stop word in some of the aliased fields */
+  @Test
+  public void testDismaxParser_stopWordReducesDMQClauseCount() throws 
Exception {
+    DisjunctionMaxQueryParser qp = newDismaxParserWithAliases();
+    String t = "hoss:\"XXXXXX YYYYY\" hoss:the";
+    Query out = qp.parse(t);
     assertNotNull(t + " was null", out);
     assertTrue(t + " wasn't a boolean:" + out.getClass(), out instanceof 
BooleanQuery);
-    {
-      BooleanQuery bq = (BooleanQuery) out;
-      List<BooleanClause> clauses = new ArrayList<>(bq.clauses());
-      assertEquals(t + " wrong number of clauses", 2, clauses.size());
-      Query sub = clauses.get(0).query();
-      assertTrue(t + " first wasn't a DMQ:" + sub.getClass(), sub instanceof 
DisjunctionMaxQuery);
-      assertEquals(
-          t + " first had wrong number of clauses",
-          4,
-          countItems(((DisjunctionMaxQuery) sub).iterator()));
-      sub = clauses.get(1).query();
-      assertTrue(t + " second wasn't a DMQ:" + sub.getClass(), sub instanceof 
DisjunctionMaxQuery);
-      assertEquals(
-          t + " second had wrong number of clauses (stop words)",
-          2,
-          countItems(((DisjunctionMaxQuery) sub).iterator()));
-    }
+
+    BooleanQuery bq = (BooleanQuery) out;
+    List<BooleanClause> clauses = new ArrayList<>(bq.clauses());
+    assertEquals(t + " wrong number of clauses", 2, clauses.size());
+
+    Query first = clauses.get(0).query();
+    assertTrue(t + " first wasn't a DMQ:" + first.getClass(), first instanceof 
DisjunctionMaxQuery);
+    assertEquals(
+        t + " first had wrong number of clauses",
+        4,
+        countItems(((DisjunctionMaxQuery) first).iterator()));
+
+    Query second = clauses.get(1).query();
+    assertTrue(
+        t + " second wasn't a DMQ:" + second.getClass(), second instanceof 
DisjunctionMaxQuery);
+    assertEquals(
+        t + " second had wrong number of clauses (stop words)",
+        2,
+        countItems(((DisjunctionMaxQuery) second).iterator()));

Review Comment:
   This assertion assumes stopwords cause aliased fields to be dropped 
(disjunct count shrinks). With `SolrQueryParserBase#createFieldQuery` now 
returning `MatchNoDocsQuery` (instead of null) for stopword-only terms, 
`DisjunctionMaxQueryParser` will likely keep those disjuncts (possibly 
boosted), so the DMQ clause count won’t be reduced anymore and this test will 
fail. Either update the DisjunctionMaxQueryParser alias logic to skip 
`MatchNoDocsQuery` disjuncts (preserving previous behavior), or update this 
expectation to match the new semantics.



##########
solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java:
##########
@@ -538,6 +538,24 @@ protected Query newFieldQuery(
     return query;
   }
 
+  /**
+   * Delegates to {@link 
QueryBuilder#createFieldQuery(org.apache.lucene.analysis.Analyzer,
+   * org.apache.lucene.search.BooleanClause.Occur, java.lang.String, 
java.lang.String, boolean,
+   * int)} but returns MatchNoDocsQuery rather than null
+   */
+  protected Query createFieldQuery(
+      Analyzer analyzer,
+      BooleanClause.Occur operator,
+      String field,
+      String queryText,
+      boolean quoted,
+      int phraseSlop) {
+

Review Comment:
   Add `@Override` to `createFieldQuery(...)` to ensure it actually overrides 
Lucene's `QueryBuilder#createFieldQuery` (and to make future signature drift a 
compile-time error).



##########
solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java:
##########
@@ -595,7 +596,9 @@ public static void setMinShouldMatch(BooleanQuery.Builder 
q, String spec, boolea
             optionalDismaxClauses++;
           }
         } else {
-          optionalClauses++;
+          if (!(mmAutoRelax && (c.query() instanceof MatchNoDocsQuery))) {
+            optionalClauses++;
+          }
         }

Review Comment:
   The new mm.autoRelax logic only skips counting empty clauses when the SHOULD 
query is a direct `MatchNoDocsQuery`. In practice, empty queries are often 
wrapped (e.g., `BoostQuery(MatchNoDocsQuery)` via boosts, and 
`MatchNoDocsQuery` disjuncts inside `DisjunctionMaxQuery`), so they’ll still be 
counted and auto-relaxation won’t kick in. Consider unwrapping common wrappers 
and (for `DisjunctionMaxQuery`) counting only non-empty disjuncts when 
computing the disjunct size / optional clause counts.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to