You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ho...@apache.org on 2021/12/09 18:42:38 UTC

[solr] branch main updated: SOLR-8319: Fix NPE in pivot facets, add non-Analyzed query method in FieldType

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

houston 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 841cbe0  SOLR-8319: Fix NPE in pivot facets, add non-Analyzed query method in FieldType
841cbe0 is described below

commit 841cbe0b9403c62214c8ead55a31dc5a9119e1e0
Author: Houston Putman <ho...@apache.org>
AuthorDate: Thu Dec 9 13:37:48 2021 -0500

    SOLR-8319: Fix NPE in pivot facets, add non-Analyzed query method in FieldType
    
    Co-authored-by: Isabelle Giguere <ig...@opentext.com>
---
 solr/CHANGES.txt                                   |   2 +
 .../handler/component/PivotFacetProcessor.java     |   4 +-
 .../java/org/apache/solr/request/SimpleFacets.java |   2 +-
 .../src/java/org/apache/solr/schema/FieldType.java |  15 +-
 .../src/java/org/apache/solr/schema/TextField.java |   9 +
 .../src/java/org/apache/solr/schema/TrieField.java |  10 -
 .../org/apache/solr/search/TermQParserPlugin.java  |  16 +-
 .../solr/search/facet/FacetFieldProcessor.java     |   6 +-
 .../search/facet/FacetFieldProcessorByHashDV.java  |   2 +-
 .../collectionA/conf/schema.xml                    | 104 +++++++
 .../collectionA/conf/solrconfig.xml                |  58 ++++
 .../collectionA/conf/stopwords.txt                 |  78 +++++
 .../collectionB/conf/schema.xml                    | 104 +++++++
 .../collectionB/conf/solrconfig.xml                |  58 ++++
 .../collectionB/conf/stopwords.txt                 |  82 +++++
 .../component/FacetPivot2CollectionsTest.java      | 338 +++++++++++++++++++++
 .../apache/solr/search/TestTermQParserPlugin.java  |  95 ++++++
 17 files changed, 954 insertions(+), 29 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index b35eb73..28757ad 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -474,6 +474,8 @@ Bug Fixes
 
 * SOLR-15832: Clean-up after publish action in Schema Designer shouldn't fail if .system collection doesn't exist (Timothy Potter)
 
+* SOLR-8319: Fix NPE in pivot facets, add non-Analyzed query method in FieldType. (Houston Putman, Isabelle Giguere)
+
 ==================  8.11.0 ==================
 
 Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release.
diff --git a/solr/core/src/java/org/apache/solr/handler/component/PivotFacetProcessor.java b/solr/core/src/java/org/apache/solr/handler/component/PivotFacetProcessor.java
index 1069c50..c3749cc 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/PivotFacetProcessor.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/PivotFacetProcessor.java
@@ -351,7 +351,7 @@ public class PivotFacetProcessor extends SimpleFacets
       DocSet hasVal = searcher.getDocSet(query);
       return base.andNotSize(hasVal);
     } else {
-      Query query = ft.getFieldQuery(null, field, pivotValue);
+      Query query = ft.getFieldTermQuery(null, field, pivotValue);
       return searcher.numDocs(query, base);
     }
   }
