diff --git a/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java b/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java
index 42cedf829799..6ba66c680361 100644
--- a/solr/core/src/java/org/apache/solr/parser/SolrQueryParserBase.java
+++ b/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) {
+
+ Query fieldQuery =
+ super.createFieldQuery(analyzer, operator, field, queryText, quoted, phraseSlop);
+ return fieldQuery != null ? fieldQuery : new MatchNoDocsQuery("empty query");
+ }
+
/**
* Base implementation delegates to {@link #getFieldQuery(String,String,boolean,boolean)}. This
* method may be overridden, for example, to return a SpanNearQuery instead of a PhraseQuery.
diff --git a/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java b/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java
index ed6178be4f9b..3a1a3f0cd3d9 100644
--- a/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java
+++ b/solr/core/src/java/org/apache/solr/util/SolrPluginUtils.java
@@ -46,6 +46,7 @@
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.DisjunctionMaxQuery;
import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.solr.common.SolrException;
@@ -595,7 +596,7 @@ public static void setMinShouldMatch(BooleanQuery.Builder q, String spec, boolea
optionalDismaxClauses++;
}
} else {
- optionalClauses++;
+ if (!(c.query() instanceof MatchNoDocsQuery)) optionalClauses++;
}
}
}
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema.xml b/solr/core/src/test-files/solr/collection1/conf/schema.xml
index e20f31d4bf3b..4cff86be2266 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema.xml
@@ -156,6 +156,7 @@
+
diff --git a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
index 3a2d976a97cb..f55df0ced89a 100644
--- a/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
+++ b/solr/core/src/test/org/apache/solr/search/TestMmBoolQParserPlugin.java
@@ -20,6 +20,7 @@
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.NamedMatches;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
@@ -109,6 +110,20 @@ public void testMinShouldMatchThresholdsLower() throws Exception {
assertEquals(expected, actual);
}
+ @Test
+ public void testMinShouldMatchWithEmptyClauseCausedByStopWord() throws Exception {
+
+ Query actual =
+ parseQuery(req("q", "{!bool should=name:foo should=name:bar should=teststop:to mm=-1}"));
+
+ BooleanQuery expected =
+ shouldBuilder("foo", "bar")
+ .setMinimumNumberShouldMatch(1)
+ .add(new MatchNoDocsQuery(""), BooleanClause.Occur.SHOULD)
+ .build();
+ assertEquals(expected, actual);
+ }
+
@Test
public void testMinShouldMatchThresholdsUpper() throws Exception {
Query actual =
diff --git a/solr/core/src/test/org/apache/solr/util/SolrPluginUtilsTest.java b/solr/core/src/test/org/apache/solr/util/SolrPluginUtilsTest.java
index 819585acc226..f4ffdb73c29e 100644
--- a/solr/core/src/test/org/apache/solr/util/SolrPluginUtilsTest.java
+++ b/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 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 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 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 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()));
+ }
+
+ private DisjunctionMaxQueryParser newDismaxParserWithAliases() throws Exception {
+ 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"));
+ return qp;
}
private static int countItems(Iterator> i) {