| /* |
| * 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); |
| } |
| } |
| } |