diff --git a/changelog/unreleased/SOLR-13309-doubleRangeField.yml b/changelog/unreleased/SOLR-13309-doubleRangeField.yml
new file mode 100644
index 00000000000..2c4bb30d0f6
--- /dev/null
+++ b/changelog/unreleased/SOLR-13309-doubleRangeField.yml
@@ -0,0 +1,7 @@
+title: Introduce new `DoubleRangeField` field type for storing and querying double-based ranges
+type: added
+authors:
+ - name: Jason Gerlowski
+links:
+ - name: SOLR-13309
+ url: https://issues.apache.org/jira/browse/SOLR-13309
diff --git a/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java b/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java
new file mode 100644
index 00000000000..5f0d17da4bd
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java
@@ -0,0 +1,304 @@
+/*
+ * 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.numericrange;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.lucene.document.DoubleRange;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.Query;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.QParser;
+
+/**
+ * Field type for double ranges with support for 1-4 dimensions.
+ *
+ *
This field type wraps Lucene's {@link DoubleRange} to provide storage and querying of
+ * double-precision floating-point range values. Ranges can be 1-dimensional (simple ranges),
+ * 2-dimensional (bounding boxes), 3-dimensional (bounding cubes), or 4-dimensional (tesseracts).
+ *
+ *
Value Format
+ *
+ * Values are specified using bracket notation with a TO keyword separator:
+ *
+ *
+ * - 1D: {@code [1.5 TO 2.5]}
+ *
- 2D: {@code [1.0,2.0 TO 3.0,4.0]}
+ *
- 3D: {@code [1.0,2.0,3.0 TO 4.0,5.0,6.0]}
+ *
- 4D: {@code [1.0,2.0,3.0,4.0 TO 5.0,6.0,7.0,8.0]}
+ *
+ *
+ * As the name suggests minimum values (those on the left) must always be less than or equal to the
+ * maximum value for the corresponding dimension. Integer values (e.g. {@code [10 TO 20]}) are also
+ * accepted and parsed as doubles.
+ *
+ * Schema Configuration
+ *
+ *
+ * <fieldType name="doublerange" class="org.apache.solr.schema.numericrange.DoubleRangeField" numDimensions="1"/>
+ * <fieldType name="doublerange2d" class="org.apache.solr.schema.numericrange.DoubleRangeField" numDimensions="2"/>
+ * <field name="price_range" type="doublerange" indexed="true" stored="true"/>
+ * <field name="my_2d_range" type="doublerange2d" indexed="true" stored="true"/>
+ *
+ *
+ * Querying
+ *
+ * Use the {@code numericRange} query parser for range queries with support for different query
+ * types:
+ *
+ *
+ * - Intersects: {@code {!numericRange criteria="intersects" field=price_range}[1.0 TO 2.0]}
+ *
- Within: {@code {!numericRange criteria="within" field=price_range}[0.0 TO 3.0]}
+ *
- Contains: {@code {!numericRange criteria="contains" field=price_range}[1.5 TO 1.75]}
+ *
- Crosses: {@code {!numericRange criteria="crosses" field=price_range}[1.5 TO 2.5]}
+ *
+ *
+ * Limitations
+ *
+ * The main limitation of this field type is that it doesn't support docValues or uninversion, and
+ * therefore can't be used for sorting, faceting, etc.
+ *
+ * @see DoubleRange
+ * @see org.apache.solr.search.numericrange.NumericRangeQParserPlugin
+ */
+public class DoubleRangeField extends AbstractNumericRangeField {
+
+ @Override
+ protected Pattern getRangePattern() {
+ return FP_RANGE_PATTERN_REGEX;
+ }
+
+ @Override
+ protected Pattern getSingleBoundPattern() {
+ return FP_SINGLE_BOUND_PATTERN;
+ }
+
+ @Override
+ public IndexableField createField(SchemaField field, Object value) {
+ if (!field.indexed() && !field.stored()) {
+ return null;
+ }
+
+ String valueStr = value.toString();
+ RangeValue rangeValue = parseRangeValue(valueStr);
+
+ return new DoubleRange(field.getName(), rangeValue.mins, rangeValue.maxs);
+ }
+
+ /**
+ * Parse a range value string into a RangeValue object.
+ *
+ * @param value the string value in format "[min1,min2,... TO max1,max2,...]"
+ * @return parsed RangeValue
+ * @throws SolrException if value format is invalid
+ */
+ @Override
+ public RangeValue parseRangeValue(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, "Range value cannot be null or empty");
+ }
+
+ Matcher matcher = FP_RANGE_PATTERN_REGEX.matcher(value.trim());
+ if (!matcher.matches()) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid range format. Expected: [min1,min2,... TO max1,max2,...] where min and max values are doubles, but got: "
+ + value);
+ }
+
+ String minPart = matcher.group(1).trim();
+ String maxPart = matcher.group(2).trim();
+
+ double[] mins = parseDoubleArray(minPart, "min values");
+ double[] maxs = parseDoubleArray(maxPart, "max values");
+
+ if (mins.length != maxs.length) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Min and max dimensions must match. Min dimensions: "
+ + mins.length
+ + ", max dimensions: "
+ + maxs.length);
+ }
+
+ if (mins.length != numDimensions) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Range dimensions ("
+ + mins.length
+ + ") do not match field type numDimensions ("
+ + numDimensions
+ + ")");
+ }
+
+ // Validate that min <= max for each dimension
+ for (int i = 0; i < mins.length; i++) {
+ if (mins[i] > maxs[i]) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Min value must be <= max value for dimension "
+ + i
+ + ". Min: "
+ + mins[i]
+ + ", Max: "
+ + maxs[i]);
+ }
+ }
+
+ return new RangeValue(mins, maxs);
+ }
+
+ @Override
+ public NumericRangeValue parseSingleBound(String value) {
+ final var singleBoundTyped = parseDoubleArray(value, "single bound values");
+ return new RangeValue(singleBoundTyped, singleBoundTyped);
+ }
+
+ /**
+ * Parse a comma-separated string of doubles into an array.
+ *
+ * @param str the string to parse
+ * @param description description for error messages
+ * @return array of parsed doubles
+ */
+ private double[] parseDoubleArray(String str, String description) {
+ String[] parts = str.split(",");
+ double[] result = new double[parts.length];
+
+ for (int i = 0; i < parts.length; i++) {
+ try {
+ result[i] = Double.parseDouble(parts[i].trim());
+ } catch (NumberFormatException e) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid double in " + description + ": '" + parts[i].trim() + "'",
+ e);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public Query newContainsQuery(String fieldName, NumericRangeValue rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return DoubleRange.newContainsQuery(fieldName, rv.mins, rv.maxs);
+ }
+
+ @Override
+ public Query newIntersectsQuery(String fieldName, NumericRangeValue rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return DoubleRange.newIntersectsQuery(fieldName, rv.mins, rv.maxs);
+ }
+
+ @Override
+ public Query newWithinQuery(String fieldName, NumericRangeValue rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return DoubleRange.newWithinQuery(fieldName, rv.mins, rv.maxs);
+ }
+
+ @Override
+ public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return DoubleRange.newCrossesQuery(fieldName, rv.mins, rv.maxs);
+ }
+
+ @Override
+ protected Query getSpecializedRangeQuery(
+ QParser parser,
+ SchemaField field,
+ String part1,
+ String part2,
+ boolean minInclusive,
+ boolean maxInclusive) {
+ // For standard range syntax field:[value TO value], default to contains query
+ if (part1 == null || part2 == null) {
+ return super.getSpecializedRangeQuery(
+ parser, field, part1, part2, minInclusive, maxInclusive);
+ }
+
+ // Parse the range bounds as single-dimensional double values
+ double min, max;
+ try {
+ min = Double.parseDouble(part1.trim());
+ max = Double.parseDouble(part2.trim());
+ } catch (NumberFormatException e) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid double values in range query: [" + part1 + " TO " + part2 + "]",
+ e);
+ }
+
+ // For exclusive bounds, step to the next representable double value
+ if (!minInclusive) {
+ min = Math.nextUp(min);
+ }
+ if (!maxInclusive) {
+ max = Math.nextDown(max);
+ }
+
+ // Build arrays for the query based on configured dimensions
+ double[] mins = new double[numDimensions];
+ double[] maxs = new double[numDimensions];
+
+ // For now, only support 1D range syntax with field:[X TO Y]
+ if (numDimensions == 1) {
+ mins[0] = min;
+ maxs[0] = max;
+ return DoubleRange.newContainsQuery(field.getName(), mins, maxs);
+ } else {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Standard range query syntax only supports 1D ranges. "
+ + "Use {!numericRange ...} for multi-dimensional queries.");
+ }
+ }
+
+ /** Simple holder class for parsed double range values. */
+ public static class RangeValue implements AbstractNumericRangeField.NumericRangeValue {
+ public final double[] mins;
+ public final double[] maxs;
+
+ public RangeValue(double[] mins, double[] maxs) {
+ this.mins = mins;
+ this.maxs = maxs;
+ }
+
+ @Override
+ public int getDimensions() {
+ return mins.length;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < mins.length; i++) {
+ if (i > 0) sb.append(",");
+ sb.append(mins[i]);
+ }
+ sb.append(" TO ");
+ for (int i = 0; i < maxs.length; i++) {
+ if (i > 0) sb.append(",");
+ sb.append(maxs[i]);
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java b/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java
index fb8c19b5a44..80a685f45ff 100644
--- a/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java
@@ -54,7 +54,7 @@
* <fieldType name="floatrange" class="org.apache.solr.schema.numericrange.FloatRangeField" numDimensions="1"/>
* <fieldType name="floatrange2d" class="org.apache.solr.schema.numericrange.FloatRangeField" numDimensions="2"/>
* <field name="price_range" type="floatrange" indexed="true" stored="true"/>
- * <field name="bbox" type="floatrange2d" indexed="true" stored="true"/>
+ * <field name="my_2d_range" type="floatrange2d" indexed="true" stored="true"/>
*
*
* Querying
diff --git a/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java b/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
index 709f7ac565a..94e113d805e 100644
--- a/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
@@ -25,6 +25,7 @@
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.numericrange.AbstractNumericRangeField;
import org.apache.solr.schema.numericrange.AbstractNumericRangeField.NumericRangeValue;
+import org.apache.solr.schema.numericrange.DoubleRangeField;
import org.apache.solr.schema.numericrange.FloatRangeField;
import org.apache.solr.schema.numericrange.IntRangeField;
import org.apache.solr.schema.numericrange.LongRangeField;
@@ -36,9 +37,9 @@
/**
* Query parser for numeric range fields with support for different query relationship types.
*
- * This parser enables queries against {@link IntRangeField}, {@link LongRangeField}, and {@link
- * FloatRangeField} fields with explicit control over the query relationship type (intersects,
- * within, contains, crosses).
+ *
This parser enables queries against {@link IntRangeField}, {@link LongRangeField}, {@link
+ * FloatRangeField}, and {@link DoubleRangeField} fields with explicit control over the query
+ * relationship type (intersects, within, contains, crosses).
*
*
Parameters
*
@@ -82,6 +83,7 @@
* @see IntRangeField
* @see LongRangeField
* @see FloatRangeField
+ * @see DoubleRangeField
* @lucene.experimental
*/
public class NumericRangeQParserPlugin extends QParserPlugin {
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml b/solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml
index 42adfe39011..145de4a46c9 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml
@@ -42,6 +42,12 @@
+
+
+
+
+
+
@@ -86,6 +92,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
id
diff --git a/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java b/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java
new file mode 100644
index 00000000000..fa31205d511
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java
@@ -0,0 +1,403 @@
+/*
+ * 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.numericrange;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.lucene.document.DoubleRange;
+import org.apache.lucene.index.IndexableField;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.junit.BeforeClass;
+
+/** Tests for {@link DoubleRangeField} */
+public class DoubleRangeFieldTest extends SolrTestCase {
+
+ @BeforeClass
+ public static void ensureAssumptions() {
+ assumeWorkingMockito();
+ }
+
+ public void test1DRangeParsing() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Valid 1D range with floating-point values
+ DoubleRangeField.RangeValue range = fieldType.parseRangeValue("[1.5 TO 3.14]");
+ assertEquals(1, range.getDimensions());
+ assertEquals(1.5, range.mins[0], 0.0);
+ assertEquals(3.14, range.maxs[0], 0.0);
+
+ // Integer values are accepted and parsed as doubles
+ range = fieldType.parseRangeValue("[10 TO 20]");
+ assertEquals(10.0, range.mins[0], 0.0);
+ assertEquals(20.0, range.maxs[0], 0.0);
+
+ // With extra whitespace
+ range = fieldType.parseRangeValue("[ 1.5 TO 3.14 ]");
+ assertEquals(1.5, range.mins[0], 0.0);
+ assertEquals(3.14, range.maxs[0], 0.0);
+
+ // Negative numbers
+ range = fieldType.parseRangeValue("[-3.5 TO -1.0]");
+ assertEquals(-3.5, range.mins[0], 0.0);
+ assertEquals(-1.0, range.maxs[0], 0.0);
+
+ // Point range (min == max)
+ range = fieldType.parseRangeValue("[5.0 TO 5.0]");
+ assertEquals(5.0, range.mins[0], 0.0);
+ assertEquals(5.0, range.maxs[0], 0.0);
+ }
+
+ public void test2DRangeParsing() {
+ DoubleRangeField fieldType = createFieldType(2);
+
+ // Valid 2D range (bounding box)
+ DoubleRangeField.RangeValue range = fieldType.parseRangeValue("[1.0,2.0 TO 3.0,4.0]");
+ assertEquals(2, range.getDimensions());
+ assertEquals(1.0, range.mins[0], 0.0);
+ assertEquals(2.0, range.mins[1], 0.0);
+ assertEquals(3.0, range.maxs[0], 0.0);
+ assertEquals(4.0, range.maxs[1], 0.0);
+
+ // With extra whitespace
+ range = fieldType.parseRangeValue("[ 1.0 , 2.0 TO 3.0 , 4.0 ]");
+ assertEquals(1.0, range.mins[0], 0.0);
+ assertEquals(2.0, range.mins[1], 0.0);
+ assertEquals(3.0, range.maxs[0], 0.0);
+ assertEquals(4.0, range.maxs[1], 0.0);
+ }
+
+ public void test3DRangeParsing() {
+ DoubleRangeField fieldType = createFieldType(3);
+
+ // Valid 3D range (bounding cube)
+ DoubleRangeField.RangeValue range = fieldType.parseRangeValue("[1.0,2.0,3.0 TO 4.0,5.0,6.0]");
+ assertEquals(3, range.getDimensions());
+ assertEquals(1.0, range.mins[0], 0.0);
+ assertEquals(2.0, range.mins[1], 0.0);
+ assertEquals(3.0, range.mins[2], 0.0);
+ assertEquals(4.0, range.maxs[0], 0.0);
+ assertEquals(5.0, range.maxs[1], 0.0);
+ assertEquals(6.0, range.maxs[2], 0.0);
+ }
+
+ public void test4DRangeParsing() {
+ DoubleRangeField fieldType = createFieldType(4);
+
+ // Valid 4D range (tesseract)
+ DoubleRangeField.RangeValue range =
+ fieldType.parseRangeValue("[1.0,2.0,3.0,4.0 TO 5.0,6.0,7.0,8.0]");
+ assertEquals(4, range.getDimensions());
+ assertEquals(1.0, range.mins[0], 0.0);
+ assertEquals(2.0, range.mins[1], 0.0);
+ assertEquals(3.0, range.mins[2], 0.0);
+ assertEquals(4.0, range.mins[3], 0.0);
+ assertEquals(5.0, range.maxs[0], 0.0);
+ assertEquals(6.0, range.maxs[1], 0.0);
+ assertEquals(7.0, range.maxs[2], 0.0);
+ assertEquals(8.0, range.maxs[3], 0.0);
+ }
+
+ public void testInvalidRangeFormat() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Missing brackets
+ SolrException e1 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("1.5 TO 3.14"));
+ assertThat(e1.getMessage(), containsString("Invalid range format"));
+ assertThat(e1.getMessage(), containsString("Expected: [min1,min2,... TO max1,max2,...]"));
+
+ // Missing TO keyword
+ SolrException e2 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[1.5 3.14]"));
+ assertThat(e2.getMessage(), containsString("Invalid range format"));
+
+ // Empty value
+ SolrException e3 = expectThrows(SolrException.class, () -> fieldType.parseRangeValue(""));
+ assertThat(e3.getMessage(), containsString("Range value cannot be null or empty"));
+
+ // Null value
+ SolrException e4 = expectThrows(SolrException.class, () -> fieldType.parseRangeValue(null));
+ assertThat(e4.getMessage(), containsString("Range value cannot be null or empty"));
+ }
+
+ public void testInvalidNumbers() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Non-numeric values
+ SolrException e1 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[abc TO def]"));
+ assertThat(e1.getMessage(), containsString("Invalid range"));
+ assertThat(e1.getMessage(), containsString("where min and max values are doubles"));
+
+ // Partially numeric
+ SolrException e2 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[1.5 TO xyz]"));
+ assertThat(e2.getMessage(), containsString("Invalid range"));
+ assertThat(e2.getMessage(), containsString("where min and max values are doubles"));
+ }
+
+ public void testDimensionMismatch() {
+ DoubleRangeField fieldType1D = createFieldType(1);
+ DoubleRangeField fieldType2D = createFieldType(2);
+
+ // 2D value on 1D field
+ SolrException e1 =
+ expectThrows(
+ SolrException.class, () -> fieldType1D.parseRangeValue("[1.0,2.0 TO 3.0,4.0]"));
+ assertThat(e1.getMessage(), containsString("Range dimensions"));
+ assertThat(e1.getMessage(), containsString("do not match field type numDimensions"));
+
+ // 1D value on 2D field
+ SolrException e2 =
+ expectThrows(SolrException.class, () -> fieldType2D.parseRangeValue("[1.0 TO 2.0]"));
+ assertThat(e2.getMessage(), containsString("Range dimensions"));
+ assertThat(e2.getMessage(), containsString("do not match field type numDimensions"));
+
+ // Min/max dimension mismatch
+ SolrException e3 =
+ expectThrows(
+ SolrException.class,
+ () -> fieldType2D.parseRangeValue("[1.0,2.0 TO 3.0]")); // 2D mins, 1D maxs
+ assertThat(e3.getMessage(), containsString("Min and max dimensions must match"));
+ }
+
+ public void testMinGreaterThanMax() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Min > max should fail
+ SolrException e1 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[3.14 TO 1.5]"));
+ assertThat(e1.getMessage(), containsString("Min value must be <= max value"));
+ assertThat(e1.getMessage(), containsString("dimension 0"));
+
+ // For 2D
+ DoubleRangeField fieldType2D = createFieldType(2);
+ SolrException e2 =
+ expectThrows(
+ SolrException.class,
+ () -> fieldType2D.parseRangeValue("[3.0,2.0 TO 1.0,4.0]")); // First dimension invalid
+ assertThat(e2.getMessage(), containsString("Min value must be <= max value"));
+ assertThat(e2.getMessage(), containsString("dimension 0"));
+ }
+
+ public void testFieldCreation1D() {
+ DoubleRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "double_range");
+
+ IndexableField field = fieldType.createField(schemaField, "[1.0 TO 2.0]");
+ assertNotNull(field);
+ assertTrue(field instanceof DoubleRange);
+ assertEquals("double_range", field.name());
+ }
+
+ public void testFieldCreation2D() {
+ DoubleRangeField fieldType = createFieldType(2);
+ SchemaField schemaField = createSchemaField(fieldType, "double_range_2d");
+
+ IndexableField field = fieldType.createField(schemaField, "[0.0,0.0 TO 10.0,10.0]");
+ assertNotNull(field);
+ assertTrue(field instanceof DoubleRange);
+ assertEquals("double_range_2d", field.name());
+ }
+
+ public void testStoredField() {
+ DoubleRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "double_range");
+
+ String value = "[1.0 TO 2.0]";
+ IndexableField storedField = fieldType.getStoredField(schemaField, value);
+ assertNotNull(storedField);
+ assertEquals("double_range", storedField.name());
+ assertEquals(value, storedField.stringValue());
+ }
+
+ public void testToInternal() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Valid value should pass through after validation
+ String value = "[1.5 TO 3.14]";
+ String internal = fieldType.toInternal(value);
+ assertEquals(value, internal);
+
+ // Invalid value should throw exception
+ SolrException e = expectThrows(SolrException.class, () -> fieldType.toInternal("invalid"));
+ assertThat(e.getMessage(), containsString("Invalid range format"));
+ }
+
+ public void testToNativeType() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // String input
+ Object nativeType = fieldType.toNativeType("[1.5 TO 3.14]");
+ assertTrue(nativeType instanceof DoubleRangeField.RangeValue);
+ DoubleRangeField.RangeValue range = (DoubleRangeField.RangeValue) nativeType;
+ assertEquals(1.5, range.mins[0], 0.0);
+ assertEquals(3.14, range.maxs[0], 0.0);
+
+ // RangeValue input (should pass through)
+ DoubleRangeField.RangeValue inputRange =
+ new DoubleRangeField.RangeValue(new double[] {5.0}, new double[] {15.0});
+ Object result = fieldType.toNativeType(inputRange);
+ assertSame(inputRange, result);
+
+ // Null input
+ assertNull(fieldType.toNativeType(null));
+ }
+
+ public void testSortFieldThrowsException() {
+ DoubleRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "double_range");
+
+ // Sorting should not be supported
+ SolrException e =
+ expectThrows(SolrException.class, () -> fieldType.getSortField(schemaField, true));
+ assertThat(e.getMessage(), containsString("Cannot sort on DoubleRangeField"));
+ assertThat(e.getMessage(), containsString("double_range"));
+ }
+
+ public void testUninversionType() {
+ DoubleRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "double_range");
+
+ // Should return null (no field cache support)
+ assertNull(fieldType.getUninversionType(schemaField));
+ }
+
+ public void testInvalidNumDimensions() {
+ DoubleRangeField field = new DoubleRangeField();
+ Map args = new HashMap<>();
+ IndexSchema schema = createMockSchema();
+
+ // Test numDimensions = 0
+ args.put("numDimensions", "0");
+ SolrException e1 = expectThrows(SolrException.class, () -> field.init(schema, args));
+ assertThat(e1.getMessage(), containsString("numDimensions must be between 1 and 4"));
+ assertThat(e1.getMessage(), containsString("but was [0]"));
+
+ // Test numDimensions = 5 (too high)
+ args.put("numDimensions", "5");
+ DoubleRangeField field2 = new DoubleRangeField();
+ SolrException e2 = expectThrows(SolrException.class, () -> field2.init(schema, args));
+ assertThat(e2.getMessage(), containsString("numDimensions must be between 1 and 4"));
+ assertThat(e2.getMessage(), containsString("but was [5]"));
+
+ // Test negative numDimensions
+ args.put("numDimensions", "-1");
+ DoubleRangeField field3 = new DoubleRangeField();
+ SolrException e3 = expectThrows(SolrException.class, () -> field3.init(schema, args));
+ assertThat(e3.getMessage(), containsString("numDimensions must be between 1 and 4"));
+ assertThat(e3.getMessage(), containsString("but was [-1]"));
+ }
+
+ public void testRangeValueToString() {
+ DoubleRangeField fieldType = createFieldType(2);
+ DoubleRangeField.RangeValue range = fieldType.parseRangeValue("[1.0,2.0 TO 3.0,4.0]");
+
+ String str = range.toString();
+ assertEquals("[1.0,2.0 TO 3.0,4.0]", str);
+ }
+
+ public void testScientificNotation() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Integer mantissa with positive exponent
+ DoubleRangeField.RangeValue range = fieldType.parseRangeValue("[123e4 TO 567e8]");
+ assertEquals(123e4, range.mins[0], 0.0);
+ assertEquals(567e8, range.maxs[0], 0.0);
+
+ // Decimal mantissa with negative exponent
+ range = fieldType.parseRangeValue("[-1.2e-4 TO 3.4e-2]");
+ assertEquals(-1.2e-4, range.mins[0], 0.0);
+ assertEquals(3.4e-2, range.maxs[0], 0.0);
+
+ // Uppercase E
+ range = fieldType.parseRangeValue("[1.5E3 TO 2.5E3]");
+ assertEquals(1.5e3, range.mins[0], 0.0);
+ assertEquals(2.5e3, range.maxs[0], 0.0);
+
+ // Explicit positive exponent sign
+ range = fieldType.parseRangeValue("[1.0e+2 TO 9.9e+2]");
+ assertEquals(1.0e+2, range.mins[0], 0.0);
+ assertEquals(9.9e+2, range.maxs[0], 0.0);
+
+ // Negative mantissa with negative exponent
+ range = fieldType.parseRangeValue("[-9.9E-9 TO -1.1E-9]");
+ assertEquals(-9.9e-9, range.mins[0], 0.0);
+ assertEquals(-1.1e-9, range.maxs[0], 0.0);
+
+ // Multi-dimensional with scientific notation
+ DoubleRangeField fieldType2D = createFieldType(2);
+ range = fieldType2D.parseRangeValue("[1e2,2.0e1 TO 3e2,4.0e1]");
+ assertEquals(1e2, range.mins[0], 0.0);
+ assertEquals(2.0e1, range.mins[1], 0.0);
+ assertEquals(3e2, range.maxs[0], 0.0);
+ assertEquals(4.0e1, range.maxs[1], 0.0);
+
+ // Single-bound scientific notation via toInternal
+ String val = "[1.2e3 TO 4.5e3]";
+ assertEquals(val, fieldType.toInternal(val));
+ }
+
+ public void testExtremeValues() {
+ DoubleRangeField fieldType = createFieldType(1);
+
+ // Test with very negative and very positive values expressible without scientific notation
+ DoubleRangeField.RangeValue range = fieldType.parseRangeValue("[-9999999.0 TO 9999999.0]");
+ assertEquals(-9999999.0, range.mins[0], 0.0);
+ assertEquals(9999999.0, range.maxs[0], 0.0);
+
+ // Test with small fractional values
+ range = fieldType.parseRangeValue("[0.0001 TO 0.9999]");
+ assertEquals(0.0001, range.mins[0], 0.0);
+ assertEquals(0.9999, range.maxs[0], 0.0);
+
+ // Test with values beyond float range
+ range = fieldType.parseRangeValue("[1.0e100 TO 1.0e200]");
+ assertEquals(1.0e100, range.mins[0], 0.0);
+ assertEquals(1.0e200, range.maxs[0], 0.0);
+ }
+
+ private IndexSchema createMockSchema() {
+ final var schema = mock(IndexSchema.class);
+ when(schema.getVersion()).thenReturn(1.7f);
+ return schema;
+ }
+
+ private DoubleRangeField createFieldType(int numDimensions) {
+ DoubleRangeField field = new DoubleRangeField();
+ Map args = new HashMap<>();
+ args.put("numDimensions", String.valueOf(numDimensions));
+
+ field.init(createMockSchema(), args);
+
+ return field;
+ }
+
+ private SchemaField createSchemaField(DoubleRangeField fieldType, String name) {
+ final var fieldProperties =
+ 0b1 | 0b100; // INDEXED | STORED - constants cannot be accessed directly due to visibility.
+ return new SchemaField(name, fieldType, fieldProperties, null);
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java
new file mode 100644
index 00000000000..e57334779c8
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java
@@ -0,0 +1,422 @@
+/*
+ * 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.numericrange;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests for {@link NumericRangeQParserPlugin} using {@link
+ * org.apache.solr.schema.numericrange.DoubleRangeField} fields.
+ */
+public class NumericRangeQParserPluginDoubleTest extends SolrTestCaseJ4 {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig.xml", "schema-numericrange.xml");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ clearIndex();
+ assertU(commit());
+ }
+
+ @Test
+ public void test1DIntersectsQuery() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
+ assertU(adoc("id", "3", "double_range", "[0.5 TO 0.8]"));
+ assertU(adoc("id", "4", "double_range", "[2.0 TO 3.0]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range}[1.2 TO 1.8]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']",
+ "//result/doc/str[@name='double_range'][.='[1.0 TO 2.0]']",
+ "//result/doc/str[@name='double_range'][.='[1.5 TO 2.5]']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range}[0.0 TO 1.0]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='3']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range}[1.75 TO 2.25]"),
+ "//result[@numFound='3']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']",
+ "//result/doc/str[@name='id'][.='4']");
+ }
+
+ @Test
+ public void test1DWithinQuery() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
+ assertU(adoc("id", "3", "double_range", "[0.5 TO 0.8]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"within\" field=double_range}[0.0 TO 3.0]"),
+ "//result[@numFound='3']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"within\" field=double_range}[1.0 TO 2.0]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"within\" field=double_range}[0.0 TO 1.0]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='3']");
+ }
+
+ @Test
+ public void test1DContainsQuery() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
+ assertU(adoc("id", "3", "double_range", "[0.5 TO 3.0]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"contains\" field=double_range}[1.6 TO 1.7]"),
+ "//result[@numFound='3']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']",
+ "//result/doc/str[@name='id'][.='3']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"contains\" field=double_range}[0.0 TO 4.0]"),
+ "//result[@numFound='0']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"contains\" field=double_range}[1.0 TO 2.0]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='3']");
+ }
+
+ @Test
+ public void test1DCrossesQuery() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
+ assertU(adoc("id", "3", "double_range", "[0.5 TO 0.8]"));
+ assertU(adoc("id", "4", "double_range", "[1.2 TO 1.8]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"crosses\" field=double_range}[1.5 TO 2.5]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='4']");
+ }
+
+ @Test
+ public void test2DIntersectsQuery() {
+ assertU(adoc("id", "1", "double_range_2d", "[0.0,0.0 TO 1.0,1.0]"));
+ assertU(adoc("id", "2", "double_range_2d", "[0.5,0.5 TO 1.5,1.5]"));
+ assertU(adoc("id", "3", "double_range_2d", "[2.0,2.0 TO 3.0,3.0]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range_2d}[0.8,0.8 TO 1.2,1.2]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range_2d}[2.5,2.5 TO 3.5,3.5]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='3']");
+
+ assertQ(
+ req(
+ "q",
+ "{!numericRange criteria=intersects field=double_range_2d}[10.0,10.0 TO 20.0,20.0]"),
+ "//result[@numFound='0']");
+ }
+
+ @Test
+ public void test3DQuery() {
+ assertU(adoc("id", "1", "double_range_3d", "[0.0,0.0,0.0 TO 1.0,1.0,1.0]"));
+ assertU(adoc("id", "2", "double_range_3d", "[0.5,0.5,0.5 TO 1.5,1.5,1.5]"));
+ assertU(commit());
+
+ assertQ(
+ req(
+ "q",
+ "{!numericRange criteria=intersects field=double_range_3d}[0.8,0.8,0.8 TO 1.2,1.2,1.2]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void test4DQuery() {
+ assertU(adoc("id", "1", "double_range_4d", "[0.0,0.0,0.0,0.0 TO 1.0,1.0,1.0,1.0]"));
+ assertU(adoc("id", "2", "double_range_4d", "[0.5,0.5,0.5,0.5 TO 1.5,1.5,1.5,1.5]"));
+ assertU(commit());
+
+ assertQ(
+ req(
+ "q",
+ "{!numericRange criteria=intersects field=double_range_4d}[0.8,0.8,0.8,0.8 TO 1.2,1.2,1.2,1.2]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testMultiValuedField() {
+ assertU(
+ adoc(
+ "id", "1", "double_range_multi", "[1.0 TO 2.0]", "double_range_multi", "[3.0 TO 4.0]"));
+ assertU(adoc("id", "2", "double_range_multi", "[1.5 TO 2.5]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range_multi}[1.1 TO 1.2]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/arr[@name='double_range_multi']/str[1][.='[1.0 TO 2.0]']",
+ "//result/doc/arr[@name='double_range_multi']/str[2][.='[3.0 TO 4.0]']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range_multi}[3.1 TO 3.2]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range_multi}[1.5 TO 2.5]"),
+ "//result[@numFound='2']");
+ }
+
+ @Test
+ public void testNegativeValues() {
+ assertU(adoc("id", "1", "double_range", "[-1.0 TO -0.5]"));
+ assertU(adoc("id", "2", "double_range", "[-0.75 TO -0.25]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range}[-0.8 TO -0.6]"),
+ "//result[@numFound='2']");
+ }
+
+ @Test
+ public void testPointRange() {
+ assertU(adoc("id", "1", "double_range", "[1.5 TO 1.5]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range}[1.5 TO 1.5]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=double_range}[0.5 TO 1.75]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+ }
+
+ @Test
+ public void testMissingFieldParameter() {
+ assertQEx(
+ "Missing field parameter should fail",
+ "Missing required parameter: field",
+ req("q", "{!numericRange criteria=intersects}[1.0 TO 2.0]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testMissingCriteriaParameter() {
+ assertQEx(
+ "Missing criteria parameter should fail",
+ "Missing required parameter: criteria",
+ req("q", "{!numericRange field=double_range}[1.0 TO 2.0]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testInvalidFieldType() {
+ // Query on a plain string field should fail
+ assertQEx(
+ "Query on wrong field type should fail",
+ "must be a numeric range field type",
+ req("q", "{!numericRange criteria=intersects field=title}[1.0 TO 2.0]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testInvalidQueryType() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(commit());
+
+ assertQEx(
+ "Invalid query criteria should fail",
+ "Unknown query criteria",
+ req("q", "{!numericRange criteria=\"invalid\" field=double_range}[1.0 TO 2.0]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testInvalidRangeValue() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(commit());
+
+ assertQEx(
+ "Invalid range format should fail",
+ "Invalid range",
+ req("q", "{!numericRange criteria=intersects field=double_range}invalid"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testEmptyRangeValue() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(commit());
+
+ assertQEx(
+ "Empty range value should fail",
+ req("q", "{!numericRange criteria=intersects field=double_range}"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ // ------------------------------------------
+ // Tests for getFieldQuery and getSpecializedRangeQuery via the standard query parser.
+ // These default to "contains" semantics.
+
+ @Test
+ public void testGetFieldQueryFullRange() {
+ // doc 1: narrow range, fully inside the query range → should NOT match (doc contains query)
+ // doc 2: wide range that fully contains the query range → should match
+ // doc 3: range that only partially overlaps → should NOT match
+ assertU(adoc("id", "1", "double_range", "[1.3 TO 1.6]")); // No match
+ assertU(adoc("id", "2", "double_range", "[1.0 TO 2.0]")); // Match!
+ assertU(adoc("id", "3", "double_range", "[1.5 TO 2.5]")); // No match
+ assertU(commit());
+
+ // Contains semantics: find indexed ranges that fully contain [1.2 TO 1.8]
+ assertQ(
+ req("q", "double_range:[1.2 TO 1.8]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQueryFullRangeMultipleMatches() {
+ assertU(adoc("id", "1", "double_range", "[0.0 TO 10.0]")); // Match!
+ assertU(adoc("id", "2", "double_range", "[1.0 TO 2.0]")); // Match!
+ assertU(adoc("id", "3", "double_range", "[1.0 TO 1.99]")); // No match - max too low
+ assertU(adoc("id", "4", "double_range", "[1.01 TO 2.0]")); // No match - min too high
+ assertU(commit());
+
+ assertQ(
+ req("q", "double_range:[1.0 TO 2.0]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQuerySingleBound() {
+ // Single-bound syntax: double_range:1.5 is sugar for contains([1.5 TO 1.5])
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]")); // Match!
+ assertU(adoc("id", "2", "double_range", "[1.5 TO 1.5]")); // Match!
+ assertU(adoc("id", "3", "double_range", "[1.0 TO 1.49]")); // No match - max below 1.5
+ assertU(adoc("id", "4", "double_range", "[1.51 TO 3.0]")); // No match - min above 1.5
+ assertU(commit());
+
+ assertQ(
+ req("q", "double_range:1.5"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQuerySingleBound2D() {
+ // 2D single-bound: double_range_2d:0.5,0.5 is sugar for contains([0.5,0.5 TO 0.5,0.5])
+ assertU(adoc("id", "1", "double_range_2d", "[0.0,0.0 TO 1.0,1.0]")); // Match!
+ assertU(adoc("id", "2", "double_range_2d", "[0.5,0.5 TO 0.5,0.5]")); // Match!
+ assertU(adoc("id", "3", "double_range_2d", "[0.0,0.0 TO 0.4,1.0]")); // No match - X too low
+ assertU(adoc("id", "4", "double_range_2d", "[0.6,0.0 TO 1.0,1.0]")); // No match - X too high
+ assertU(commit());
+
+ assertQ(
+ req("q", "double_range_2d:0.5,0.5"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQueryFieldFormatting() {
+ assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+ assertU(adoc("id", "2", "double_range_2d", "[1.0,2.0 TO 3.0,4.0]"));
+ assertU(adoc("id", "3", "double_range_3d", "[0.5,1.0,1.5 TO 2.5,3.0,3.5]"));
+ assertU(adoc("id", "4", "double_range_4d", "[0.1,0.2,0.3,0.4 TO 1.1,1.2,1.3,1.4]"));
+ assertU(
+ adoc(
+ "id",
+ "5",
+ "double_range_multi",
+ "[0.5 TO 1.0]",
+ "double_range_multi",
+ "[2.0 TO 3.0]",
+ "double_range_multi",
+ "[4.0 TO 5.0]"));
+ assertU(commit());
+
+ // Verify 1D field returns correctly formatted value
+ assertQ(
+ req("q", "id:1"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='double_range'][.='[1.0 TO 2.0]']");
+
+ // Verify 2D field returns correctly formatted value
+ assertQ(
+ req("q", "id:2"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='double_range_2d'][.='[1.0,2.0 TO 3.0,4.0]']");
+
+ // Verify 3D field returns correctly formatted value
+ assertQ(
+ req("q", "id:3"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='double_range_3d'][.='[0.5,1.0,1.5 TO 2.5,3.0,3.5]']");
+
+ // Verify 4D field returns correctly formatted value
+ assertQ(
+ req("q", "id:4"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='double_range_4d'][.='[0.1,0.2,0.3,0.4 TO 1.1,1.2,1.3,1.4]']");
+
+ // Verify multi-valued field returns all values correctly formatted
+ assertQ(
+ req("q", "id:5"),
+ "//result[@numFound='1']",
+ "//result/doc/arr[@name='double_range_multi']/str[1][.='[0.5 TO 1.0]']",
+ "//result/doc/arr[@name='double_range_multi']/str[2][.='[2.0 TO 3.0]']",
+ "//result/doc/arr[@name='double_range_multi']/str[3][.='[4.0 TO 5.0]']");
+ }
+}
diff --git a/solr/solr-ref-guide/modules/indexing-guide/pages/field-types-included-with-solr.adoc b/solr/solr-ref-guide/modules/indexing-guide/pages/field-types-included-with-solr.adoc
index d3cda92cf71..c7124e6c816 100644
--- a/solr/solr-ref-guide/modules/indexing-guide/pages/field-types-included-with-solr.adoc
+++ b/solr/solr-ref-guide/modules/indexing-guide/pages/field-types-included-with-solr.adoc
@@ -59,6 +59,8 @@ The {solr-javadocs}/core/org/apache/solr/schema/package-summary.html[`org.apache
|LongRangeField |Stores single or multi-dimensional ranges of long integers, using syntax like `[1000000000 TO 4000000000]` or `[1,2 TO 3,4]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type is defined in the `org.apache.solr.schema.numericrange` package; fieldType definitions typically reference this as: ``. Field type does not support docValues. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
+|DoubleRangeField |Stores single or multi-dimensional ranges of double-precision floating-point numbers, using syntax like `[1.5 TO 2.5]` or `[1.0,2.0 TO 3.0,4.0]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type is defined in the `org.apache.solr.schema.numericrange` package; fieldType definitions typically reference this as: ``. Field type does not support docValues. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
+
|FloatRangeField |Stores single or multi-dimensional ranges of floating-point numbers, using syntax like `[1.5 TO 4.5]` or `[1.0,2.0 TO 3.0,4.0]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type is defined in the `org.apache.solr.schema.numericrange` package; fieldType definitions typically reference this as: ``. Field type does not support docValues. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
|NestPathField | Specialized field type storing enhanced information, when xref:indexing-nested-documents.adoc#schema-configuration[working with nested documents].
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
index 04fa0125dfd..e584e7c21f4 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
@@ -1006,7 +1006,7 @@ For more information about the possibilities of nested queries, see Yonik Seeley
NOTE: Syntax specifics of the `{!numericRange}` query parser are considered experimental and may change in the future.
-Allows users to search range fields (e.g. `IntRangeField`, `LongRangeField`, `FloatRangeField`) using a specified query-range.
+Allows users to search range fields (e.g. `IntRangeField`, `LongRangeField`, `FloatRangeField`, `DoubleRangeField`) using a specified query-range.
Multiple match semantics supported, see the `criteria` parameter below for more details.
=== Numeric Range Parameters
@@ -1019,7 +1019,7 @@ Multiple match semantics supported, see the `criteria` parameter below for more
|===
+
The field name to operate on.
-Must be a numeric range field type (e.g. `IntRangeField`, `LongRangeField`, `FloatRangeField`)
+Must be a numeric range field type (e.g. `IntRangeField`, `LongRangeField`, `FloatRangeField`, `DoubleRangeField`)
`criteria`::
+
[%autowidth,frame=none]