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']");
+ }
+}