SOLR-11746: Existence query support for numeric point fields

This commit is contained in:
Houston Putman 2020-01-06 12:12:22 -05:00
parent b6f31835ad
commit f5ab3ca688
19 changed files with 196 additions and 42 deletions

View File

@ -214,6 +214,8 @@ Bug Fixes
* SOLR-13089: Fix lsof edge cases in the solr CLI script (Martijn Koster via janhoy) * SOLR-13089: Fix lsof edge cases in the solr CLI script (Martijn Koster via janhoy)
* SOLR-11746: Fixed existence query support for numeric point fields. (Kai Chan, hossman, Houston Putman)
Other Changes Other Changes
--------------------- ---------------------

View File

@ -266,7 +266,7 @@ public class ICUCollationField extends FieldType {
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
String f = field.getName(); String f = field.getName();
BytesRef low = part1 == null ? null : getCollationKey(f, part1); BytesRef low = part1 == null ? null : getCollationKey(f, part1);
BytesRef high = part2 == null ? null : getCollationKey(f, part2); BytesRef high = part2 == null ? null : getCollationKey(f, part2);

View File

@ -1187,10 +1187,14 @@ public abstract class SolrQueryParserBase extends QueryBuilder {
// called from parser // called from parser
protected Query getWildcardQuery(String field, String termStr) throws SyntaxError { protected Query getWildcardQuery(String field, String termStr) throws SyntaxError {
checkNullField(field); checkNullField(field);
// *:* -> MatchAllDocsQuery
if ("*".equals(termStr)) { if ("*".equals(termStr)) {
if ("*".equals(field) || getExplicitField() == null) { if ("*".equals(field) || getExplicitField() == null) {
// '*:*' and '*' -> MatchAllDocsQuery
return newMatchAllDocsQuery(); return newMatchAllDocsQuery();
} else {
// 'foo:*' -> empty prefix query
return getPrefixQuery(field, "");
} }
} }

View File

@ -316,7 +316,7 @@ public abstract class AbstractSpatialFieldType<T extends SpatialStrategy> extend
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
if (!minInclusive || !maxInclusive) if (!minInclusive || !maxInclusive)
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Both sides of spatial range query must be inclusive: " + field.getName()); throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Both sides of spatial range query must be inclusive: " + field.getName());
Point p1 = SpatialUtils.parsePointSolrException(part1, ctx); Point p1 = SpatialUtils.parsePointSolrException(part1, ctx);

View File

@ -236,7 +236,7 @@ public class CollationField extends FieldType {
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
String f = field.getName(); String f = field.getName();
BytesRef low = part1 == null ? null : getCollationKey(f, part1); BytesRef low = part1 == null ? null : getCollationKey(f, part1);
BytesRef high = part2 == null ? null : getCollationKey(f, part2); BytesRef high = part2 == null ? null : getCollationKey(f, part2);

View File

@ -251,7 +251,7 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
CurrencyValue valueDefault; CurrencyValue valueDefault;
valueDefault = value.convertTo(provider, defaultCurrency); valueDefault = value.convertTo(provider, defaultCurrency);
return getRangeQuery(parser, field, valueDefault, valueDefault, true, true); return getRangeQueryInternal(parser, field, valueDefault, valueDefault, true, true);
} }
/** /**
@ -317,7 +317,7 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, final boolean minInclusive, final boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, final boolean minInclusive, final boolean maxInclusive) {
final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency); final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency);
final CurrencyValue p2 = CurrencyValue.parse(part2, defaultCurrency); final CurrencyValue p2 = CurrencyValue.parse(part2, defaultCurrency);
@ -327,10 +327,10 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
": range queries only supported when upper and lower bound have same currency."); ": range queries only supported when upper and lower bound have same currency.");
} }
return getRangeQuery(parser, field, p1, p2, minInclusive, maxInclusive); return getRangeQueryInternal(parser, field, p1, p2, minInclusive, maxInclusive);
} }
public Query getRangeQuery(QParser parser, SchemaField field, final CurrencyValue p1, final CurrencyValue p2, final boolean minInclusive, final boolean maxInclusive) { private Query getRangeQueryInternal(QParser parser, SchemaField field, final CurrencyValue p1, final CurrencyValue p2, final boolean minInclusive, final boolean maxInclusive) {
String currencyCode = (p1 != null) ? p1.getCurrencyCode() : String currencyCode = (p1 != null) ? p1.getCurrencyCode() :
(p2 != null) ? p2.getCurrencyCode() : defaultCurrency; (p2 != null) ? p2.getCurrencyCode() : defaultCurrency;

View File

@ -143,7 +143,7 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String startStr, String endStr, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String startStr, String endStr, boolean minInclusive, boolean maxInclusive) {
if (parser == null) {//null when invoked by SimpleFacets. But getQueryFromSpatialArgs expects to get localParams. if (parser == null) {//null when invoked by SimpleFacets. But getQueryFromSpatialArgs expects to get localParams.
final SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo(); final SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
parser = new QParser("", null, requestInfo.getReq().getParams(), requestInfo.getReq()) { parser = new QParser("", null, requestInfo.getReq().getParams(), requestInfo.getReq()) {

View File

@ -63,13 +63,13 @@ public class EnumField extends AbstractEnumField {
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) {
Integer minValue = enumMapping.stringValueToIntValue(min); Integer minValue = enumMapping.stringValueToIntValue(min);
Integer maxValue = enumMapping.stringValueToIntValue(max); Integer maxValue = enumMapping.stringValueToIntValue(max);
if (field.multiValued() && field.hasDocValues() && !field.indexed()) { if (field.multiValued() && field.hasDocValues() && !field.indexed()) {
// for the multi-valued dv-case, the default rangeimpl over toInternal is correct // for the multi-valued dv-case, the default rangeimpl over toInternal is correct
return super.getRangeQuery(parser, field, minValue.toString(), maxValue.toString(), minInclusive, maxInclusive); return super.getSpecializedRangeQuery(parser, field, minValue.toString(), maxValue.toString(), minInclusive, maxInclusive);
} }
Query query = null; Query query = null;
final boolean matchOnly = field.hasDocValues() && !field.indexed(); final boolean matchOnly = field.hasDocValues() && !field.indexed();

View File

@ -57,7 +57,7 @@ public class EnumFieldType extends AbstractEnumField {
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) {
Integer minValue = enumMapping.stringValueToIntValue(min); Integer minValue = enumMapping.stringValueToIntValue(min);
Integer maxValue = enumMapping.stringValueToIntValue(max); Integer maxValue = enumMapping.stringValueToIntValue(max);

View File

@ -43,6 +43,7 @@ import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.ValueSource; import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DocValuesFieldExistsQuery;
import org.apache.lucene.search.DocValuesRewriteMethod; import org.apache.lucene.search.DocValuesRewriteMethod;
import org.apache.lucene.search.MultiTermQuery; import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.PrefixQuery;
@ -457,11 +458,13 @@ public abstract class FieldType extends FieldProperties {
* *
* @param parser the {@link org.apache.solr.search.QParser} calling the method * @param parser the {@link org.apache.solr.search.QParser} calling the method
* @param sf the schema field * @param sf the schema field
* @param termStr the term string for prefix query * @param termStr the term string for prefix query, if blank then this query should match all docs with this field
* @return a Query instance to perform prefix search * @return a Query instance to perform prefix search
*
*/ */
public Query getPrefixQuery(QParser parser, SchemaField sf, String termStr) { public Query getPrefixQuery(QParser parser, SchemaField sf, String termStr) {
if ("".equals(termStr)) {
return getRangeQuery(parser, sf, null, null, true, true);
}
PrefixQuery query = new PrefixQuery(new Term(sf.getName(), termStr)); PrefixQuery query = new PrefixQuery(new Term(sf.getName(), termStr));
query.setRewriteMethod(sf.getType().getRewriteMethod(parser, sf)); query.setRewriteMethod(sf.getType().getRewriteMethod(parser, sf));
return query; return query;
@ -846,9 +849,36 @@ public abstract class FieldType extends FieldProperties {
// trivial base case // trivial base case
return null; return null;
} }
/**
* Returns a Query instance for doing range searches on this field type. {@link org.apache.solr.search.SolrQueryParser}
* currently passes part1 and part2 as null if they are '*' respectively. minInclusive and maxInclusive are both true
* currently by SolrQueryParser but that may change in the future. Also, other QueryParser implementations may have
* different semantics.
* <p>
* If the field has docValues enabled, and the range query has '*'s or nulls on either side, then a {@link org.apache.lucene.search.DocValuesFieldExistsQuery} is returned.
*
* Sub-classes should override the "getSpecializedRangeQuery" method to provide their own range query implementation. They should strive to
* handle nulls in part1 and/or part2 as well as unequal minInclusive and maxInclusive parameters gracefully.
*
*
* @param parser the {@link org.apache.solr.search.QParser} calling the method
* @param field the schema field
* @param part1 the lower boundary of the range, nulls are allowed.
* @param part2 the upper boundary of the range, nulls are allowed
* @param minInclusive whether the minimum of the range is inclusive or not
* @param maxInclusive whether the maximum of the range is inclusive or not
* @return a Query instance to perform range search according to given parameters
*
*/
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
if (field.hasDocValues() && part1 == null && part2 == null) {
return new DocValuesFieldExistsQuery(field.getName());
} else {
return getSpecializedRangeQuery(parser, field, part1, part2, minInclusive, maxInclusive);
}
}
/** /**
* Returns a Query instance for doing range searches on this field type. {@link org.apache.solr.search.SolrQueryParser} * Returns a Query instance for doing range searches on this field type. {@link org.apache.solr.search.SolrQueryParser}
* currently passes part1 and part2 as null if they are '*' respectively. minInclusive and maxInclusive are both true * currently passes part1 and part2 as null if they are '*' respectively. minInclusive and maxInclusive are both true
@ -867,20 +897,21 @@ public abstract class FieldType extends FieldProperties {
* @return a Query instance to perform range search according to given parameters * @return a Query instance to perform range search according to given parameters
* *
*/ */
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
// TODO: change these all to use readableToIndexed/bytes instead (e.g. for unicode collation) // TODO: change these all to use readableToIndexed/bytes instead (e.g. for unicode collation)
final BytesRef miValue = part1 == null ? null : new BytesRef(toInternal(part1)); final BytesRef miValue = part1 == null ? null : new BytesRef(toInternal(part1));
final BytesRef maxValue = part2 == null ? null : new BytesRef(toInternal(part2)); final BytesRef maxValue = part2 == null ? null : new BytesRef(toInternal(part2));
if (field.hasDocValues() && !field.indexed()) { if (field.hasDocValues() && !field.indexed()) {
return SortedSetDocValuesField.newSlowRangeQuery( return SortedSetDocValuesField.newSlowRangeQuery(
field.getName(), field.getName(),
miValue, maxValue, miValue, maxValue,
minInclusive, maxInclusive); minInclusive, maxInclusive);
} else { } else {
SolrRangeQuery rangeQuery = new SolrRangeQuery( SolrRangeQuery rangeQuery = new SolrRangeQuery(
field.getName(), field.getName(),
miValue, maxValue, miValue, maxValue,
minInclusive, maxInclusive); minInclusive, maxInclusive);
return rangeQuery; return rangeQuery;
} }
} }
@ -891,7 +922,6 @@ public abstract class FieldType extends FieldProperties {
* @param field The {@link org.apache.solr.schema.SchemaField} of the field to search * @param field The {@link org.apache.solr.schema.SchemaField} of the field to search
* @param externalVal The String representation of the value to search * @param externalVal The String representation of the value to search
* @return The {@link org.apache.lucene.search.Query} instance. This implementation returns a {@link org.apache.lucene.search.TermQuery} but overriding queries may not * @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) { public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
BytesRefBuilder br = new BytesRefBuilder(); BytesRefBuilder br = new BytesRefBuilder();

View File

@ -103,7 +103,7 @@ public class LatLonType extends AbstractSubTypeFieldType implements SpatialQuery
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
Point p1 = SpatialUtils.parsePointSolrException(part1, SpatialContext.GEO); Point p1 = SpatialUtils.parsePointSolrException(part1, SpatialContext.GEO);
Point p2 = SpatialUtils.parsePointSolrException(part2, SpatialContext.GEO); Point p2 = SpatialUtils.parsePointSolrException(part2, SpatialContext.GEO);

View File

@ -165,8 +165,8 @@ public abstract class PointField extends NumericFieldType {
boolean maxInclusive); boolean maxInclusive);
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive,
boolean maxInclusive) { boolean maxInclusive) {
if (!field.indexed() && field.hasDocValues()) { if (!field.indexed() && field.hasDocValues()) {
return getDocValuesRangeQuery(parser, field, min, max, minInclusive, maxInclusive); return getDocValuesRangeQuery(parser, field, min, max, minInclusive, maxInclusive);
} else if (field.indexed() && field.hasDocValues()) { } else if (field.indexed() && field.hasDocValues()) {
@ -222,6 +222,9 @@ public abstract class PointField extends NumericFieldType {
@Override @Override
public Query getPrefixQuery(QParser parser, SchemaField sf, String termStr) { public Query getPrefixQuery(QParser parser, SchemaField sf, String termStr) {
if ("".equals(termStr)) {
return super.getPrefixQuery(parser, sf, termStr);
}
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't run prefix queries on numeric fields"); throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't run prefix queries on numeric fields");
} }

View File

@ -128,7 +128,7 @@ public class PointType extends CoordinateFieldType implements SpatialQueryable {
/** /**
* Care should be taken in calling this with higher order dimensions for performance reasons. * Care should be taken in calling this with higher order dimensions for performance reasons.
*/ */
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
//Query could look like: [x1,y1 TO x2,y2] for 2 dimension, but could look like: [x1,y1,z1 TO x2,y2,z2], and can be extrapolated to n-dimensions //Query could look like: [x1,y1 TO x2,y2] for 2 dimension, but could look like: [x1,y1,z1 TO x2,y2,z2], and can be extrapolated to n-dimensions
//thus, this query essentially creates a box, cube, etc. //thus, this query essentially creates a box, cube, etc.
String[] p1 = parseCommaSeparatedList(part1, dimension); String[] p1 = parseCommaSeparatedList(part1, dimension);

View File

@ -158,7 +158,7 @@ public class TextField extends FieldType {
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
Analyzer multiAnalyzer = getMultiTermAnalyzer(); Analyzer multiAnalyzer = getMultiTermAnalyzer();
BytesRef lower = analyzeMultiTerm(field.getName(), part1, multiAnalyzer); BytesRef lower = analyzeMultiTerm(field.getName(), part1, multiAnalyzer);
BytesRef upper = analyzeMultiTerm(field.getName(), part2, multiAnalyzer); BytesRef upper = analyzeMultiTerm(field.getName(), part2, multiAnalyzer);

View File

@ -298,10 +298,10 @@ public class TrieField extends NumericFieldType {
} }
@Override @Override
public Query getRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) { protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) {
if (field.multiValued() && field.hasDocValues() && !field.indexed()) { if (field.multiValued() && field.hasDocValues() && !field.indexed()) {
// for the multi-valued dv-case, the default rangeimpl over toInternal is correct // for the multi-valued dv-case, the default rangeimpl over toInternal is correct
return super.getRangeQuery(parser, field, min, max, minInclusive, maxInclusive); return super.getSpecializedRangeQuery(parser, field, min, max, minInclusive, maxInclusive);
} }
int ps = precisionStep; int ps = precisionStep;
Query query; Query query;

View File

@ -686,23 +686,33 @@
<dynamicField name="*_l_dv" type="long" indexed="true" stored="true" docValues="true" multiValued="false"/> <dynamicField name="*_l_dv" type="long" indexed="true" stored="true" docValues="true" multiValued="false"/>
<dynamicField name="*_ls_dv" type="long" indexed="true" stored="true" docValues="true" multiValued="true"/> <dynamicField name="*_ls_dv" type="long" indexed="true" stored="true" docValues="true" multiValued="true"/>
<dynamicField name="*_l_dvo" type="long" indexed="false" stored="true" docValues="true"/> <dynamicField name="*_l_dvo" type="long" indexed="false" stored="true" docValues="true"/>
<dynamicField name="*_d" type="double" indexed="true" stored="true"/> <dynamicField name="*_d" type="double" indexed="true" stored="true"/>
<dynamicField name="*_ds" type="double" indexed="true" stored="true" multiValued="true"/> <dynamicField name="*_ds" type="double" indexed="true" stored="true" multiValued="true"/>
<dynamicField name="*_d_dv" type="double" indexed="true" stored="true" docValues="true" multiValued="false"/> <dynamicField name="*_d_dv" type="double" indexed="true" stored="true" docValues="true" multiValued="false"/>
<dynamicField name="*_ds_dv" type="double" indexed="true" stored="true" docValues="true" multiValued="true"/> <dynamicField name="*_ds_dv" type="double" indexed="true" stored="true" docValues="true" multiValued="true"/>
<dynamicField name="*_d_dvo" type="double" indexed="false" stored="true" docValues="true"/> <dynamicField name="*_d_dvo" type="double" indexed="false" stored="true" docValues="true"/>
<dynamicField name="*_dt" type="date" indexed="true" stored="true"/>
<dynamicField name="*_dts" type="date" indexed="true" stored="true" multiValued="true"/>
<dynamicField name="*_dt_dv" type="date" indexed="true" stored="true" docValues="true" multiValued="false"/>
<dynamicField name="*_dts_dv" type="date" indexed="true" stored="true" docValues="true" multiValued="true"/>
<dynamicField name="*_dt_dvo" type="date" indexed="false" stored="true" docValues="true"/>
<dynamicField name="*_s1" type="string" indexed="true" stored="true" multiValued="false"/> <dynamicField name="*_s1" type="string" indexed="true" stored="true" multiValued="false"/>
<!-- :TODO: why are these identical?!?!?! --> <!-- :TODO: why are these identical?!?!?! -->
<dynamicField name="*_s" type="string" indexed="true" stored="true" multiValued="true"/> <dynamicField name="*_s" type="string" indexed="true" stored="true" multiValued="true"/>
<dynamicField name="*_ss" type="string" indexed="true" stored="true" multiValued="true"/> <dynamicField name="*_ss" type="string" indexed="true" stored="true" multiValued="true"/>
<dynamicField name="*_s_dv" type="string" indexed="true" stored="true" docValues="true"/>
<dynamicField name="*_sdv" type="string" indexed="false" stored="false" docValues="true" useDocValuesAsStored="true"/> <dynamicField name="*_sdv" type="string" indexed="false" stored="false" docValues="true" useDocValuesAsStored="true"/>
<dynamicField name="*_bdv" type="boolean" indexed="false" stored="false" docValues="true" useDocValuesAsStored="true"/> <dynamicField name="*_ss_dv" type="string" indexed="true" stored="true" docValues="true" multiValued="true"/>
<dynamicField name="*_t" type="text" indexed="true" stored="true"/> <dynamicField name="*_t" type="text" indexed="true" stored="true"/>
<dynamicField name="*_tt" type="text" indexed="true" stored="true"/> <dynamicField name="*_tt" type="text" indexed="true" stored="true"/>
<dynamicField name="*_b" type="boolean" indexed="true" stored="true"/> <dynamicField name="*_b" type="boolean" indexed="true" stored="true"/>
<dynamicField name="*_dt" type="date" indexed="true" stored="true"/> <dynamicField name="*_bs" type="boolean" indexed="true" stored="true" multiValued="true"/>
<dynamicField name="*_bdv" type="boolean" indexed="false" stored="false" docValues="true" useDocValuesAsStored="true"/>
<dynamicField name="*_b_dv" type="boolean" indexed="true" stored="true" docValues="true"/>
<dynamicField name="*_bs_dv" type="boolean" indexed="true" stored="true" docValues="true" multiValued="true"/>
<dynamicField name="*_pi" type="pint" indexed="true" multiValued="false"/> <dynamicField name="*_pi" type="pint" indexed="true" multiValued="false"/>
<dynamicField name="*_pl" type="plong" indexed="true" multiValued="false"/> <dynamicField name="*_pl" type="plong" indexed="true" multiValued="false"/>

View File

@ -16,6 +16,7 @@
*/ */
package org.apache.solr.search; package org.apache.solr.search;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -94,6 +95,23 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
" +apache +solr"); " +apache +solr");
} }
public void testQueryLuceneAllDocsWithField() throws Exception {
// for all "primative" types, 'foo:*' should be functionally equivilent to "foo:[* TO *]"
// whatever implementation/optimizations exist for one syntax, should exist for the other syntax as well
// (regardless of docValues, multivalued, etc...)
for (String field : Arrays.asList("foo_sI", "foo_sS", "foo_s1", "foo_s",
"t_foo", "tv_foo", "tv_mv_foo",
"foo_b",
"foo_i", "foo_is", "foo_i_dvo",
"foo_l", "foo_ll", "foo_l_dvo",
"foo_f", "foo_f_dvo",
"foo_d",
"foo_dt")) {
assertQueryEquals("lucene", field + ":*", field + ":[* TO *]");
}
}
public void testQueryPrefix() throws Exception { public void testQueryPrefix() throws Exception {
SolrQueryRequest req = req("myField","foo_s"); SolrQueryRequest req = req("myField","foo_s");
try { try {

View File

@ -17,6 +17,7 @@
package org.apache.solr.search; package org.apache.solr.search;
import java.util.Arrays; import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -28,6 +29,7 @@ import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.DocValuesFieldExistsQuery;
import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PointInSetQuery; import org.apache.lucene.search.PointInSetQuery;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
@ -35,6 +37,7 @@ import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermQuery;
import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.MapSolrParams; import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.params.SolrParams;
@ -44,7 +47,9 @@ import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.parser.QueryParser; import org.apache.solr.parser.QueryParser;
import org.apache.solr.query.FilterQuery; import org.apache.solr.query.FilterQuery;
import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField; import org.apache.solr.schema.SchemaField;
import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@ -59,6 +64,13 @@ public class TestSolrQueryParser extends SolrTestCaseJ4 {
createIndex(); createIndex();
} }
private static final List<String> HAS_VAL_FIELDS = new ArrayList<String>(31);
@AfterClass
public static void afterClass() throws Exception {
HAS_VAL_FIELDS.clear();
}
public static void createIndex() { public static void createIndex() {
String v; String v;
v = "how now brown cow"; v = "how now brown cow";
@ -73,12 +85,55 @@ public class TestSolrQueryParser extends SolrTestCaseJ4 {
assertU(adoc("id", "13", "eee_s", "'balance'", "rrr_s", "/leading_slash")); assertU(adoc("id", "13", "eee_s", "'balance'", "rrr_s", "/leading_slash"));
assertU(adoc("id", "20", "syn", "wifi ATM")); assertU(adoc("id", "20", "syn", "wifi ATM"));
{ // make a doc that has a value in *lots* of fields that no other doc has
SolrInputDocument doc = sdoc("id", "999");
// numbers...
for (String t : Arrays.asList("i", "l", "f", "d")) {
for (String s : Arrays.asList("", "s", "_dv", "s_dv", "_dvo")) {
final String f = "has_val_" + t + s;
HAS_VAL_FIELDS.add(f);
doc.addField(f, "42");
}
}
// boolean...
HAS_VAL_FIELDS.add("has_val_b");
doc.addField("has_val_b", "false");
// dates (and strings/text -- they don't care about the format)...
for (String s : Arrays.asList("dt", "s", "s1", "t")) {
final String f = "has_val_" + s;
HAS_VAL_FIELDS.add(f);
doc.addField(f, "2019-01-12T00:00:00Z");
}
assertU(adoc(doc));
}
assertU(adoc("id", "30", "shingle23", "A B X D E")); assertU(adoc("id", "30", "shingle23", "A B X D E"));
assertU(commit()); assertU(commit());
} }
public void testDocsWithValuesInField() throws Exception {
assertEquals("someone changed the test setup of HAS_VAL_FIELDS, w/o updating the sanity check",
25, HAS_VAL_FIELDS.size());
for (String f : HAS_VAL_FIELDS) {
// for all of these fields, these 2 syntaxes should be functionally equivilent
// in matching the one doc that contains these fields
for (String q : Arrays.asList( f + ":*", f + ":[* TO *]" )) {
assertJQ(req("q", q)
, "/response/numFound==1"
, "/response/docs/[0]/id=='999'"
);
// the same syntaxes should be valid even if no doc has the field...
assertJQ(req("q", "bogus___" + q)
, "/response/numFound==0"
);
}
}
}
@Test @Test
public void testPhrase() { public void testPhrase() {
// "text" field's type has WordDelimiterGraphFilter (WDGFF) and autoGeneratePhraseQueries=true // "text" field's type has WordDelimiterGraphFilter (WDGFF) and autoGeneratePhraseQueries=true
@ -1135,14 +1190,14 @@ public class TestSolrQueryParser extends SolrTestCaseJ4 {
"is_dv", "fs_dv", "ds_dv", "ls_dv", "is_dv", "fs_dv", "ds_dv", "ls_dv",
"i_dvo", "f_dvo", "d_dvo", "l_dvo", "i_dvo", "f_dvo", "d_dvo", "l_dvo",
}; };
for (String suffix:fieldSuffix) { for (String suffix:fieldSuffix) {
//Good queries //Good queries
qParser = QParser.getParser("foo_" + suffix + ":(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 25)", req); qParser = QParser.getParser("foo_" + suffix + ":(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 25)", req);
qParser.setIsFilter(true); qParser.setIsFilter(true);
qParser.getQuery(); qParser.getQuery();
} }
for (String suffix:fieldSuffix) { for (String suffix:fieldSuffix) {
qParser = QParser.getParser("foo_" + suffix + ":(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 NOT_A_NUMBER)", req); qParser = QParser.getParser("foo_" + suffix + ":(1 2 3 4 5 6 7 8 9 10 20 19 18 17 16 15 14 13 12 NOT_A_NUMBER)", req);
qParser.setIsFilter(true); // this may change in the future qParser.setIsFilter(true); // this may change in the future
@ -1150,7 +1205,39 @@ public class TestSolrQueryParser extends SolrTestCaseJ4 {
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code()); assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertTrue("Unexpected exception: " + e.getMessage(), e.getMessage().contains("Invalid Number: NOT_A_NUMBER")); assertTrue("Unexpected exception: " + e.getMessage(), e.getMessage().contains("Invalid Number: NOT_A_NUMBER"));
} }
}
@Test
public void testFieldExistsQueries() throws SyntaxError {
SolrQueryRequest req = req();
IndexSchema indexSchema = h.getCore().getLatestSchema();
String[] fieldSuffix = new String[] {
"ti", "tf", "td", "tl", "tdt",
"pi", "pf", "pd", "pl", "pdt",
"i", "f", "d", "l", "dt", "s", "b",
"is", "fs", "ds", "ls", "dts", "ss", "bs",
"i_dv", "f_dv", "d_dv", "l_dv", "dt_dv", "s_dv", "b_dv",
"is_dv", "fs_dv", "ds_dv", "ls_dv", "dts_dv", "ss_dv", "bs_dv",
"i_dvo", "f_dvo", "d_dvo", "l_dvo", "dt_dvo",
"t"
};
String[] existenceQueries = new String[] {
"*", "[* TO *]"
};
for (String existenceQuery : existenceQueries) {
for (String suffix : fieldSuffix) {
String field = "foo_" + suffix;
String query = field + ":" + existenceQuery;
QParser qParser = QParser.getParser(query, req);
if (indexSchema.getField(field).hasDocValues()) {
assertTrue("Field has docValues, so existence query \"" + query + "\" should return DocValuesFieldExistsQuery", qParser.getQuery() instanceof DocValuesFieldExistsQuery);
} else {
assertFalse("Field doesn't have docValues, so existence query \"" + query + "\" should not return DocValuesFieldExistsQuery", qParser.getQuery() instanceof DocValuesFieldExistsQuery);
}
}
}
} }
} }

View File

@ -322,10 +322,10 @@ Comments may be nested.
Solr's standard query parser originated as a variation of Lucene's "classic" QueryParser. It diverges in the following ways: Solr's standard query parser originated as a variation of Lucene's "classic" QueryParser. It diverges in the following ways:
* A `*` may be used for either or both endpoints to specify an open-ended range query * A `*` may be used for either or both endpoints to specify an open-ended range query, or by itself as an existence query.
** `field:[* TO 100]` finds all field values less than or equal to 100 ** `field:[* TO 100]` finds all field values less than or equal to 100
** `field:[100 TO *]` finds all field values greater than or equal to 100 ** `field:[100 TO *]` finds all field values greater than or equal to 100
** `field:[* TO *]` matches all documents with the field ** `field:*` or `field:[* TO *]` matches all documents where the field exists
* Pure negative queries (all clauses prohibited) are allowed (only as a top-level clause) * Pure negative queries (all clauses prohibited) are allowed (only as a top-level clause)
** `-inStock:false` finds all field values where inStock is not false ** `-inStock:false` finds all field values where inStock is not false
** `-field:[* TO *]` finds all documents without a value for field ** `-field:[* TO *]` finds all documents without a value for field