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

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

commit 5e49d6a0e4528d306ccc8553df88817816069abf
Author: Abhishek Umarjikar <[email protected]>
AuthorDate: Mon Aug 10 08:27:50 2026 +0530

    SOLR-18256: Fix lucene QParser to support nested pure negative queries 
(#4552)
    
    Users needn't do the *:* hack anymore.
    
    (cherry picked from commit 0eca10b6189e5f0146e02d10bcf2177b2a5a5991)
---
 .../SOLR_18256_autofix_negative_lucene_clauses.yml |  8 +++
 .../src/java/org/apache/solr/search/QParser.java   | 18 +++++++
 .../org/apache/solr/search/SolrQueryParser.java    |  9 ++++
 .../apache/solr/search/TestSolrQueryParser.java    | 62 ++++++++++++++++++++++
 4 files changed, 97 insertions(+)

diff --git 
a/changelog/unreleased/SOLR_18256_autofix_negative_lucene_clauses.yml 
b/changelog/unreleased/SOLR_18256_autofix_negative_lucene_clauses.yml
new file mode 100644
index 00000000000..e3f762093a0
--- /dev/null
+++ b/changelog/unreleased/SOLR_18256_autofix_negative_lucene_clauses.yml
@@ -0,0 +1,8 @@
+title: Pure negative (NOT) expressions in the "lucene" (default) QParser no 
longer need an explicit *:* to work.
+type: fixed
+authors:
+  - name: Abhishek Umarjikar
+    nick: abumarjikar
+links:
+  - name: SOLR-18256
+    url: https://issues.apache.org/jira/browse/SOLR-18256
diff --git a/solr/core/src/java/org/apache/solr/search/QParser.java 
b/solr/core/src/java/org/apache/solr/search/QParser.java
index 21c7d764997..b3a77ff7427 100644
--- a/solr/core/src/java/org/apache/solr/search/QParser.java
+++ b/solr/core/src/java/org/apache/solr/search/QParser.java
@@ -28,6 +28,7 @@ import org.apache.lucene.queries.function.ValueSource;
 import org.apache.lucene.queries.function.valuesource.QueryValueSource;
 import org.apache.lucene.search.NamedMatches;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.util.Version;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
@@ -65,6 +66,7 @@ public abstract class QParser {
       stringIncludingLocalParams; // the original query string including any 
local params
   protected boolean valFollowedParams; // true if the value "qstr" followed 
the localParams
   protected int localParamsEnd; // the position one past where the localParams 
ended
+  protected boolean autoFixPureNegative;
 
   /**
    * Constructor for the QParser
@@ -107,6 +109,13 @@ public abstract class QParser {
 
     this.params = Objects.requireNonNull(params);
     this.req = req;
+
+    if (req != null && req.getCore() != null && req.getCore().getSolrConfig() 
!= null) {
+      this.autoFixPureNegative =
+          
req.getCore().getSolrConfig().luceneMatchVersion.onOrAfter(Version.LUCENE_10_2_0);
+    } else {
+      this.autoFixPureNegative = true;
+    }
   }
 
   /**
@@ -187,6 +196,14 @@ public abstract class QParser {
     this.qstr = s;
   }
 
+  public boolean isAutoFixPureNegative() {
+    return autoFixPureNegative;
+  }
+
+  public void setAutoFixPureNegative(boolean autoFixPureNegative) {
+    this.autoFixPureNegative = autoFixPureNegative;
+  }
+
   /**
    * Returns the resulting query from this QParser, calling parse() only the 
first time and caching
    * the Query result. <em>A null return is possible!</em>
@@ -267,6 +284,7 @@ public abstract class QParser {
     // TODO: this would be better passed in to the constructor... change to a 
ParserContext object?
     nestedParser.flags = this.flags;
     nestedParser.recurseCount = recurseCount;
+    nestedParser.autoFixPureNegative = this.autoFixPureNegative;
     recurseCount--;
     return nestedParser;
   }
diff --git a/solr/core/src/java/org/apache/solr/search/SolrQueryParser.java 
b/solr/core/src/java/org/apache/solr/search/SolrQueryParser.java
index f64f8c3bdaa..58f2231fc9f 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrQueryParser.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrQueryParser.java
@@ -16,6 +16,9 @@
  */
 package org.apache.solr.search;
 
+import java.util.List;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.Query;
 import org.apache.solr.parser.QueryParser;
 
 /** Solr's default query parser, a schema-driven superset of the classic 
lucene query parser. */
@@ -24,4 +27,10 @@ public class SolrQueryParser extends QueryParser {
   public SolrQueryParser(QParser parser, String defaultField) {
     super(defaultField, parser);
   }
+
+  @Override
+  protected Query getBooleanQuery(List<BooleanClause> clauses) throws 
SyntaxError {
+    Query q = super.getBooleanQuery(clauses);
+    return (parser.isAutoFixPureNegative()) ? QueryUtils.makeQueryable(q) : q;
+  }
 }
diff --git a/solr/core/src/test/org/apache/solr/search/TestSolrQueryParser.java 
b/solr/core/src/test/org/apache/solr/search/TestSolrQueryParser.java
index 6c9cd362e41..deef7f49d49 100644
--- a/solr/core/src/test/org/apache/solr/search/TestSolrQueryParser.java
+++ b/solr/core/src/test/org/apache/solr/search/TestSolrQueryParser.java
@@ -1901,4 +1901,66 @@ public class TestSolrQueryParser extends SolrTestCaseJ4 {
       }
     }
   }
