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