@@ -370,7 +370,7 @@ public class PivotFacetProcessor extends SimpleFacets
       DocSet hasVal = searcher.getDocSet(query);
       return base.andNot(hasVal);
     } else {
-      Query query = ft.getFieldQuery(null, field, pivotValue);
+      Query query = ft.getFieldTermQuery(null, field, pivotValue);
       return searcher.getDocSet(query, base);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/request/SimpleFacets.java b/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
index d98cf79..099000a 100644
--- a/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
+++ b/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
@@ -897,7 +897,7 @@ public class SimpleFacets {
 
   private int numDocs(String term, final SchemaField sf, final FieldType ft, final DocSet baseDocset) {
     try {
-      return searcher.numDocs(ft.getFieldQuery(null, sf, term), baseDocset);
+      return searcher.numDocs(ft.getFieldTermQuery(null, sf, term), baseDocset);
     } catch (IOException e1) {
       throw new RuntimeException(e1);
     }
diff --git a/solr/core/src/java/org/apache/solr/schema/FieldType.java b/solr/core/src/java/org/apache/solr/schema/FieldType.java
index 0a7a47a..bf4461a 100644
--- a/solr/core/src/java/org/apache/solr/schema/FieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/FieldType.java
@@ -974,15 +974,26 @@ public abstract class FieldType extends FieldProperties {
    * @return The {@link org.apache.lucene.search.Query} instance.  This implementation returns a {@link org.apache.lucene.search.TermQuery} but overriding queries may not
    */
   public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
-    BytesRefBuilder br = new BytesRefBuilder();
-    readableToIndexed(externalVal, br);
     if (field.hasDocValues() && !field.indexed()) {
       // match-only
       return getRangeQuery(parser, field, externalVal, externalVal, true, true);
     } else {
+      BytesRefBuilder br = new BytesRefBuilder();
+      readableToIndexed(externalVal, br);
       return new TermQuery(new Term(field.getName(), br));
     }
   }
+  
+  /**
+   * Returns a Query instance for doing a single term search against a field. This term will not be analyzed before searching.
+   * @param parser The {@link org.apache.solr.search.QParser} calling the method
+   * @param field The {@link org.apache.solr.schema.SchemaField} of the field to search
+   * @param externalVal The String representation of the term value to search
+   * @return The {@link org.apache.lucene.search.Query} instance.
+   */
+  public Query getFieldTermQuery(QParser parser, SchemaField field, String externalVal) {
+    return getFieldQuery(parser, field, externalVal);
+  }
 
   /** @lucene.experimental  */
   public Query getSetQuery(QParser parser, SchemaField field, Collection<String> externalVals) {
diff --git a/solr/core/src/java/org/apache/solr/schema/TextField.java b/solr/core/src/java/org/apache/solr/schema/TextField.java
index d79823b..424b123 100644
--- a/solr/core/src/java/org/apache/solr/schema/TextField.java
+++ b/solr/core/src/java/org/apache/solr/schema/TextField.java
@@ -24,10 +24,12 @@ import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.tokenattributes.TermToBytesRefAttribute;
 import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.Term;
 import org.apache.lucene.queries.function.ValueSource;
 import org.apache.lucene.queries.function.valuesource.SortedSetFieldSource;
 import org.apache.lucene.search.*;
 import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
 import org.apache.lucene.util.QueryBuilder;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.parser.SolrQueryParserBase;
@@ -146,6 +148,13 @@ public class TextField extends FieldType {
   public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
     return parseFieldQuery(parser, getQueryAnalyzer(), field.getName(), externalVal);
   }
+  
+  @Override
+  public Query getFieldTermQuery(QParser parser, SchemaField field, String externalVal) {
+    BytesRefBuilder br = new BytesRefBuilder();
+    readableToIndexed(externalVal, br);
+    return new TermQuery(new Term(field.getName(), br));
+  }
 
   @Override
   public Object toObject(SchemaField sf, BytesRef term) {
diff --git a/solr/core/src/java/org/apache/solr/schema/TrieField.java b/solr/core/src/java/org/apache/solr/schema/TrieField.java
index a80cae7..feadb05 100644
--- a/solr/core/src/java/org/apache/solr/schema/TrieField.java
+++ b/solr/core/src/java/org/apache/solr/schema/TrieField.java
@@ -349,16 +349,6 @@ public class TrieField extends NumericFieldType {
   }
 
   @Override
-  public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
-    if (!field.indexed() && field.hasDocValues()) {
-      // currently implemented as singleton range
-      return getRangeQuery(parser, field, externalVal, externalVal, true, true);
-    } else {
-      return super.getFieldQuery(parser, field, externalVal);
-    }
-  }
-
-  @Override
   public String storedToReadable(IndexableField f) {
     return toExternal(f);
   }
diff --git a/solr/core/src/java/org/apache/solr/search/TermQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/TermQParserPlugin.java
index 89b3d28..4a5a26a 100644
--- a/solr/core/src/java/org/apache/solr/search/TermQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/TermQParserPlugin.java
@@ -20,6 +20,7 @@ import org.apache.lucene.index.Term;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.schema.FieldType;
@@ -48,21 +49,18 @@ public class TermQParserPlugin extends QParserPlugin {
       @Override
       public Query parse() {
         String fname = localParams.get(QueryParsing.F);
+        if (fname == null || fname.isEmpty()) {
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing field to query");
+        }
         FieldType ft = req.getSchema().getFieldTypeNoEx(fname);
         String val = localParams.get(QueryParsing.V);
-        BytesRefBuilder term;
         if (ft != null) {
-          if (ft.isPointField()) {
-            return ft.getFieldQuery(this, req.getSchema().getField(fname), val);
-          } else {
-            term = new BytesRefBuilder();
-            ft.readableToIndexed(val, term);
-          }
+          return ft.getFieldTermQuery(this, req.getSchema().getField(fname), val);
         } else {
-          term = new BytesRefBuilder();
+          BytesRefBuilder term = new BytesRefBuilder();
           term.copyChars(val);
+          return new TermQuery(new Term(fname, term.get()));
         }
-        return new TermQuery(new Term(fname, term.get()));
       }
     };
   }
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessor.java b/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessor.java
index b6d55d7..330d72d 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessor.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessor.java
@@ -470,9 +470,7 @@ abstract class FacetFieldProcessor extends FacetProcessor<FacetField> {
    * Trivial helper method for building up a bucket query given the (Stringified) bucket value
    */
   protected Query makeBucketQuery(final String bucketValue) {
-    // TODO: this isn't viable for things like text fields w/ analyzers that are non-idempotent (ie: stemmers)
-    // TODO: but changing it to just use TermQuery isn't safe for things like numerics, dates, etc...
-    return sf.getType().getFieldQuery(null, sf, bucketValue);
+    return sf.getType().getFieldTermQuery(null, sf, bucketValue);
   }
 
   private void calculateNumBuckets(SimpleOrderedMap<Object> target) throws IOException {
@@ -997,7 +995,7 @@ abstract class FacetFieldProcessor extends FacetProcessor<FacetField> {
 
     // fieldQuery currently relies on a string input of the value...
     String bucketStr = bucketVal instanceof Date ? ((Date)bucketVal).toInstant().toString() : bucketVal.toString();
-    Query domainQ = ft.getFieldQuery(null, sf, bucketStr);
+    Query domainQ = ft.getFieldTermQuery(null, sf, bucketStr);
 
     fillBucket(bucket, domainQ, null, skip, facetInfo);
 
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessorByHashDV.java b/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessorByHashDV.java
index e39055b..358e8f2 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessorByHashDV.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetFieldProcessorByHashDV.java
@@ -443,7 +443,7 @@ class FacetFieldProcessorByHashDV extends FacetFieldProcessor {
     long val = table.vals[slotNum];
     @SuppressWarnings({"rawtypes"})
     Comparable value = calc.bitsToValue(val);
-    return new SlotContext(sf.getType().getFieldQuery(null, sf, calc.formatValue(value)));
+    return new SlotContext(sf.getType().getFieldTermQuery(null, sf, calc.formatValue(value)));
   };
 
   private void doRehash(LongCounts table) {
diff --git a/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/schema.xml b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/schema.xml
new file mode 100644
index 0000000..ca48085
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/schema.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" ?>
+<!--
+ 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.
+-->
+
+<!-- The Solr schema file, version 1.6  -->
+
+<schema name="collectionA" version="1.6">
+  <!-- attribute "name" is the name of this schema and is only used for display purposes.
+       Applications should change this to reflect the nature of the search collection.
+       version="x.y" is Solr's version number for the schema syntax and semantics.  It should
+       not normally be changed by applications.
+       1.0: multiValued attribute did not exist, all fields are multiValued by nature
+       1.1: multiValued attribute introduced, false by default 
+       1.2: omitTermFreqAndPositions attribute introduced, true by default except for text fields.
+       1.3: removed optional field compress feature
+       1.4: default auto-phrase (QueryParser feature) to off
+       1.5: omitNorms defaults to true for primitive field types (int, float, boolean, string...)
+       1.6: useDocValuesAsStored defaults to true.
+     -->
+
+  <!-- Collection meant to hold whitespace delimited text -->
+  <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
+    <analyzer type="index">
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="0" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="0" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+    <analyzer type="query">
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="0" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="0" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+  </fieldType>
+
+  <fieldType name="filename" class="solr.TextField" positionIncrementGap="100">
+    <analyzer type="index">
+    <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="\." replacement=" " />
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+    <analyzer type="query">
+    <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="\." replacement=" " />
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+  </fieldType>
+
+  <fieldType name="lowercase" class="solr.TextField" positionIncrementGap="100">
+    <analyzer>
+      <tokenizer class="solr.KeywordTokenizerFactory" />
+      <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+  </fieldType>
+
+  <fieldType name="date" class="${solr.tests.DateFieldType}" docValues="true" sortMissingLast="true"/>
+  
+  <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+  <fieldType name="float" class="${solr.tests.FloatFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+  <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+  <fieldType name="double" class="${solr.tests.DoubleFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+
+  <fieldType name="string" class="solr.StrField" sortMissingLast="true" />
+
+  <field name="id" type="string" indexed="true" stored="true" multiValued="false" required="true"/>
+  <field name="name" type="filename" indexed="true" stored="true" multiValued="false" />
+  <field name="text" type="text" indexed="true" stored="false" multiValued="true"/>
+  <field name="subject" type="text" indexed="true" stored="true" multiValued="true" />
+  <field name="title" type="text" indexed="true" stored="true" multiValued="false"/>
+  <field name="fileType" type="lowercase" indexed="true" stored="true" multiValued="false" />
+
+  <field name="_version_" type="long" indexed="true" stored="true" />
+
+  <dynamicField name="*_d" type="double" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_f" type="float" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_i" type="int" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_dt" type="date" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_s1" type="string" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_s" type="string" indexed="true" stored="true" multiValued="true"/>
+
+
+  <uniqueKey>id</uniqueKey>
+
+
+</schema>
\ No newline at end of file
diff --git a/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/solrconfig.xml
new file mode 100644
index 0000000..52da2f1
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/solrconfig.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" ?>
+
+<!--
+ 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.
+-->
+
+<!-- This is a "kitchen sink" config file that tests can use.
+     When writting a new test, feel free to add *new* items (plugins,
+     config options, etc...) as long as they don't break any existing
+     tests.  if you need to test something esoteric please add a new
+     "solrconfig-your-esoteric-purpose.xml" config file.
+
+     Note in particular that this test is used by MinimalSchemaTest so
+     Anything added to this file needs to work correctly even if there
+     is now uniqueKey or defaultSearch Field.
+  -->
+
+<config>
+
+  <dataDir>${solr.data.dir:}</dataDir>
+
+  <directoryFactory name="DirectoryFactory"
+                    class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+  <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+  <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+  <statsCache class="${solr.statsCache:}"/>
+
+  <updateHandler class="solr.DirectUpdateHandler2">
+    <commitWithin>
+      <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+    </commitWithin>
+    <updateLog></updateLog>
+  </updateHandler>
+
+  <requestHandler name="/select" class="solr.SearchHandler">
+    <lst name="defaults">
+      <str name="echoParams">explicit</str>
+      <str name="indent">true</str>
+      <str name="df">text</str>
+    </lst>
+
+  </requestHandler>
+</config>
\ No newline at end of file
diff --git a/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/stopwords.txt b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/stopwords.txt
new file mode 100644
index 0000000..e4d5930
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionA/conf/stopwords.txt
@@ -0,0 +1,78 @@
+# 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.
+
+#-----------------------------------------------------------------------
+
+#Standard english stop words taken from Lucene's StopAnalyzer
+a
+an
+and
+are
+as
+at
+be
+but
+by
+for
+if
+in
+into
+is
+it
+no
+not
+of
+on
+or
+s
+such
+t
+that
+the
+their
+then
+there
+these
+they
+this
+to
+was
+will
+with
+
+# Create more potential error conditions: any single letter is a stop word
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+u
+v
+w
+x
+y
+z
diff --git a/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/schema.xml b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/schema.xml
new file mode 100644
index 0000000..84cfbfb
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/schema.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" ?>
+<!--
+ 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.
+-->
+
+<!-- The Solr schema file, version 1.6  -->
+
+<schema name="collectionA" version="1.6">
+  <!-- attribute "name" is the name of this schema and is only used for display purposes.
+       Applications should change this to reflect the nature of the search collection.
+       version="x.y" is Solr's version number for the schema syntax and semantics.  It should
+       not normally be changed by applications.
+       1.0: multiValued attribute did not exist, all fields are multiValued by nature
+       1.1: multiValued attribute introduced, false by default 
+       1.2: omitTermFreqAndPositions attribute introduced, true by default except for text fields.
+       1.3: removed optional field compress feature
+       1.4: default auto-phrase (QueryParser feature) to off
+       1.5: omitNorms defaults to true for primitive field types (int, float, boolean, string...)
+       1.6: useDocValuesAsStored defaults to true.
+     -->
+
+  <!-- Collection meant to hold whitespace delimited text -->
+  <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
+    <analyzer type="index">
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="0" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="0" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+    <analyzer type="query">
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="0" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="0" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+  </fieldType>
+
+  <fieldType name="filename" class="solr.TextField" positionIncrementGap="100">
+    <analyzer type="index">
+    <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="\." replacement=" " />
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+    <analyzer type="query">
+    <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="\." replacement=" " />
+    <tokenizer class="solr.WhitespaceTokenizerFactory" />
+    <filter class="solr.WordDelimiterFilterFactory" preserveOriginal="1" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1" />
+    <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt" />
+    <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+  </fieldType>
+
+  <fieldType name="lowercase" class="solr.TextField" positionIncrementGap="100">
+    <analyzer>
+      <tokenizer class="solr.KeywordTokenizerFactory" />
+      <filter class="solr.LowerCaseFilterFactory" />
+    </analyzer>
+  </fieldType>
+
+  <fieldType name="date" class="${solr.tests.DateFieldType}" docValues="true" sortMissingLast="true"/>
+  
+  <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+  <fieldType name="float" class="${solr.tests.FloatFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+  <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+  <fieldType name="double" class="${solr.tests.DoubleFieldType}" docValues="true" precisionStep="0" positionIncrementGap="0"/>
+
+  <fieldType name="string" class="solr.StrField" sortMissingLast="true" />
+
+  <field name="id" type="string" indexed="true" stored="true" multiValued="false" required="true"/>
+  <field name="name" type="filename" indexed="true" stored="true" multiValued="false" />
+  <field name="text" type="text" indexed="true" stored="false" multiValued="true"/>
+  <field name="subject" type="text" indexed="true" stored="true" multiValued="true" />
+  <field name="title" type="text" indexed="true" stored="true" multiValued="false"/>
+  <field name="fileType" type="lowercase" indexed="true" stored="true" multiValued="false" />
+
+  <field name="_version_" type="long" indexed="true" stored="true" />
+
+  <dynamicField name="*_d" type="double" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_f" type="float" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_i" type="int" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_dt" type="date" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_s1" type="string" indexed="true" stored="true" multiValued="false"/>
+  <dynamicField name="*_s" type="string" indexed="true" stored="true" multiValued="true"/>
+
+
+  <uniqueKey>id</uniqueKey>
+
+
+</schema>
diff --git a/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/solrconfig.xml
new file mode 100644
index 0000000..f190e0d
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/solrconfig.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" ?>
+
+<!--
+ 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.
+-->
+
+<!-- This is a "kitchen sink" config file that tests can use.
+     When writting a new test, feel free to add *new* items (plugins,
+     config options, etc...) as long as they don't break any existing
+     tests.  if you need to test something esoteric please add a new
+     "solrconfig-your-esoteric-purpose.xml" config file.
+
+     Note in particular that this test is used by MinimalSchemaTest so
+     Anything added to this file needs to work correctly even if there
+     is now uniqueKey or defaultSearch Field.
+  -->
+
+<config>
+
+  <dataDir>${solr.data.dir:}</dataDir>
+
+  <directoryFactory name="DirectoryFactory"
+                    class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+  <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+  <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+  <statsCache class="${solr.statsCache:}"/>
+
+  <updateHandler class="solr.DirectUpdateHandler2">
+    <commitWithin>
+      <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+    </commitWithin>
+    <updateLog></updateLog>
+  </updateHandler>
+
+  <requestHandler name="/select" class="solr.SearchHandler">
+    <lst name="defaults">
+      <str name="echoParams">explicit</str>
+      <str name="indent">true</str>
+      <str name="df">text</str>
+    </lst>
+
+  </requestHandler>
+</config>
diff --git a/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/stopwords.txt b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/stopwords.txt
new file mode 100644
index 0000000..1aec25e
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/different-stopwords/collectionB/conf/stopwords.txt
@@ -0,0 +1,82 @@
++# 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.
++
++#-----------------------------------------------------------------------
++
++#Standard english stop words taken from Lucene's StopAnalyzer
++a
++an
++and
++are
++as
++at
++be
++but
++by
++for
++if
++in
++into
++is
++it
++no
++not
++of
++on
++or
++s
++such
++t
++that
++the
++their
++then
++there
++these
++they
++this
++to
++was
++will
++with
++
++
++# Create more potential error conditions: any single letter is a stop word
++aa
++bb
++cc
++dd
++ee
++ff
++gg
++hh
++ii
++jj
++kk
++ll
++mm
++nn
++oo
++pp
++qq
++rr
++ss
++tt
++uu
++vv
++ww
++xx
++yy
++zz
diff --git a/solr/core/src/test/org/apache/solr/handler/component/FacetPivot2CollectionsTest.java b/solr/core/src/test/org/apache/solr/handler/component/FacetPivot2CollectionsTest.java
new file mode 100644
index 0000000..50c2a25
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/component/FacetPivot2CollectionsTest.java
@@ -0,0 +1,338 @@
+/*
+ * 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.handler.component;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.lucene.util.TestUtil;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.response.CollectionAdminResponse;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.SolrInputField;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Testing of pivot facets on multiple collections.
+ * This class mainly aims to test that there is no issue with stop words on multiple collections.
+ * Facets pivot counts are validated elsewhere.  There's no validation of counts here.
+ */
+@SuppressSSL
+public class FacetPivot2CollectionsTest extends SolrCloudTestCase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  
+  private static MiniSolrCloudCluster solrCluster;
+  
+  private static final String COLL_A = "collectionA";
+  private static final String COLL_B = "collectionB";
+  private static final String ALIAS = "all";
+
+  // available schema fields
+  private static final String ID_FIELD = "id";
+  private static final String TXT_FIELD_MULTIVALUED = "text";
+  private static final String NAME_TXT_FIELD_NOT_MULTIVALUED = "name";
+  private static final String TITLE_TXT_FIELD_NOT_MULTIVALUED = "title";
+  private static final String SUBJECT_TXT_FIELD_MULTIVALUED = "subject";
+  private static final String FILETYPE_TXT_FIELD_NOT_MULTIVALUED = "fileType";
+  private static final String DYNAMIC_INT_FIELD_NOT_MULTIVALUED = "int_i";
+  private static final String DYNAMIC_FLOAT_FIELD_NOT_MULTIVALUED = "float_f";
+  private static final String DYNAMIC_DATE_FIELD_NOT_MULTIVALUED = "date_dt";
+  private static final String DYNAMIC_STR_FIELD_MULTIVALUED = "strFieldMulti_s";
+  private static final String DYNAMIC_STR_FIELD_NOT_MULTIVALUED = "strFieldSingle_s1";
+
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+
+    // create and configure cluster
+    solrCluster = configureCluster(1)
+        .addConfig(COLL_A, configset("different-stopwords" + File.separator + COLL_A))
+        .addConfig(COLL_B, configset("different-stopwords" + File.separator + COLL_B))
+        .configure();
+    
+    try {
+      CollectionAdminResponse responseA = CollectionAdminRequest.createCollection(COLL_A,COLL_A,1,1).process(solrCluster.getSolrClient());
+      NamedList<Object> result = responseA.getResponse();
+      if(result.get("failure") != null) {
+        fail("Collection A creation failed : " + result.get("failure"));
+      }
+    } catch (SolrException e) {
+      fail("Collection A creation failed : " + e.getMessage());
+    }
+
+    try {
+      CollectionAdminResponse responseB = CollectionAdminRequest.createCollection(COLL_B,COLL_B,1,1).process(solrCluster.getSolrClient());
+      NamedList<Object> result = responseB.getResponse();
+      if(result.get("failure") != null) {
+        fail("Collection B creation failed : " + result.get("failure"));
+      }
+    }catch (SolrException e) {
+      fail("Collection B creation failed : " + e.getMessage());    
+    }
+    
+    CollectionAdminResponse response = CollectionAdminRequest.createAlias(ALIAS, COLL_A+","+COLL_B).process(solrCluster.getSolrClient());
+    NamedList<Object> result = response.getResponse();
+    if(result.get("failure") != null) {
+      fail("Alias creation failed : " + result.get("failure"));
+    }
+    
+    index(COLL_A, 10);
+    index(COLL_B, 10);
+    
+  }
+  
+  
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();    
+  }
+  
+  @Override
+  @After
+  public void tearDown() throws Exception {
+    super.tearDown();
+  }
+  
+  @AfterClass
+  public static void tearDownCluster() throws Exception {
+    solrCluster.shutdown();    
+  }
+  
+  public void testOneCollectionPivotName() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", NAME_TXT_FIELD_NOT_MULTIVALUED);
+    QueryResponse response = solrCluster.getSolrClient().query(COLL_A, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      /*
+       * Can happen if the random string that is used as facet pivot value is a stopword, or an empty string.
+       * PivotFacetProcessor.getDocSet
+       *  ft.getFieldQuery(null, field, "a") // a stopword
+       *   -> returns null
+       *  searcher.getDocSet(query, base);
+       *   -> throws NPE
+       *  
+       *  ft.getFieldQuery(null, field, "") // empty str
+       *   -> returned query= name:
+       *  searcher.getDocSet(query, base);
+       *   -> returns DocSet size -1
+       */
+      fail("Facet pivot on one collection failed");
+    }
+  }
+  
+  public void testOneCollectionPivotInt() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", DYNAMIC_INT_FIELD_NOT_MULTIVALUED);
+    QueryResponse response = solrCluster.getSolrClient().query(COLL_A, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on one collection failed");
+    }
+  }
+  
+  public void testOneCollectionPivotFloat() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", DYNAMIC_FLOAT_FIELD_NOT_MULTIVALUED);
+    QueryResponse response = solrCluster.getSolrClient().query(COLL_A, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on one collection failed");
+    }
+  }
+  
+  public void testOneCollectionPivotDate() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", DYNAMIC_DATE_FIELD_NOT_MULTIVALUED);
+    QueryResponse response = solrCluster.getSolrClient().query(COLL_A, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on one collection failed");
+    }
+  }
+  
+  public void testOneCollectionPivotTitleFileType() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", TITLE_TXT_FIELD_NOT_MULTIVALUED,
+        "facet.pivot", String.join(",", FILETYPE_TXT_FIELD_NOT_MULTIVALUED,TITLE_TXT_FIELD_NOT_MULTIVALUED));
+    QueryResponse response = solrCluster.getSolrClient().query(COLL_A, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on one collection failed");
+    }
+  }
+  
+  public void testAliasPivotName() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", NAME_TXT_FIELD_NOT_MULTIVALUED);
+    final QueryResponse response = solrCluster.getSolrClient().query(ALIAS, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on the alias failed");
+    }
+  }
+  
+  public void testAliasPivotType() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", FILETYPE_TXT_FIELD_NOT_MULTIVALUED);
+    final QueryResponse response = solrCluster.getSolrClient().query(ALIAS, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on the alias failed");
+    }
+  }
+  
+  public void testAliasPivotFloat() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", DYNAMIC_FLOAT_FIELD_NOT_MULTIVALUED);
+    final QueryResponse response = solrCluster.getSolrClient().query(ALIAS, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on the alias failed");
+    }
+  }
+  
+  public void testAliasPivotDate() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", SUBJECT_TXT_FIELD_MULTIVALUED,
+        "facet.pivot", DYNAMIC_FLOAT_FIELD_NOT_MULTIVALUED);
+    final QueryResponse response = solrCluster.getSolrClient().query(ALIAS, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on the alias failed");
+    }
+  }
+  
+  public void testAliasPivotTitleFileType() throws SolrServerException, IOException {
+    SolrParams params = params("q", "*:*", "wt", "xml",
+        "rows", "0",
+        "facet", "true",
+        "facet.field", TITLE_TXT_FIELD_NOT_MULTIVALUED,
+        "facet.pivot", String.join(",", FILETYPE_TXT_FIELD_NOT_MULTIVALUED,TITLE_TXT_FIELD_NOT_MULTIVALUED));
+    QueryResponse response = solrCluster.getSolrClient().query(ALIAS, params);
+    NamedList<Object> result = response.getResponse();
+    if(result.get("facet_counts") == null) {
+      fail("Facet pivot on the alias failed");
+    }
+  }
+  
+  
+  private static void index(final String collection, final int numDocs) throws SolrServerException, IOException {
+    for(int i=0; i < numDocs; i++) {
+      final Map<String,SolrInputField> fieldValues= addDocFields(i);
+      final SolrInputDocument solrDoc = new SolrInputDocument(fieldValues);
+      solrDoc.addField(DYNAMIC_DATE_FIELD_NOT_MULTIVALUED, skewed(randomSkewedDate(), randomDate()));
+      solrDoc.addField(DYNAMIC_INT_FIELD_NOT_MULTIVALUED, skewed(TestUtil.nextInt(random(), 0, 100), random().nextInt()));
+      solrDoc.addField(DYNAMIC_FLOAT_FIELD_NOT_MULTIVALUED, skewed(1.0F / random().nextInt(25), random().nextFloat() * random().nextInt()));
+      solrCluster.getSolrClient().add(collection, solrDoc);      
+    }
+    solrCluster.getSolrClient().commit(COLL_A);
+    solrCluster.getSolrClient().commit(COLL_B);
+  }
+
+  private static Map<String,SolrInputField> addDocFields(final int id) {
+    final Map<String,SolrInputField> fieldValues = new HashMap<>();
+    final SolrInputField idField = new SolrInputField(ID_FIELD);
+    idField.setValue(String.valueOf(id));
+    fieldValues.put(ID_FIELD, idField);
+    final SolrInputField textField1 = new SolrInputField(NAME_TXT_FIELD_NOT_MULTIVALUED);
+    final SolrInputField textField2 = new SolrInputField(TXT_FIELD_MULTIVALUED);
+    final SolrInputField textField3 = new SolrInputField(TITLE_TXT_FIELD_NOT_MULTIVALUED);
+    final SolrInputField textField4 = new SolrInputField(FILETYPE_TXT_FIELD_NOT_MULTIVALUED);
+    final SolrInputField textField5 = new SolrInputField(SUBJECT_TXT_FIELD_MULTIVALUED);
+    final SolrInputField strField1 = new SolrInputField(DYNAMIC_STR_FIELD_NOT_MULTIVALUED);
+    final SolrInputField strField2 = new SolrInputField(DYNAMIC_STR_FIELD_MULTIVALUED);
+    final Random random = random();
+    textField1.setValue(randomText(random, 10, true) + ".txt"); // make it look like a file name
+    textField2.setValue(new String[] {randomText(random, 25, false), randomText(random, 25, false)});
+    textField3.setValue(randomText(random, 10, true));
+    textField4.setValue(randomText(random, 1, false));
+    textField5.setValue(new String[] {randomText(random, 2, false), randomText(random, 5, false)});
+    strField1.addValue(randomText(random, 1, false));
+    strField2.addValue(new String[] {randomText(random, 1, false), randomText(random, 1, false)});
+    fieldValues.put(NAME_TXT_FIELD_NOT_MULTIVALUED, textField1);
+    fieldValues.put(TXT_FIELD_MULTIVALUED, textField2);
+    if(random.nextInt(10) % 3 == 0) { // every now and then, a doc without a 'title' field.
+      fieldValues.put(TITLE_TXT_FIELD_NOT_MULTIVALUED, textField3);
+    }
+    fieldValues.put(FILETYPE_TXT_FIELD_NOT_MULTIVALUED, textField4);
+    fieldValues.put(SUBJECT_TXT_FIELD_MULTIVALUED, textField5);
+    fieldValues.put(DYNAMIC_STR_FIELD_NOT_MULTIVALUED, strField1);
+    fieldValues.put(DYNAMIC_STR_FIELD_MULTIVALUED, strField2);
+    return fieldValues;
+  }
+
+  private static String randomText(final Random random, final int maxWords, final boolean addNonAlphaChars) {
+    final StringBuilder builder = new StringBuilder();
+    int words = random.nextInt(maxWords);
+    while(words-- > 0) {
+      String word = "";
+      if(addNonAlphaChars && (words % 3 == 0)) {
+        word = RandomStringUtils.random(random.nextInt(3), "\\p{Digit}\\p{Punct}");
+        System.out.println("generated non-alpha string:" + word);     
+      } else {
+        word = RandomStringUtils.randomAlphabetic(1, 10);
+      }
+      builder.append(word).append(" ");
+    }
+    return builder.toString().trim();
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/search/TestTermQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestTermQParserPlugin.java
new file mode 100644
index 0000000..829d01f
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/TestTermQParserPlugin.java
@@ -0,0 +1,95 @@
+/*
+ * 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.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestTermQParserPlugin extends SolrTestCaseJ4 {
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig.xml", "schema.xml");
+
+    assertU(adoc("id","1", "author_s1", "Lev Grossman", "t_title", "The Magicians",  "cat_s", "fantasy", "pubyear_i", "2009"));
+    assertU(adoc("id", "2", "author_s1", "Robert Jordan", "t_title", "The Eye of the World", "cat_s", "fantasy", "cat_s", "childrens", "pubyear_i", "1990"));
+    assertU(adoc("id", "3", "author_s1", "Robert Jordan", "t_title", "The Great Hunt", "cat_s", "fantasy", "cat_s", "childrens", "pubyear_i", "1990"));
+    assertU(adoc("id", "4", "author_s1", "N.K. Jemisin", "t_title", "The Fifth Season", "cat_s", "fantasy", "pubyear_i", "2015"));
+    assertU(commit());
+    assertU(adoc("id", "5", "author_s1", "Ursula K. Le Guin", "t_title", "The Dispossessed", "cat_s", "scifi", "pubyear_i", "1974"));
+    assertU(adoc("id", "6", "author_s1", "Ursula K. Le Guin", "t_title", "The Left Hand of Darkness", "cat_s", "scifi", "pubyear_i", "1969"));
+    assertU(adoc("id", "7", "author_s1", "Isaac Asimov", "t_title", "Foundation", "cat_s", "scifi", "pubyear_i", "1951"));
+    assertU(commit());
+  }
+
+  @Test
+  public void testTextTermsQuery() {
+    // Single term value
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("q", "{!term f=t_title}left");
+    params.add("sort", "id asc");
+    assertQ(req(params, "indent", "on"), "*[count(//doc)=1]",
+        "//result/doc[1]/str[@name='id'][.='6']"
+    );
+    // Single term value
+    params = new ModifiableSolrParams();
+    params.add("q", "{!term f=t_title}the");
+    params.add("sort", "id asc");
+    assertQ(req(params, "indent", "on"), "*[count(//doc)=0]");
+  }
+  
+  @Test
+  public void testMissingField() {
+    assertQEx("Expecting bad request", "Missing field to query", req("q", "{!term}childrens"), SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testTermsMethodEquivalency() {
+    // Single-valued field
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.add("q","{!term f=author_s1}Robert Jordan");
+    params.add("sort", "id asc");
+    assertQ(req(params, "indent", "on"), "*[count(//doc)=2]",
+        "//result/doc[1]/str[@name='id'][.='2']",
+        "//result/doc[2]/str[@name='id'][.='3']"
+    );
+
+    // Multi-valued field
+    params = new ModifiableSolrParams();
+    params.add("q", "{!term f=cat_s}childrens");
+    params.add("sort", "id asc");
+    assertQ(req(params, "indent", "on"), "*[count(//doc)=2]",
+        "//result/doc[1]/str[@name='id'][.='2']",
+        "//result/doc[2]/str[@name='id'][.='3']"
+    );
+
+    // Numeric field
+    params = new ModifiableSolrParams();
+    params.add("q", "{!term f=pubyear_i}2009");
+    params.add("sort", "id asc");
+    assertQ(req(params, "indent", "on"), "*[count(//doc)=1]", "//result/doc[1]/str[@name='id'][.='1']");
+
+    // Numeric field
+    params = new ModifiableSolrParams();
+    params.add("q", "{!term f=pubyear_i}2009");
+    params.add("sort", "id asc");
+    assertQ(req(params, "indent", "on"), "*[count(//doc)=1]", "//result/doc[1]/str[@name='id'][.='1']");
+  }
+}