This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git

commit df88299aa84e2fbe0dfd21cdf96b768c3c4a1eaa
Author: Jason Gerlowski <[email protected]>
AuthorDate: Tue Mar 24 12:50:45 2026 -0400

    SOLR-13309: Introduce FloatRangeField to expose Lucene 'FloatRange' (#4229)
    
    Mirrors recently added 'IntRangeField' and 'LongRangeField' types.
    
    (cherry picked from commit 64be53056aa382594c03dc63d3b83d7586b8dada)
---
 .../unreleased/SOLR-13309-floatRangeField.yml      |   7 +
 .../numericrange/AbstractNumericRangeField.java    |  51 ++-
 .../solr/schema/numericrange/FloatRangeField.java  | 304 +++++++++++++++
 .../numericrange/NumericRangeQParserPlugin.java    |  16 +-
 .../solr/collection1/conf/schema-numericrange.xml  |  19 +
 .../schema/numericrange/FloatRangeFieldTest.java   | 398 +++++++++++++++++++
 .../NumericRangeQParserPluginFloatTest.java        | 421 +++++++++++++++++++++
 .../pages/field-types-included-with-solr.adoc      |   2 +
 .../modules/query-guide/pages/other-parsers.adoc   |   4 +-
 9 files changed, 1213 insertions(+), 9 deletions(-)

diff --git a/changelog/unreleased/SOLR-13309-floatRangeField.yml 
b/changelog/unreleased/SOLR-13309-floatRangeField.yml
new file mode 100644
index 00000000000..f447e5f3902
--- /dev/null
+++ b/changelog/unreleased/SOLR-13309-floatRangeField.yml
@@ -0,0 +1,7 @@
+title: Introduce new `FloatRangeField` field type for storing and querying 
float-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/AbstractNumericRangeField.java
 
b/solr/core/src/java/org/apache/solr/schema/numericrange/AbstractNumericRangeField.java
index 8d61af15df8..a05a766ac52 100644
--- 
a/solr/core/src/java/org/apache/solr/schema/numericrange/AbstractNumericRangeField.java
+++ 
b/solr/core/src/java/org/apache/solr/schema/numericrange/AbstractNumericRangeField.java
@@ -53,6 +53,7 @@ import org.apache.solr.uninverting.UninvertingReader.Type;
  *
  * @see IntRangeField
  * @see LongRangeField
+ * @see FloatRangeField
  */
 public abstract class AbstractNumericRangeField extends PrimitiveFieldType {
 
@@ -82,9 +83,55 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
   protected static final Pattern SINGLE_BOUND_PATTERN =
       Pattern.compile("^" + COMMA_DELIMITED_NUMS + "$");
 
+  /**
+   * Regex fragment matching a comma-separated list of signed floating-point 
numbers (integers,
+   * floating-point literals, or values in scientific notation such as {@code 
1.2e3} or {@code
+   * -4.5E-6}).
+   */
+  protected static final String COMMA_DELIMITED_FP_NUMS =
+      
"-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?(?:\\s*,\\s*-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)*";
+
+  private static final String FP_RANGE_PATTERN_STR =
+      "\\[\\s*(" + COMMA_DELIMITED_FP_NUMS + ")\\s+TO\\s+(" + 
COMMA_DELIMITED_FP_NUMS + ")\\s*\\]";
+
+  /**
+   * Pre-compiled pattern matching {@code [min1,min2,... TO max1,max2,...]} 
range syntax where
+   * values may be floating-point numbers.
+   */
+  protected static final Pattern FP_RANGE_PATTERN_REGEX = 
Pattern.compile(FP_RANGE_PATTERN_STR);
+
+  /**
+   * Pre-compiled pattern matching a single (multi-dimensional) floating-point 
bound, e.g. {@code
+   * 1.5,2.0,3.14}.
+   */
+  protected static final Pattern FP_SINGLE_BOUND_PATTERN =
+      Pattern.compile("^" + COMMA_DELIMITED_FP_NUMS + "$");
+
   /** Configured number of dimensions for this field type; defaults to 1. */
   protected int numDimensions = 1;
 
+  /**
+   * Returns the regex {@link Pattern} used to match a full range value string 
of the form {@code
+   * [min TO max]}. Subclasses may override to use an alternative pattern 
(e.g. one that accepts
+   * floating-point numbers).
+   *
+   * @return the range pattern for this field type
+   */
+  protected Pattern getRangePattern() {
+    return RANGE_PATTERN_REGEX;
+  }
+
+  /**
+   * Returns the regex {@link Pattern} used to match a single 
multi-dimensional bound (e.g. {@code
+   * 1,2,3}). Subclasses may override to use an alternative pattern (e.g. one 
that accepts
+   * floating-point numbers).
+   *
+   * @return the single-bound pattern for this field type
+   */
+  protected Pattern getSingleBoundPattern() {
+    return SINGLE_BOUND_PATTERN;
+  }
+
   @Override
   protected boolean enableDocValuesByDefault() {
     return false; // Range fields do not support docValues
@@ -287,13 +334,13 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
     String trimmed = externalVal.trim();
 
     // Check if it's the full range syntax: [min1,min2 TO max1,max2]
-    if (RANGE_PATTERN_REGEX.matcher(trimmed).matches()) {
+    if (getRangePattern().matcher(trimmed).matches()) {
       final var rangeValue = parseRangeValue(trimmed);
       return newContainsQuery(field.getName(), rangeValue);
     }
 
     // Syntax sugar: also accept a single-bound (i.e pX,pY,pZ)
-    if (SINGLE_BOUND_PATTERN.matcher(trimmed).matches()) {
+    if (getSingleBoundPattern().matcher(trimmed).matches()) {
       final var singleBoundRange = parseSingleBound(trimmed);
 
       if (singleBoundRange.getDimensions() != numDimensions) {
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
new file mode 100644
index 00000000000..fb8c19b5a44
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.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.FloatRange;
+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 float ranges with support for 1-4 dimensions.
+ *
+ * <p>This field type wraps Lucene's {@link FloatRange} to provide storage and 
querying of float
+ * range values. Ranges can be 1-dimensional (simple ranges), 2-dimensional 
(bounding boxes),
+ * 3-dimensional (bounding cubes), or 4-dimensional (tesseracts).
+ *
+ * <h2>Value Format</h2>
+ *
+ * Values are specified using bracket notation with a TO keyword separator:
+ *
+ * <ul>
+ *   <li>1D: {@code [1.5 TO 2.5]}
+ *   <li>2D: {@code [1.0,2.0 TO 3.0,4.0]}
+ *   <li>3D: {@code [1.0,2.0,3.0 TO 4.0,5.0,6.0]}
+ *   <li>4D: {@code [1.0,2.0,3.0,4.0 TO 5.0,6.0,7.0,8.0]}
+ * </ul>
+ *
+ * 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 floats.
+ *
+ * <h2>Schema Configuration</h2>
+ *
+ * <pre>
+ * &lt;fieldType name="floatrange" 
class="org.apache.solr.schema.numericrange.FloatRangeField" 
numDimensions="1"/&gt;
+ * &lt;fieldType name="floatrange2d" 
class="org.apache.solr.schema.numericrange.FloatRangeField" 
numDimensions="2"/&gt;
+ * &lt;field name="price_range" type="floatrange" indexed="true" 
stored="true"/&gt;
+ * &lt;field name="bbox" type="floatrange2d" indexed="true" stored="true"/&gt;
+ * </pre>
+ *
+ * <h2>Querying</h2>
+ *
+ * Use the {@code numericRange} query parser for range queries with support 
for different query
+ * types:
+ *
+ * <ul>
+ *   <li>Intersects: {@code {!numericRange criteria="intersects" 
field=price_range}[1.0 TO 2.0]}
+ *   <li>Within: {@code {!numericRange criteria="within" 
field=price_range}[0.0 TO 3.0]}
+ *   <li>Contains: {@code {!numericRange criteria="contains" 
field=price_range}[1.5 TO 1.75]}
+ *   <li>Crosses: {@code {!numericRange criteria="crosses" 
field=price_range}[1.5 TO 2.5]}
+ * </ul>
+ *
+ * <h2>Limitations</h2>
+ *
+ * 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 FloatRange
+ * @see org.apache.solr.search.numericrange.NumericRangeQParserPlugin
+ */
+public class FloatRangeField 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 FloatRange(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 floats, but got: "
+              + value);
+    }
+
+    String minPart = matcher.group(1).trim();
+    String maxPart = matcher.group(2).trim();
+
+    float[] mins = parseFloatArray(minPart, "min values");
+    float[] maxs = parseFloatArray(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 = parseFloatArray(value, "single bound values");
+    return new RangeValue(singleBoundTyped, singleBoundTyped);
+  }
+
+  /**
+   * Parse a comma-separated string of floats into an array.
+   *
+   * @param str the string to parse
+   * @param description description for error messages
+   * @return array of parsed floats
+   */
+  private float[] parseFloatArray(String str, String description) {
+    String[] parts = str.split(",");
+    float[] result = new float[parts.length];
+
+    for (int i = 0; i < parts.length; i++) {
+      try {
+        result[i] = Float.parseFloat(parts[i].trim());
+      } catch (NumberFormatException e) {
+        throw new SolrException(
+            ErrorCode.BAD_REQUEST,
+            "Invalid float in " + description + ": '" + parts[i].trim() + "'",
+            e);
+      }
+    }
+
+    return result;
+  }
+
+  @Override
+  public Query newContainsQuery(String fieldName, NumericRangeValue 
rangeValue) {
+    final var rv = (RangeValue) rangeValue;
+    return FloatRange.newContainsQuery(fieldName, rv.mins, rv.maxs);
+  }
+
+  @Override
+  public Query newIntersectsQuery(String fieldName, NumericRangeValue 
rangeValue) {
+    final var rv = (RangeValue) rangeValue;
+    return FloatRange.newIntersectsQuery(fieldName, rv.mins, rv.maxs);
+  }
+
+  @Override
+  public Query newWithinQuery(String fieldName, NumericRangeValue rangeValue) {
+    final var rv = (RangeValue) rangeValue;
+    return FloatRange.newWithinQuery(fieldName, rv.mins, rv.maxs);
+  }
+
+  @Override
+  public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue) 
{
+    final var rv = (RangeValue) rangeValue;
+    return FloatRange.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 float values
+    float min, max;
+    try {
+      min = Float.parseFloat(part1.trim());
+      max = Float.parseFloat(part2.trim());
+    } catch (NumberFormatException e) {
+      throw new SolrException(
+          ErrorCode.BAD_REQUEST,
+          "Invalid float values in range query: [" + part1 + " TO " + part2 + 
"]",
+          e);
+    }
+
+    // For exclusive bounds, step to the next representable float value
+    if (!minInclusive) {
+      min = Math.nextUp(min);
+    }
+    if (!maxInclusive) {
+      max = Math.nextDown(max);
+    }
+
+    // Build arrays for the query based on configured dimensions
+    float[] mins = new float[numDimensions];
+    float[] maxs = new float[numDimensions];
+
+    // For now, only support 1D range syntax with field:[X TO Y]
+    if (numDimensions == 1) {
+      mins[0] = min;
+      maxs[0] = max;
+      return FloatRange.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 float range values. */
+  public static class RangeValue implements 
AbstractNumericRangeField.NumericRangeValue {
+    public final float[] mins;
+    public final float[] maxs;
+
+    public RangeValue(float[] mins, float[] 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/search/numericrange/NumericRangeQParserPlugin.java
 
b/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
index 65939903e9b..142a240b0b8 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.request.SolrQueryRequest;
 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.FloatRangeField;
 import org.apache.solr.schema.numericrange.IntRangeField;
 import org.apache.solr.schema.numericrange.LongRangeField;
 import org.apache.solr.search.QParser;
@@ -35,8 +36,9 @@ import org.apache.solr.search.SyntaxError;
 /**
  * Query parser for numeric range fields with support for different query 
relationship types.
  *
- * <p>This parser enables queries against {@link IntRangeField} and {@link 
LongRangeField} fields
- * with explicit control over the query relationship type (intersects, within, 
contains, crosses).
+ * <p>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).
  *
  * <h2>Parameters</h2>
  *
@@ -69,13 +71,17 @@ import org.apache.solr.search.SyntaxError;
  * {!numericRange criteria="intersects" field=long_range}[1000000000 TO 
2000000000]
  * {!numericRange criteria="within" field=long_range}[0 TO 9999999999]
  *
- * // Multi-dimensional queries (bounding boxes, cubes, tesseracts)
- * {!numericRange criteria="intersects" field=bbox}[0,0 TO 10,10]
- * {!numericRange criteria="within" field=bbox}[-10,-10 TO 20,20]
+ * // FloatRangeField queries
+ * {!numericRange criteria="intersects" field=float_range}[1.0 TO 2.5]
+ * {!numericRange criteria="within" field=float_range}[0.0 TO 9.99]
+ *
+ * // Multi-dimensional queries (2D, 3D, 4D)
+ * {!numericRange criteria="within" field=bounding_cube}[-10,-10,-4 TO 
20,20,18]
  * </pre>
  *
  * @see IntRangeField
  * @see LongRangeField
+ * @see FloatRangeField
  * @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 4d70167a59e..42adfe39011 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
@@ -36,6 +36,12 @@
   <fieldType name="longrange3d" class="solr.numericrange.LongRangeField" 
numDimensions="3"/>
   <fieldType name="longrange4d" class="solr.numericrange.LongRangeField" 
numDimensions="4"/>
 
+  <!-- FloatRangeField types with different dimensions -->
+  <fieldType name="floatrange" class="solr.numericrange.FloatRangeField" 
numDimensions="1"/>
+  <fieldType name="floatrange2d" class="solr.numericrange.FloatRangeField" 
numDimensions="2"/>
+  <fieldType name="floatrange3d" class="solr.numericrange.FloatRangeField" 
numDimensions="3"/>
+  <fieldType name="floatrange4d" class="solr.numericrange.FloatRangeField" 
numDimensions="4"/>
+
   <!-- Field definitions -->
   <field name="id" type="string" indexed="true" stored="true" required="true" 
multiValued="false"/>
   <field name="_version_" type="long" indexed="false" stored="false" 
docValues="true"/>
@@ -67,6 +73,19 @@
   <!-- 4D LongRangeField (tesseract) -->
   <field name="long_range_4d" type="longrange4d" indexed="true" stored="true"/>
 
+  <!-- 1D FloatRangeField -->
+  <field name="float_range" type="floatrange" indexed="true" stored="true"/>
+  <field name="float_range_multi" type="floatrange" indexed="true" 
stored="true" multiValued="true"/>
+
+  <!-- 2D FloatRangeField (bounding box) -->
+  <field name="float_range_2d" type="floatrange2d" indexed="true" 
stored="true"/>
+
+  <!-- 3D FloatRangeField (bounding cube) -->
+  <field name="float_range_3d" type="floatrange3d" indexed="true" 
stored="true"/>
+
+  <!-- 4D FloatRangeField (tesseract) -->
+  <field name="float_range_4d" type="floatrange4d" indexed="true" 
stored="true"/>
+
   <!-- Required by Solr -->
   <uniqueKey>id</uniqueKey>
 </schema>
diff --git 
a/solr/core/src/test/org/apache/solr/schema/numericrange/FloatRangeFieldTest.java
 
b/solr/core/src/test/org/apache/solr/schema/numericrange/FloatRangeFieldTest.java
new file mode 100644
index 00000000000..a2349abe11d
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/schema/numericrange/FloatRangeFieldTest.java
@@ -0,0 +1,398 @@
+/*
+ * 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.FloatRange;
+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 FloatRangeField} */
+public class FloatRangeFieldTest extends SolrTestCase {
+
+  @BeforeClass
+  public static void ensureAssumptions() {
+    assumeWorkingMockito();
+  }
+
+  public void test1DRangeParsing() {
+    FloatRangeField fieldType = createFieldType(1);
+
+    // Valid 1D range with floating-point values
+    FloatRangeField.RangeValue range = fieldType.parseRangeValue("[1.5 TO 
3.14]");
+    assertEquals(1, range.getDimensions());
+    assertEquals(1.5f, range.mins[0], 0.0f);
+    assertEquals(3.14f, range.maxs[0], 0.0f);
+
+    // Integer values are accepted and parsed as floats
+    range = fieldType.parseRangeValue("[10 TO 20]");
+    assertEquals(10.0f, range.mins[0], 0.0f);
+    assertEquals(20.0f, range.maxs[0], 0.0f);
+
+    // With extra whitespace
+    range = fieldType.parseRangeValue("[  1.5   TO   3.14  ]");
+    assertEquals(1.5f, range.mins[0], 0.0f);
+    assertEquals(3.14f, range.maxs[0], 0.0f);
+
+    // Negative numbers
+    range = fieldType.parseRangeValue("[-3.5 TO -1.0]");
+    assertEquals(-3.5f, range.mins[0], 0.0f);
+    assertEquals(-1.0f, range.maxs[0], 0.0f);
+
+    // Point range (min == max)
+    range = fieldType.parseRangeValue("[5.0 TO 5.0]");
+    assertEquals(5.0f, range.mins[0], 0.0f);
+    assertEquals(5.0f, range.maxs[0], 0.0f);
+  }
+
+  public void test2DRangeParsing() {
+    FloatRangeField fieldType = createFieldType(2);
+
+    // Valid 2D range (bounding box)
+    FloatRangeField.RangeValue range = fieldType.parseRangeValue("[1.0,2.0 TO 
3.0,4.0]");
+    assertEquals(2, range.getDimensions());
+    assertEquals(1.0f, range.mins[0], 0.0f);
+    assertEquals(2.0f, range.mins[1], 0.0f);
+    assertEquals(3.0f, range.maxs[0], 0.0f);
+    assertEquals(4.0f, range.maxs[1], 0.0f);
+
+    // With extra whitespace
+    range = fieldType.parseRangeValue("[  1.0 , 2.0   TO   3.0 , 4.0  ]");
+    assertEquals(1.0f, range.mins[0], 0.0f);
+    assertEquals(2.0f, range.mins[1], 0.0f);
+    assertEquals(3.0f, range.maxs[0], 0.0f);
+    assertEquals(4.0f, range.maxs[1], 0.0f);
+  }
+
+  public void test3DRangeParsing() {
+    FloatRangeField fieldType = createFieldType(3);
+
+    // Valid 3D range (bounding cube)
+    FloatRangeField.RangeValue range = fieldType.parseRangeValue("[1.0,2.0,3.0 
TO 4.0,5.0,6.0]");
+    assertEquals(3, range.getDimensions());
+    assertEquals(1.0f, range.mins[0], 0.0f);
+    assertEquals(2.0f, range.mins[1], 0.0f);
+    assertEquals(3.0f, range.mins[2], 0.0f);
+    assertEquals(4.0f, range.maxs[0], 0.0f);
+    assertEquals(5.0f, range.maxs[1], 0.0f);
+    assertEquals(6.0f, range.maxs[2], 0.0f);
+  }
+
+  public void test4DRangeParsing() {
+    FloatRangeField fieldType = createFieldType(4);
+
+    // Valid 4D range (tesseract)
+    FloatRangeField.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.0f, range.mins[0], 0.0f);
+    assertEquals(2.0f, range.mins[1], 0.0f);
+    assertEquals(3.0f, range.mins[2], 0.0f);
+    assertEquals(4.0f, range.mins[3], 0.0f);
+    assertEquals(5.0f, range.maxs[0], 0.0f);
+    assertEquals(6.0f, range.maxs[1], 0.0f);
+    assertEquals(7.0f, range.maxs[2], 0.0f);
+    assertEquals(8.0f, range.maxs[3], 0.0f);
+  }
+
+  public void testInvalidRangeFormat() {
+    FloatRangeField 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() {
+    FloatRangeField 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 
floats"));
+
+    // 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 
floats"));
+  }
+
+  public void testDimensionMismatch() {
+    FloatRangeField fieldType1D = createFieldType(1);
+    FloatRangeField 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() {
+    FloatRangeField 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
+    FloatRangeField 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() {
+    FloatRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "float_range");
+
+    IndexableField field = fieldType.createField(schemaField, "[1.0 TO 2.0]");
+    assertNotNull(field);
+    assertTrue(field instanceof FloatRange);
+    assertEquals("float_range", field.name());
+  }
+
+  public void testFieldCreation2D() {
+    FloatRangeField fieldType = createFieldType(2);
+    SchemaField schemaField = createSchemaField(fieldType, "float_range_2d");
+
+    IndexableField field = fieldType.createField(schemaField, "[0.0,0.0 TO 
10.0,10.0]");
+    assertNotNull(field);
+    assertTrue(field instanceof FloatRange);
+    assertEquals("float_range_2d", field.name());
+  }
+
+  public void testStoredField() {
+    FloatRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "float_range");
+
+    String value = "[1.0 TO 2.0]";
+    IndexableField storedField = fieldType.getStoredField(schemaField, value);
+    assertNotNull(storedField);
+    assertEquals("float_range", storedField.name());
+    assertEquals(value, storedField.stringValue());
+  }
+
+  public void testToInternal() {
+    FloatRangeField 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() {
+    FloatRangeField fieldType = createFieldType(1);
+
+    // String input
+    Object nativeType = fieldType.toNativeType("[1.5 TO 3.14]");
+    assertTrue(nativeType instanceof FloatRangeField.RangeValue);
+    FloatRangeField.RangeValue range = (FloatRangeField.RangeValue) nativeType;
+    assertEquals(1.5f, range.mins[0], 0.0f);
+    assertEquals(3.14f, range.maxs[0], 0.0f);
+
+    // RangeValue input (should pass through)
+    FloatRangeField.RangeValue inputRange =
+        new FloatRangeField.RangeValue(new float[] {5.0f}, new float[] 
{15.0f});
+    Object result = fieldType.toNativeType(inputRange);
+    assertSame(inputRange, result);
+
+    // Null input
+    assertNull(fieldType.toNativeType(null));
+  }
+
+  public void testSortFieldThrowsException() {
+    FloatRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "float_range");
+
+    // Sorting should not be supported
+    SolrException e =
+        expectThrows(SolrException.class, () -> 
fieldType.getSortField(schemaField, true));
+    assertThat(e.getMessage(), containsString("Cannot sort on 
FloatRangeField"));
+    assertThat(e.getMessage(), containsString("float_range"));
+  }
+
+  public void testUninversionType() {
+    FloatRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "float_range");
+
+    // Should return null (no field cache support)
+    assertNull(fieldType.getUninversionType(schemaField));
+  }
+
+  public void testInvalidNumDimensions() {
+    FloatRangeField field = new FloatRangeField();
+    Map<String, String> 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");
+    FloatRangeField field2 = new FloatRangeField();
+    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");
+    FloatRangeField field3 = new FloatRangeField();
+    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() {
+    FloatRangeField fieldType = createFieldType(2);
+    FloatRangeField.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() {
+    FloatRangeField fieldType = createFieldType(1);
+
+    // Integer mantissa with positive exponent
+    FloatRangeField.RangeValue range = fieldType.parseRangeValue("[123e4 TO 
567e8]");
+    assertEquals(123e4f, range.mins[0], 0.0f);
+    assertEquals(567e8f, range.maxs[0], 0.0f);
+
+    // Decimal mantissa with negative exponent
+    range = fieldType.parseRangeValue("[-1.2e-4 TO 3.4e-2]");
+    assertEquals(-1.2e-4f, range.mins[0], 0.0f);
+    assertEquals(3.4e-2f, range.maxs[0], 0.0f);
+
+    // Uppercase E
+    range = fieldType.parseRangeValue("[1.5E3 TO 2.5E3]");
+    assertEquals(1.5e3f, range.mins[0], 0.0f);
+    assertEquals(2.5e3f, range.maxs[0], 0.0f);
+
+    // Explicit positive exponent sign
+    range = fieldType.parseRangeValue("[1.0e+2 TO 9.9e+2]");
+    assertEquals(1.0e+2f, range.mins[0], 0.0f);
+    assertEquals(9.9e+2f, range.maxs[0], 0.0f);
+
+    // Negative mantissa with negative exponent
+    range = fieldType.parseRangeValue("[-9.9E-9 TO -1.1E-9]");
+    assertEquals(-9.9e-9f, range.mins[0], 0.0f);
+    assertEquals(-1.1e-9f, range.maxs[0], 0.0f);
+
+    // Multi-dimensional with scientific notation
+    FloatRangeField fieldType2D = createFieldType(2);
+    range = fieldType2D.parseRangeValue("[1e2,2.0e1 TO 3e2,4.0e1]");
+    assertEquals(1e2f, range.mins[0], 0.0f);
+    assertEquals(2.0e1f, range.mins[1], 0.0f);
+    assertEquals(3e2f, range.maxs[0], 0.0f);
+    assertEquals(4.0e1f, range.maxs[1], 0.0f);
+
+    // Single-bound scientific notation via toInternal
+    String val = "[1.2e3 TO 4.5e3]";
+    assertEquals(val, fieldType.toInternal(val));
+  }
+
+  public void testExtremeValues() {
+    FloatRangeField fieldType = createFieldType(1);
+
+    // Test with very negative and very positive values expressible without 
scientific notation
+    FloatRangeField.RangeValue range = fieldType.parseRangeValue("[-9999999.0 
TO 9999999.0]");
+    assertEquals(-9999999.0f, range.mins[0], 0.0f);
+    assertEquals(9999999.0f, range.maxs[0], 0.0f);
+
+    // Test with small fractional values
+    range = fieldType.parseRangeValue("[0.0001 TO 0.9999]");
+    assertEquals(0.0001f, range.mins[0], 0.0f);
+    assertEquals(0.9999f, range.maxs[0], 0.0f);
+  }
+
+  private IndexSchema createMockSchema() {
+    final var schema = mock(IndexSchema.class);
+    when(schema.getVersion()).thenReturn(1.7f);
+    return schema;
+  }
+
+  private FloatRangeField createFieldType(int numDimensions) {
+    FloatRangeField field = new FloatRangeField();
+    Map<String, String> args = new HashMap<>();
+    args.put("numDimensions", String.valueOf(numDimensions));
+
+    field.init(createMockSchema(), args);
+
+    return field;
+  }
+
+  private SchemaField createSchemaField(FloatRangeField 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/NumericRangeQParserPluginFloatTest.java
 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginFloatTest.java
new file mode 100644
index 00000000000..32b1e1200bd
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginFloatTest.java
@@ -0,0 +1,421 @@
+/*
+ * 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.FloatRangeField} fields.
+ */
+public class NumericRangeQParserPluginFloatTest 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", "float_range", "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", "float_range", "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", "float_range", "[0.5 TO 0.8]"));
+    assertU(adoc("id", "4", "float_range", "[2.0 TO 3.0]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=float_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='float_range'][.='[1.0 TO 2.0]']",
+        "//result/doc/str[@name='float_range'][.='[1.5 TO 2.5]']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=float_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=float_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", "float_range", "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", "float_range", "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", "float_range", "[0.5 TO 0.8]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=float_range}[0.0 TO 
3.0]"),
+        "//result[@numFound='3']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=float_range}[1.0 TO 
2.0]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=float_range}[0.0 TO 
1.0]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='3']");
+  }
+
+  @Test
+  public void test1DContainsQuery() {
+    assertU(adoc("id", "1", "float_range", "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", "float_range", "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", "float_range", "[0.5 TO 3.0]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=\"contains\" field=float_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=float_range}[0.0 
TO 4.0]"),
+        "//result[@numFound='0']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=\"contains\" field=float_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", "float_range", "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", "float_range", "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", "float_range", "[0.5 TO 0.8]"));
+    assertU(adoc("id", "4", "float_range", "[1.2 TO 1.8]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=\"crosses\" field=float_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", "float_range_2d", "[0.0,0.0 TO 1.0,1.0]"));
+    assertU(adoc("id", "2", "float_range_2d", "[0.5,0.5 TO 1.5,1.5]"));
+    assertU(adoc("id", "3", "float_range_2d", "[2.0,2.0 TO 3.0,3.0]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=float_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=float_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=float_range_2d}[10.0,10.0 TO 20.0,20.0]"),
+        "//result[@numFound='0']");
+  }
+
+  @Test
+  public void test3DQuery() {
+    assertU(adoc("id", "1", "float_range_3d", "[0.0,0.0,0.0 TO 1.0,1.0,1.0]"));
+    assertU(adoc("id", "2", "float_range_3d", "[0.5,0.5,0.5 TO 1.5,1.5,1.5]"));
+    assertU(commit());
+
+    assertQ(
+        req(
+            "q",
+            "{!numericRange criteria=intersects 
field=float_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", "float_range_4d", "[0.0,0.0,0.0,0.0 TO 
1.0,1.0,1.0,1.0]"));
+    assertU(adoc("id", "2", "float_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=float_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", "float_range_multi", "[1.0 TO 2.0]", 
"float_range_multi", "[3.0 TO 4.0]"));
+    assertU(adoc("id", "2", "float_range_multi", "[1.5 TO 2.5]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=float_range_multi}[1.1 TO 1.2]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/arr[@name='float_range_multi']/str[1][.='[1.0 TO 2.0]']",
+        "//result/doc/arr[@name='float_range_multi']/str[2][.='[3.0 TO 
4.0]']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=float_range_multi}[3.1 TO 3.2]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=float_range_multi}[1.5 TO 2.5]"),
+        "//result[@numFound='2']");
+  }
+
+  @Test
+  public void testNegativeValues() {
+    assertU(adoc("id", "1", "float_range", "[-1.0 TO -0.5]"));
+    assertU(adoc("id", "2", "float_range", "[-0.75 TO -0.25]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=float_range}[-0.8 
TO -0.6]"),
+        "//result[@numFound='2']");
+  }
+
+  @Test
+  public void testPointRange() {
+    assertU(adoc("id", "1", "float_range", "[1.5 TO 1.5]"));
+    assertU(commit());
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=float_range}[1.5 TO 
1.5]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=float_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=float_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", "float_range", "[1.0 TO 2.0]"));
+    assertU(commit());
+
+    assertQEx(
+        "Invalid query criteria should fail",
+        "Unknown query criteria",
+        req("q", "{!numericRange criteria=\"invalid\" field=float_range}[1.0 
TO 2.0]"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testInvalidRangeValue() {
+    assertU(adoc("id", "1", "float_range", "[1.0 TO 2.0]"));
+    assertU(commit());
+
+    assertQEx(
+        "Invalid range format should fail",
+        "Invalid range",
+        req("q", "{!numericRange criteria=intersects 
field=float_range}invalid"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testEmptyRangeValue() {
+    assertU(adoc("id", "1", "float_range", "[1.0 TO 2.0]"));
+    assertU(commit());
+
+    assertQEx(
+        "Empty range value should fail",
+        req("q", "{!numericRange criteria=intersects field=float_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", "float_range", "[1.3 TO 1.6]")); // No match
+    assertU(adoc("id", "2", "float_range", "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "3", "float_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", "float_range:[1.2 TO 1.8]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void testGetFieldQueryFullRangeMultipleMatches() {
+    assertU(adoc("id", "1", "float_range", "[0.0 TO 10.0]")); // Match!
+    assertU(adoc("id", "2", "float_range", "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "3", "float_range", "[1.0 TO 1.99]")); // No match - 
max too low
+    assertU(adoc("id", "4", "float_range", "[1.01 TO 2.0]")); // No match - 
min too high
+    assertU(commit());
+
+    assertQ(
+        req("q", "float_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: float_range:1.5 is sugar for contains([1.5 TO 1.5])
+    assertU(adoc("id", "1", "float_range", "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "2", "float_range", "[1.5 TO 1.5]")); // Match!
+    assertU(adoc("id", "3", "float_range", "[1.0 TO 1.49]")); // No match - 
max below 1.5
+    assertU(adoc("id", "4", "float_range", "[1.51 TO 3.0]")); // No match - 
min above 1.5
+    assertU(commit());
+
+    assertQ(
+        req("q", "float_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: float_range_2d:0.5,0.5 is sugar for contains([0.5,0.5 
TO 0.5,0.5])
+    assertU(adoc("id", "1", "float_range_2d", "[0.0,0.0 TO 1.0,1.0]")); // 
Match!
+    assertU(adoc("id", "2", "float_range_2d", "[0.5,0.5 TO 0.5,0.5]")); // 
Match!
+    assertU(adoc("id", "3", "float_range_2d", "[0.0,0.0 TO 0.4,1.0]")); // No 
match - X too low
+    assertU(adoc("id", "4", "float_range_2d", "[0.6,0.0 TO 1.0,1.0]")); // No 
match - X too high
+    assertU(commit());
+
+    assertQ(
+        req("q", "float_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", "float_range", "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", "float_range_2d", "[1.0,2.0 TO 3.0,4.0]"));
+    assertU(adoc("id", "3", "float_range_3d", "[0.5,1.0,1.5 TO 2.5,3.0,3.5]"));
+    assertU(adoc("id", "4", "float_range_4d", "[0.1,0.2,0.3,0.4 TO 
1.1,1.2,1.3,1.4]"));
+    assertU(
+        adoc(
+            "id",
+            "5",
+            "float_range_multi",
+            "[0.5 TO 1.0]",
+            "float_range_multi",
+            "[2.0 TO 3.0]",
+            "float_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='float_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='float_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='float_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='float_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='float_range_multi']/str[1][.='[0.5 TO 1.0]']",
+        "//result/doc/arr[@name='float_range_multi']/str[2][.='[2.0 TO 3.0]']",
+        "//result/doc/arr[@name='float_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 2b176e77786..69aea0b8b12 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
@@ -61,6 +61,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: `<fieldType name="long [...]
 
+|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: `<fieldType name=" [...]
+
 |NestPathField | Specialized field type storing enhanced information, when 
xref:indexing-nested-documents.adoc#schema-configuration[working with nested 
documents].
 
 |PointType |A single-valued n-dimensional point. It's both for sorting spatial 
data that is _not_ lat-lon, and for some more rare use-cases. (NOTE: this is 
_not_ related to the "Point" based numeric fields). See 
xref:query-guide:spatial-search.adoc[] for more information.
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 45e32c61c64..6e948958556 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`) 
using a specified query-range.
+Allows users to search range fields (e.g. `IntRangeField`, `LongRangeField`, 
`FloatRangeField`) 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`)
+Must be a numeric range field type (e.g. `IntRangeField`, `LongRangeField`, 
`FloatRangeField`)
 `criteria`::
 +
 [%autowidth,frame=none]

Reply via email to