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()); + } + } }