+
+  @Test
+  public void testNestedPureNegativeQuery() throws Exception {
+
+    // Standard sample data with completely unique field values to isolate 
matches
+    assertU(adoc("id", "9414", "v_t", "pureneg foo bar", "type_t", 
"negativetest"));
+    assertU(adoc("id", "9415", "v_t", "pureneg foo baz", "type_t", 
"negativetest"));
+    assertU(adoc("id", "9416", "v_t", "pureneg baz", "type_t", 
"negativetest"));
+    assertU(commit());
+
+    QParser pLatest = QParser.getParser("-foo", req());
+    assertTrue(
+        "Should default to true on latest luceneMatchVersion", 
pLatest.isAutoFixPureNegative());
+
+    // Top-level negative query must exclude 'bar' but successfully find our 
other docs
+    // Force sort by ID so the array index expectations always line up 
perfectly
+    assertJQ(
+        req("q", "v_t:pureneg AND -v_t:bar", "df", "v_t", "sort", "id asc"),
+        "/response/docs/[0]/id=='9415'",
+        "/response/docs/[1]/id=='9416'");
+
+    // Nested pure negative query inside a parenthesized group with AND
+    assertJQ(
+        req("q", "v_t:pureneg AND v_t:foo AND (-v_t:bar)", "df", "v_t"),
+        "/response/numFound==1",
+        "/response/docs/[0]/id=='9415'");
+
+    // Nested pure negative query using explicit NOT syntax
+    assertJQ(
+        req("q", "v_t:pureneg AND v_t:foo AND (NOT v_t:bar)", "df", "v_t"),
+        "/response/numFound==1",
+        "/response/docs/[0]/id=='9415'");
+
+    assertJQ(
+        req("q", "v_t:pureneg NOT v_t:foo", "df", "v_t"),
+        "/response/numFound==1",
+        "/response/docs/[0]/id=='9416'");
+
+    assertJQ(
+        req("q", "NOT v_t:pureneg", "df", "v_t", "fq", "type_t:negativetest"),
+        "/response/numFound==0");
+
+    try (SolrQueryRequest req = req("df", "v_t")) {
+      // Pure negative clause without auto-fix should NOT inject *:*
+      QParser pDisabled = QParser.getParser("-v_t:bar", req);
+      pDisabled.setAutoFixPureNegative(false);
+      assertFalse(
+          "autoFixPureNegative should be false when set explicitly",
+          pDisabled.isAutoFixPureNegative());
+
+      Query qDisabled = pDisabled.parse();
+      assertFalse(
+          "Query should NOT contain MatchAllDocsQuery when autoFixPureNegative 
is false",
+          qDisabled.toString().contains("*:*"));
+
+      // Sub-query propagation test: Verify subQuery inherits 
autoFixPureNegative = false
+      QParser subParserDisabled = pDisabled.subQuery("-v_t:bar", null);
+      assertFalse(
+          "subQuery should inherit autoFixPureNegative=false from parent 
parser",
+          subParserDisabled.isAutoFixPureNegative());
+    }
+  }
 }

Reply via email to