blob: d33e48441cdda18b8814d413b5f921e1f7497733 [file] [log] [blame]
/*
* 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.schema;
import java.util.EnumSet;
import org.apache.lucene.document.DoublePoint;
import org.apache.lucene.document.FloatPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.NumericUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.search.FunctionRangeQuery;
import org.apache.solr.search.QParser;
import org.apache.solr.search.function.ValueSourceRangeFilter;
import org.apache.solr.util.DateMathParser;
public abstract class NumericFieldType extends PrimitiveFieldType {
protected NumberType type;
/**
* @return the type of this field
*/
@Override
public NumberType getNumberType() {
return type;
}
private static long FLOAT_MINUS_ZERO_BITS = (long)Float.floatToIntBits(-0f);
private static long DOUBLE_MINUS_ZERO_BITS = Double.doubleToLongBits(-0d);
protected Query getDocValuesRangeQuery(QParser parser, SchemaField field, String min, String max,
boolean minInclusive, boolean maxInclusive) {
assert field.hasDocValues() && (field.getType().isPointField() || !field.multiValued());
switch (getNumberType()) {
case INTEGER:
return numericDocValuesRangeQuery(field.getName(),
min == null ? null : (long) parseIntFromUser(field.getName(), min),
max == null ? null : (long) parseIntFromUser(field.getName(), max),
minInclusive, maxInclusive, field.multiValued());
case FLOAT:
if (field.multiValued()) {
return getRangeQueryForMultiValuedFloatDocValues(field, min, max, minInclusive, maxInclusive);
} else {
return getRangeQueryForFloatDoubleDocValues(field, min, max, minInclusive, maxInclusive);
}
case LONG:
return numericDocValuesRangeQuery(field.getName(),
min == null ? null : parseLongFromUser(field.getName(), min),
max == null ? null : parseLongFromUser(field.getName(),max),
minInclusive, maxInclusive, field.multiValued());
case DOUBLE:
if (field.multiValued()) {
return getRangeQueryForMultiValuedDoubleDocValues(field, min, max, minInclusive, maxInclusive);
} else {
return getRangeQueryForFloatDoubleDocValues(field, min, max, minInclusive, maxInclusive);
}
case DATE:
return numericDocValuesRangeQuery(field.getName(),
min == null ? null : DateMathParser.parseMath(null, min).getTime(),
max == null ? null : DateMathParser.parseMath(null, max).getTime(),
minInclusive, maxInclusive, field.multiValued());
default:
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown type for numeric field");
}
}
protected Query getRangeQueryForFloatDoubleDocValues(SchemaField sf, String min, String max, boolean minInclusive, boolean maxInclusive) {
Query query;
String fieldName = sf.getName();
long minBits, maxBits;
boolean minNegative, maxNegative;
Number minVal, maxVal;
if (getNumberType() == NumberType.FLOAT) {
if (min == null) {
minVal = Float.NEGATIVE_INFINITY;
} else {
minVal = parseFloatFromUser(sf.getName(), min);
if (!minInclusive) {
if (minVal.floatValue() == Float.POSITIVE_INFINITY) return new MatchNoDocsQuery();
minVal = FloatPoint.nextUp(minVal.floatValue());
}
}
if (max == null) {
maxVal = Float.POSITIVE_INFINITY;
} else {
maxVal = parseFloatFromUser(sf.getName(), max);
if (!maxInclusive) {
if (maxVal.floatValue() == Float.NEGATIVE_INFINITY) return new MatchNoDocsQuery();
maxVal = FloatPoint.nextDown(maxVal.floatValue());
}
}
minBits = Float.floatToIntBits(minVal.floatValue());
maxBits = Float.floatToIntBits(maxVal.floatValue());
minNegative = minVal.floatValue() < 0f || minBits == FLOAT_MINUS_ZERO_BITS;
maxNegative = maxVal.floatValue() < 0f || maxBits == FLOAT_MINUS_ZERO_BITS;
} else {
assert getNumberType() == NumberType.DOUBLE;
if (min == null) {
minVal = Double.NEGATIVE_INFINITY;
} else {
minVal = parseDoubleFromUser(sf.getName(), min);
if (!minInclusive) {
if (minVal.doubleValue() == Double.POSITIVE_INFINITY) return new MatchNoDocsQuery();
minVal = DoublePoint.nextUp(minVal.doubleValue());
}
}
if (max == null) {
maxVal = Double.POSITIVE_INFINITY;
} else {
maxVal = parseDoubleFromUser(sf.getName(), max);
if (!maxInclusive) {
if (maxVal.doubleValue() == Double.NEGATIVE_INFINITY) return new MatchNoDocsQuery();
maxVal = DoublePoint.nextDown(maxVal.doubleValue());
}
}
minBits = Double.doubleToLongBits(minVal.doubleValue());
maxBits = Double.doubleToLongBits(maxVal.doubleValue());
minNegative = minVal.doubleValue() < 0d || minBits == DOUBLE_MINUS_ZERO_BITS;
maxNegative = maxVal.doubleValue() < 0d || maxBits == DOUBLE_MINUS_ZERO_BITS;
}
// If min is negative (or -0d) and max is positive (or +0d), then issue a FunctionRangeQuery
if (minNegative && !maxNegative) {
ValueSource vs = getValueSource(sf, null);
query = new FunctionRangeQuery(new ValueSourceRangeFilter(vs, minVal.toString(), maxVal.toString(), true, true));
} else if (minNegative && maxNegative) {// If both max and min are negative (or -0d), then issue range query with max and min reversed
query = numericDocValuesRangeQuery
(fieldName, maxBits, minBits, true, true, false);
} else { // If both max and min are positive, then issue range query
query = numericDocValuesRangeQuery
(fieldName, minBits, maxBits, true, true, false);
}
return query;
}
protected Query getRangeQueryForMultiValuedDoubleDocValues(SchemaField sf, String min, String max, boolean minInclusive, boolean maxInclusive) {
double minVal,maxVal;
if (min == null) {
minVal = Double.NEGATIVE_INFINITY;
} else {
minVal = parseDoubleFromUser(sf.getName(), min);
if (!minInclusive) {
if (minVal == Double.POSITIVE_INFINITY) return new MatchNoDocsQuery();
minVal = DoublePoint.nextUp(minVal);
}
}
if (max == null) {
maxVal = Double.POSITIVE_INFINITY;
} else {
maxVal = parseDoubleFromUser(sf.getName(), max);
if (!maxInclusive) {
if (maxVal == Double.NEGATIVE_INFINITY) return new MatchNoDocsQuery();
maxVal = DoublePoint.nextDown(maxVal);
}
}
Long minBits = NumericUtils.doubleToSortableLong(minVal);
Long maxBits = NumericUtils.doubleToSortableLong(maxVal);
return numericDocValuesRangeQuery(sf.getName(), minBits, maxBits, true, true, true);
}
protected Query getRangeQueryForMultiValuedFloatDocValues(SchemaField sf, String min, String max, boolean minInclusive, boolean maxInclusive) {
float minVal,maxVal;
if (min == null) {
minVal = Float.NEGATIVE_INFINITY;
} else {
minVal = parseFloatFromUser(sf.getName(), min);
if (!minInclusive) {
if (minVal == Float.POSITIVE_INFINITY) return new MatchNoDocsQuery();
minVal = FloatPoint.nextUp(minVal);
}
}
if (max == null) {
maxVal = Float.POSITIVE_INFINITY;
} else {
maxVal = parseFloatFromUser(sf.getName(), max);
if (!maxInclusive) {
if (maxVal == Float.NEGATIVE_INFINITY) return new MatchNoDocsQuery();
maxVal = FloatPoint.nextDown(maxVal);
}
}
Long minBits = (long)NumericUtils.floatToSortableInt(minVal);
Long maxBits = (long)NumericUtils.floatToSortableInt(maxVal);
return numericDocValuesRangeQuery(sf.getName(), minBits, maxBits, true, true, true);
}
public static Query numericDocValuesRangeQuery(
String field,
Number lowerValue, Number upperValue,
boolean lowerInclusive, boolean upperInclusive,
boolean multiValued) {
long actualLowerValue = Long.MIN_VALUE;
if (lowerValue != null) {
actualLowerValue = lowerValue.longValue();
if (lowerInclusive == false) {
if (actualLowerValue == Long.MAX_VALUE) {
return new MatchNoDocsQuery();
}
++actualLowerValue;
}
}
long actualUpperValue = Long.MAX_VALUE;
if (upperValue != null) {
actualUpperValue = upperValue.longValue();
if (upperInclusive == false) {
if (actualUpperValue == Long.MIN_VALUE) {
return new MatchNoDocsQuery();
}
--actualUpperValue;
}
}
if (multiValued) {
// In multiValued case use SortedNumericDocValuesField, this won't work for Trie*Fields wince they use BinaryDV in the multiValue case
return SortedNumericDocValuesField.newSlowRangeQuery(field, actualLowerValue, actualUpperValue);
} else {
return NumericDocValuesField.newSlowRangeQuery(field, actualLowerValue, actualUpperValue);
}
}
/**
* Wrapper for {@link Long#parseLong(String)} that throws a BAD_REQUEST error if the input is not valid
* @param fieldName used in any exception, may be null
* @param val string to parse, NPE if null
*/
static long parseLongFromUser(String fieldName, String val) {
if (val == null) {
throw new NullPointerException("Invalid input" + (null == fieldName ? "" : " for field " + fieldName));
}
try {
return Long.parseLong(val);
} catch (NumberFormatException e) {
String msg = "Invalid Number: " + val + (null == fieldName ? "" : " for field " + fieldName);
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, msg);
}
}
/**
* Wrapper for {@link Integer#parseInt(String)} that throws a BAD_REQUEST error if the input is not valid
* @param fieldName used in any exception, may be null
* @param val string to parse, NPE if null
*/
static int parseIntFromUser(String fieldName, String val) {
if (val == null) {
throw new NullPointerException("Invalid input" + (null == fieldName ? "" : " for field " + fieldName));
}
try {
return Integer.parseInt(val);
} catch (NumberFormatException e) {
String msg = "Invalid Number: " + val + (null == fieldName ? "" : " for field " + fieldName);
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, msg);
}
}
/**
* Wrapper for {@link Double#parseDouble(String)} that throws a BAD_REQUEST error if the input is not valid
* @param fieldName used in any exception, may be null
* @param val string to parse, NPE if null
*/
static double parseDoubleFromUser(String fieldName, String val) {
if (val == null) {
throw new NullPointerException("Invalid input" + (null == fieldName ? "" : " for field " + fieldName));
}
try {
return Double.parseDouble(val);
} catch (NumberFormatException e) {
String msg = "Invalid Number: " + val + (null == fieldName ? "" : " for field " + fieldName);
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, msg);
}
}
/**
* Wrapper for {@link Float#parseFloat(String)} that throws a BAD_REQUEST error if the input is not valid
* @param fieldName used in any exception, may be null
* @param val string to parse, NPE if null
*/
static float parseFloatFromUser(String fieldName, String val) {
if (val == null) {
throw new NullPointerException("Invalid input" + (null == fieldName ? "" : " for field " + fieldName));
}
try {
return Float.parseFloat(val);
} catch (NumberFormatException e) {
String msg = "Invalid Number: " + val + (null == fieldName ? "" : " for field " + fieldName);
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, msg);
}
}
public static EnumSet<NumberType> doubleOrFloat = EnumSet.of(NumberType.FLOAT, NumberType.DOUBLE);
/**
* For doubles and floats, unbounded range queries (which do not match NaN values) are not equivalent to existence queries (which do match NaN values).
*
* The two types of queries are equivalent for all other numeric types.
*
* @param field the schema field
* @return false for double and float fields, true for all others
*/
@Override
protected boolean treatUnboundedRangeAsExistence(SchemaField field) {
return !doubleOrFloat.contains(getNumberType());
}
/**
* Override the default existence behavior, so that the non-docValued/norms implementation matches NaN values for double and float fields.
* The [* TO *] query for those fields does not match 'NaN' values, so they must be matched separately.
* <p>
* For doubles and floats the query behavior is equivalent to (field:[* TO *] OR field:NaN).
* For all other numeric types, the default existence query behavior is used.
*/
@Override
public Query getSpecializedExistenceQuery(QParser parser, SchemaField field) {
if (doubleOrFloat.contains(getNumberType())) {
return new ConstantScoreQuery(new BooleanQuery.Builder()
.add(getSpecializedRangeQuery(parser, field, null, null, true, true), BooleanClause.Occur.SHOULD)
.add(getFieldQuery(parser, field, Float.toString(Float.NaN)), BooleanClause.Occur.SHOULD)
.setMinimumNumberShouldMatch(1).build());
} else {
return super.getSpecializedExistenceQuery(parser, field);
}
}
}