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

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


The following commit(s) were added to refs/heads/main by this push:
     new efca456419b SOLR-13309: Add IntRangeField for Lucenes IntRange (#4141)
efca456419b is described below

commit efca456419bfdcb18a156f79daba62d9d9e10a7b
Author: Jason Gerlowski <[email protected]>
AuthorDate: Tue Mar 3 07:07:02 2026 -0500

    SOLR-13309: Add IntRangeField for Lucenes IntRange (#4141)
    
    This commit adds a new field type, IntRangeField, that can be used to
    hold singular or multi-dimensional (up to 4) ranges of integers.
    
    Field values are represented using brackets and the "TO" operator, with
    commas used to delimit dimensions (when a particular field is defined as
    having more than 1 dimension), e.g.
    
    [-1 TO 5]
    [1,2 TO 5,10]
    [1 TO 1]
    
    IntRangeField does not support docValues or uninversion, meaning it's
    primarily only used for querying. The field can be stored and returned in
    search-results. Searches on these range-fields rely on a new
    QParserPlugin implementation, {!numericRange}, which supports
    "intersects", "crosses", "within", and "contains" semantics via a
    "criteria" local param. e.g.
    
      - {!numericRange field=price_range criteria=within}[1 TO 5]
         Matches docs whose 'price_range' field falls fully within [1 TO 5].
         A doc with [2 TO 3] would match; [3 TO 6] or [8 TO 10] would not.
      - {!numericRange field=price_range criteria=crosses}[1,10 TO 5,20]
         Matches docs whose 'price_range' field is partially but not fully
         contained within [1,10 TO 5,20]. A doc with [2,11 TO 6,21] would
         match, but [3,11 TO 5,19] would not.
    
    This syntax is experimental and may change in the future.
---
 .../SOLR-13309-introduce-int-range-field.yml       |   8 +
 .../solr/schema/numericrange/IntRangeField.java    | 417 +++++++++++++++++
 .../solr/schema/numericrange/package-info.java     |  19 +
 .../java/org/apache/solr/search/QParserPlugin.java |   2 +
 .../search/numericrange/IntRangeQParserPlugin.java | 235 ++++++++++
 .../solr/search/numericrange/package-info.java     |  19 +
 .../solr/collection1/conf/schema-intrange.xml      |  53 +++
 .../schema/numericrange/IntRangeFieldTest.java     | 352 +++++++++++++++
 .../solr/schema/numericrange/package-info.java     |  19 +
 .../numericrange/IntRangeQParserPluginTest.java    | 499 +++++++++++++++++++++
 .../pages/field-types-included-with-solr.adoc      |   4 +-
 .../modules/query-guide/pages/other-parsers.adoc   |  66 +++
 12 files changed, 1692 insertions(+), 1 deletion(-)

diff --git a/changelog/unreleased/SOLR-13309-introduce-int-range-field.yml 
b/changelog/unreleased/SOLR-13309-introduce-int-range-field.yml
new file mode 100644
index 00000000000..c4e2159df65
--- /dev/null
+++ b/changelog/unreleased/SOLR-13309-introduce-int-range-field.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Introduce new `IntRangeField` field type and (experimental) 
`{!numericRange}` query parser for storing and querying integer 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/IntRangeField.java 
b/solr/core/src/java/org/apache/solr/schema/numericrange/IntRangeField.java
new file mode 100644
index 00000000000..e0a143da144
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/IntRangeField.java
@@ -0,0 +1,417 @@
+/*
+ * 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.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.lucene.document.IntRange;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.response.TextResponseWriter;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.PrimitiveFieldType;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.QParser;
+import org.apache.solr.uninverting.UninvertingReader.Type;
+
+/**
+ * Field type for integer ranges with support for 1-4 dimensions.
+ *
+ * <p>This field type wraps Lucene's {@link IntRange} to provide storage and 
querying of integer
+ * 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 [10 TO 20]}
+ *   <li>2D: {@code [10,20 TO 30,40]}
+ *   <li>3D: {@code [10,20,30 TO 40,50,60]}
+ *   <li>4D: {@code [10,20,30,40 TO 50,60,70,80]}
+ * </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.
+ *
+ * <h2>Schema Configuration</h2>
+ *
+ * <pre>
+ * &lt;fieldType name="intrange" 
class="org.apache.solr.schema.numericrange.IntRangeField" numDimensions="1"/&gt;
+ * &lt;fieldType name="intrange2d" 
class="org.apache.solr.schema.numericrange.IntRangeField" numDimensions="2"/&gt;
+ * &lt;field name="price_range" type="intrange" indexed="true" 
stored="true"/&gt;
+ * &lt;field name="bbox" type="intrange2d" 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}[100 TO 200]}
+ *   <li>Within: {@code {!numericRange criteria="within" field=price_range}[0 
TO 300]}
+ *   <li>Contains: {@code {!numericRange criteria="contains" 
field=price_range}[150 TO 175]}
+ *   <li>Crosses: {@code {!numericRange criteria="crosses" 
field=price_range}[150 TO 250]}
+ * </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 IntRange
+ * @see org.apache.solr.search.numericrange.IntRangeQParserPlugin
+ */
+public class IntRangeField extends PrimitiveFieldType {
+
+  private static final String COMMA_DELIMITED_INTS = 
"-?\\d+(?:\\s*,\\s*-?\\d+)*";
+  private static final String RANGE_PATTERN =
+      "\\[\\s*(" + COMMA_DELIMITED_INTS + ")\\s+TO\\s+(" + 
COMMA_DELIMITED_INTS + ")\\s*\\]";
+  private static final Pattern RANGE_PATTERN_REGEX = 
Pattern.compile(RANGE_PATTERN);
+  private static final Pattern SINGLE_BOUND_PATTERN =
+      Pattern.compile("^" + COMMA_DELIMITED_INTS + "$");
+
+  private int numDimensions = 1;
+
+  @Override
+  protected boolean enableDocValuesByDefault() {
+    return false; // IntRange does not support docValues
+  }
+
+  @Override
+  protected void init(IndexSchema schema, Map<String, String> args) {
+    super.init(schema, args);
+
+    String numDimensionsStr = args.remove("numDimensions");
+    if (numDimensionsStr != null) {
+      numDimensions = Integer.parseInt(numDimensionsStr);
+      if (numDimensions < 1 || numDimensions > 4) {
+        throw new SolrException(
+            ErrorCode.SERVER_ERROR,
+            "numDimensions must be between 1 and 4, but was ["
+                + numDimensions
+                + "] for field type "
+                + typeName);
+      }
+    }
+
+    // IntRange does not support docValues - validate this wasn't explicitly 
set
+    if (hasProperty(DOC_VALUES)) {
+      throw new SolrException(
+          ErrorCode.SERVER_ERROR,
+          "docValues=true enabled but IntRangeField does not support docValues 
for field type "
+              + typeName);
+    }
+  }
+
+  @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 IntRange(field.getName(), rangeValue.mins, rangeValue.maxs);
+  }
+
+  @Override
+  public List<IndexableField> createFields(SchemaField field, Object value) {
+    IndexableField indexedField = createField(field, value);
+    List<IndexableField> fields = new java.util.ArrayList<>();
+
+    if (indexedField != null) {
+      fields.add(indexedField);
+    }
+
+    if (field.stored()) {
+      String valueStr = value.toString();
+      fields.add(getStoredField(field, valueStr));
+    }
+
+    return fields;
+  }
+
+  @Override
+  public void write(TextResponseWriter writer, String name, IndexableField f) 
throws IOException {
+    writer.writeStr(name, toExternal(f), false);
+  }
+
+  @Override
+  public SortField getSortField(SchemaField field, boolean top) {
+    throw new SolrException(
+        ErrorCode.BAD_REQUEST, "Cannot sort on IntRangeField: " + 
field.getName());
+  }
+
+  @Override
+  public Type getUninversionType(SchemaField sf) {
+    return null; // No field cache support
+  }
+
+  @Override
+  public String toInternal(String val) {
+    // Validate format and return as-is
+    parseRangeValue(val);
+    return val;
+  }
+
+  @Override
+  public String toExternal(IndexableField f) {
+    return f.stringValue();
+  }
+
+  @Override
+  public Object toNativeType(Object val) {
+    if (val == null) return null;
+    if (val instanceof RangeValue) return val;
+    return parseRangeValue(val.toString());
+  }
+
+  /**
+   * 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
+   */
+  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 = 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 ints, but got: "
+              + value);
+    }
+
+    String minPart = matcher.group(1).trim();
+    String maxPart = matcher.group(2).trim();
+
+    int[] mins = parseIntArray(minPart, "min values");
+    int[] maxs = parseIntArray(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);
+  }
+
+  /**
+   * Parse a comma-separated string of integers into an array.
+   *
+   * @param str the string to parse
+   * @param description description for error messages
+   * @return array of parsed integers
+   */
+  private int[] parseIntArray(String str, String description) {
+    String[] parts = str.split(",");
+    int[] result = new int[parts.length];
+
+    for (int i = 0; i < parts.length; i++) {
+      try {
+        result[i] = Integer.parseInt(parts[i].trim());
+      } catch (NumberFormatException e) {
+        throw new SolrException(
+            ErrorCode.BAD_REQUEST,
+            "Invalid integer in " + description + ": '" + parts[i].trim() + 
"'",
+            e);
+      }
+    }
+
+    return result;
+  }
+
+  protected StoredField getStoredField(SchemaField sf, Object value) {
+    return new StoredField(sf.getName(), value.toString());
+  }
+
+  /**
+   * Creates a query for this field that matches docs where the query-range is 
fully contained by
+   * the field value.
+   *
+   * <p>Queries requiring other match semantics can use {@link
+   * org.apache.solr.search.numericrange.IntRangeQParserPlugin}
+   *
+   * @param parser The {@link org.apache.solr.search.QParser} calling the 
method
+   * @param field The {@link org.apache.solr.schema.SchemaField} of the field 
to search
+   * @param externalVal The String representation of the value to search. 
Supports both a
+   *     (multi-)dimensional range of the form [1,2 TO 3,4], or a single 
(multi-)dimensional bound
+   *     (e.g. 1,2). In the latter case, the single bound will be used as both 
the min and max. Both
+   *     formats use "contains" query semantics to find indexed ranges that 
contain the query range.
+   * @return Query for this field using contains semantics
+   */
+  @Override
+  public Query getFieldQuery(QParser parser, SchemaField field, String 
externalVal) {
+    if (externalVal == null || externalVal.trim().isEmpty()) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Query value cannot be 
null or empty");
+    }
+
+    String trimmed = externalVal.trim();
+
+    // Check if it's the full range syntax: [min1,min2 TO max1,max2]
+    if (RANGE_PATTERN_REGEX.matcher(trimmed).matches()) {
+      RangeValue rangeValue = parseRangeValue(trimmed);
+      return IntRange.newContainsQuery(field.getName(), rangeValue.mins, 
rangeValue.maxs);
+    }
+
+    // Syntax sugar: also accept a single-bound (i.e pX,pY,pZ)
+    if (SINGLE_BOUND_PATTERN.matcher(trimmed).matches()) {
+      int[] bound = parseIntArray(trimmed, "single bound values");
+
+      if (bound.length != numDimensions) {
+        throw new SolrException(
+            ErrorCode.BAD_REQUEST,
+            "Single bound dimensions ("
+                + bound.length
+                + ") do not match field type numDimensions ("
+                + numDimensions
+                + ")");
+      }
+
+      return IntRange.newContainsQuery(field.getName(), bound, bound);
+    }
+
+    throw new SolrException(
+        ErrorCode.BAD_REQUEST,
+        "Invalid query format. Expected either a range [min TO max] or a 
single bound to search for, got: "
+            + externalVal);
+  }
+
+  @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 intersects 
query
+    if (part1 == null || part2 == null) {
+      return super.getSpecializedRangeQuery(
+          parser, field, part1, part2, minInclusive, maxInclusive);
+    }
+
+    // Parse the range bounds as single-dimensional values
+    int min, max;
+    try {
+      min = Integer.parseInt(part1.trim());
+      max = Integer.parseInt(part2.trim());
+    } catch (NumberFormatException e) {
+      throw new SolrException(
+          ErrorCode.BAD_REQUEST,
+          "Invalid integer values in range query: [" + part1 + " TO " + part2 
+ "]",
+          e);
+    }
+
+    if (!minInclusive) {
+      min = (min == Integer.MAX_VALUE) ? min : min + 1;
+    }
+    if (!maxInclusive) {
+      max = (max == Integer.MIN_VALUE) ? max : max - 1;
+    }
+
+    // Build arrays for the query based on configured dimensions
+    int[] mins = new int[numDimensions];
+    int[] maxs = new int[numDimensions];
+
+    // For now, only support 1D range syntax with field:[X TO Y]
+    if (numDimensions == 1) {
+      mins[0] = min;
+      maxs[0] = max;
+      return IntRange.newIntersectsQuery(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 range values. */
+  public static class RangeValue {
+    public final int[] mins;
+    public final int[] maxs;
+
+    public RangeValue(int[] mins, int[] maxs) {
+      this.mins = mins;
+      this.maxs = maxs;
+    }
+
+    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/package-info.java 
b/solr/core/src/java/org/apache/solr/schema/numericrange/package-info.java
new file mode 100644
index 00000000000..1b30e748fde
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Support for numeric-range field types. */
+package org.apache.solr.schema.numericrange;
diff --git a/solr/core/src/java/org/apache/solr/search/QParserPlugin.java 
b/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
index 45409cb8982..ba27a86b1f2 100644
--- a/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
@@ -28,6 +28,7 @@ import org.apache.solr.search.join.GraphQParserPlugin;
 import org.apache.solr.search.join.HashRangeQParserPlugin;
 import org.apache.solr.search.mlt.MLTContentQParserPlugin;
 import org.apache.solr.search.mlt.MLTQParserPlugin;
+import org.apache.solr.search.numericrange.IntRangeQParserPlugin;
 import org.apache.solr.search.vector.KnnQParserPlugin;
 import org.apache.solr.search.vector.VectorSimilarityQParserPlugin;
 import org.apache.solr.util.plugin.NamedListInitializedPlugin;
@@ -90,6 +91,7 @@ public abstract class QParserPlugin implements 
NamedListInitializedPlugin {
     map.put(KnnQParserPlugin.NAME, new KnnQParserPlugin());
     map.put(VectorSimilarityQParserPlugin.NAME, new 
VectorSimilarityQParserPlugin());
     map.put(FuzzyQParserPlugin.NAME, new FuzzyQParserPlugin());
+    map.put(IntRangeQParserPlugin.NAME, new IntRangeQParserPlugin());
 
     standardPlugins = Collections.unmodifiableMap(map);
   }
diff --git 
a/solr/core/src/java/org/apache/solr/search/numericrange/IntRangeQParserPlugin.java
 
b/solr/core/src/java/org/apache/solr/search/numericrange/IntRangeQParserPlugin.java
new file mode 100644
index 00000000000..5c1e8a6402a
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/search/numericrange/IntRangeQParserPlugin.java
@@ -0,0 +1,235 @@
+/*
+ * 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 java.util.Locale;
+import org.apache.lucene.document.IntRange;
+import org.apache.lucene.search.Query;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.numericrange.IntRangeField;
+import org.apache.solr.schema.numericrange.IntRangeField.RangeValue;
+import org.apache.solr.search.QParser;
+import org.apache.solr.search.QParserPlugin;
+import org.apache.solr.search.QueryParsing;
+import org.apache.solr.search.SyntaxError;
+
+/**
+ * Query parser for IntRangeField with support for different query 
relationship types.
+ *
+ * <p>This parser enables queries against {@link IntRangeField} fields with 
explicit control over
+ * the query relationship type (intersects, within, contains, crosses).
+ *
+ * <h2>Parameters</h2>
+ *
+ * <ul>
+ *   <li><b>field</b> (required): The IntRangeField to query
+ *   <li><b>criteria</b> (required): Query relationship criteria. One of: 
intersects, within,
+ *       contains, crosses
+ * </ul>
+ *
+ * <h2>Query Types</h2>
+ *
+ * <ul>
+ *   <li><b>intersects</b>: Matches ranges that overlap with the query range 
(most permissive)
+ *   <li><b>within</b>: Matches ranges completely contained by the query range
+ *   <li><b>contains</b>: Matches ranges that completely contain the query 
range
+ *   <li><b>crosses</b>: Matches ranges that cross the query range boundaries 
(not disjoint, not
+ *       wholly contained)
+ * </ul>
+ *
+ * <h2>Example Usage</h2>
+ *
+ * <pre>
+ * // 1D range queries
+ * {!numericRange criteria="intersects" field=price_range}[100 TO 200]
+ * {!numericRange criteria="within" field=price_range}[0 TO 300]
+ * {!numericRange criteria="contains" field=price_range}[150 TO 175]
+ * {!numericRange criteria="crosses" field=price_range}[150 TO 250]
+ *
+ * // 2D range queries (bounding boxes)
+ * {!numericRange criteria="intersects" field=bbox}[0,0 TO 10,10]
+ * {!numericRange criteria="within" field=bbox}[-10,-10 TO 20,20]
+ *
+ * // 3D range queries (bounding cubes)
+ * {!numericRange criteria="intersects" field=cube}[0,0,0 TO 10,10,10]
+ *
+ * // 4D range queries (tesseracts)
+ * {!numericRange criteria="intersects" field=tesseract}[0,0,0,0 TO 
10,10,10,10]
+ * </pre>
+ *
+ * @see IntRangeField
+ * @see IntRange
+ * @lucene.experimental
+ */
+public class IntRangeQParserPlugin extends QParserPlugin {
+
+  /** Query relationship criteria for range queries. */
+  public enum QueryCriteria {
+    /** Matches ranges that overlap with the query range (most permissive). */
+    INTERSECTS("intersects"),
+
+    /** Matches ranges completely contained by the query range. */
+    WITHIN("within"),
+
+    /** Matches ranges that completely contain the query range. */
+    CONTAINS("contains"),
+
+    /**
+     * Matches ranges that cross the query range boundaries (not disjoint, not 
wholly contained).
+     */
+    CROSSES("crosses");
+
+    private final String name;
+
+    QueryCriteria(String name) {
+      this.name = name;
+    }
+
+    /**
+     * Parse a criteria string into a QueryCriteria enum value.
+     *
+     * @param criteriaStr the criteria string (case-insensitive)
+     * @return the corresponding QueryCriteria
+     * @throws SolrException if the criteria string is not recognized
+     */
+    public static QueryCriteria fromString(String criteriaStr) {
+      if (criteriaStr == null || criteriaStr.trim().isEmpty()) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "Query criteria cannot 
be null or empty");
+      }
+
+      String normalized = criteriaStr.trim().toLowerCase(Locale.ROOT);
+      for (QueryCriteria criteria : values()) {
+        if (criteria.name.equals(normalized)) {
+          return criteria;
+        }
+      }
+
+      throw new SolrException(
+          ErrorCode.BAD_REQUEST,
+          "Unknown query criteria: '"
+              + criteriaStr
+              + "'. Valid criteria are: intersects, within, contains, 
crosses");
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  /** Parser name used in local params syntax: {@code {!numericRange ...}} */
+  public static final String NAME = "numericRange";
+
+  /** Parameter name for the field to query */
+  public static final String FIELD_PARAM = "field";
+
+  /** Parameter name for the query criteria (intersects, within, contains, 
crosses) */
+  public static final String CRITERIA_PARAM = "criteria";
+
+  @Override
+  public QParser createParser(
+      String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest 
req) {
+    return new QParser(qstr, localParams, params, req) {
+      @Override
+      public Query parse() throws SyntaxError {
+        // Get required field parameter
+        String fieldName = localParams.get(FIELD_PARAM);
+        if (fieldName == null || fieldName.trim().isEmpty()) {
+          throw new SolrException(
+              ErrorCode.BAD_REQUEST, "Missing required parameter: " + 
FIELD_PARAM);
+        }
+
+        // Get required query criteria parameter and parse to enum
+        String criteriaStr = localParams.get(CRITERIA_PARAM);
+        if (criteriaStr == null || criteriaStr.trim().isEmpty()) {
+          throw new SolrException(
+              ErrorCode.BAD_REQUEST, "Missing required parameter: " + 
CRITERIA_PARAM);
+        }
+        QueryCriteria criteria = QueryCriteria.fromString(criteriaStr);
+
+        // Get the range value from the query string or 'v' param
+        String rangeValue = localParams.get(QueryParsing.V, qstr);
+        if (rangeValue == null || rangeValue.trim().isEmpty()) {
+          throw new SolrException(ErrorCode.BAD_REQUEST, "Range value cannot 
be empty");
+        }
+
+        // Validate field exists and is an IntRangeField
+        SchemaField schemaField;
+        try {
+          schemaField = req.getSchema().getField(fieldName);
+        } catch (SolrException e) {
+          throw new SolrException(ErrorCode.BAD_REQUEST, "Field not found: " + 
fieldName, e);
+        }
+
+        if (!(schemaField.getType() instanceof IntRangeField)) {
+          throw new SolrException(
+              ErrorCode.BAD_REQUEST,
+              "Field '"
+                  + fieldName
+                  + "' must be of type IntRangeField, but is: "
+                  + schemaField.getType().getTypeName());
+        }
+
+        IntRangeField fieldType = (IntRangeField) schemaField.getType();
+
+        // Parse the range value
+        RangeValue range;
+        try {
+          range = fieldType.parseRangeValue(rangeValue);
+        } catch (SolrException e) {
+          throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid range value: 
" + rangeValue, e);
+        }
+
+        // Create appropriate query based on criteria
+        return createRangeQuery(fieldName, range.mins, range.maxs, criteria);
+      }
+
+      /**
+       * Create the appropriate Lucene query based on the query criteria.
+       *
+       * @param fieldName the field to query
+       * @param mins minimum values for each dimension
+       * @param maxs maximum values for each dimension
+       * @param criteria the query relationship criteria
+       * @return the created Lucene Query
+       */
+      private Query createRangeQuery(
+          String fieldName, int[] mins, int[] maxs, QueryCriteria criteria) {
+        switch (criteria) {
+          case INTERSECTS:
+            return IntRange.newIntersectsQuery(fieldName, mins, maxs);
+
+          case WITHIN:
+            return IntRange.newWithinQuery(fieldName, mins, maxs);
+
+          case CONTAINS:
+            return IntRange.newContainsQuery(fieldName, mins, maxs);
+
+          case CROSSES:
+            return IntRange.newCrossesQuery(fieldName, mins, maxs);
+
+          default:
+            throw new AssertionError("Unhandled QueryCriteria: " + criteria);
+        }
+      }
+    };
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/search/numericrange/package-info.java 
b/solr/core/src/java/org/apache/solr/search/numericrange/package-info.java
new file mode 100644
index 00000000000..395317a0118
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/numericrange/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** QParser and related classes for searching numeric range fields. */
+package org.apache.solr.search.numericrange;
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-intrange.xml 
b/solr/core/src/test-files/solr/collection1/conf/schema-intrange.xml
new file mode 100644
index 00000000000..bcfd1b5dc1a
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-intrange.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" ?>
+<!--
+ 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.
+-->
+
+<!-- Test schema for IntRangeField -->
+<schema name="intrange-test" version="1.7">
+
+  <!-- Basic field types -->
+  <fieldType name="string" class="solr.StrField" sortMissingLast="true" />
+  <fieldType name="int" class="solr.IntPointField"/>
+  <fieldType name="long" class="solr.LongPointField"/>
+
+  <!-- IntRangeField types with different dimensions -->
+  <fieldType name="intrange" class="solr.numericrange.IntRangeField" 
numDimensions="1"/>
+  <fieldType name="intrange2d" class="solr.numericrange.IntRangeField" 
numDimensions="2"/>
+  <fieldType name="intrange3d" class="solr.numericrange.IntRangeField" 
numDimensions="3"/>
+  <fieldType name="intrange4d" class="solr.numericrange.IntRangeField" 
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"/>
+  <field name="title" type="string" indexed="true" stored="true"/>
+
+  <!-- 1D IntRangeField -->
+  <field name="price_range" type="intrange" indexed="true" stored="true"/>
+  <field name="price_range_multi" type="intrange" indexed="true" stored="true" 
multiValued="true"/>
+
+  <!-- 2D IntRangeField (bounding box) -->
+  <field name="bbox" type="intrange2d" indexed="true" stored="true"/>
+
+  <!-- 3D IntRangeField (bounding cube) -->
+  <field name="cube" type="intrange3d" indexed="true" stored="true"/>
+
+  <!-- 4D IntRangeField (tesseract) -->
+  <field name="tesseract" type="intrange4d" indexed="true" stored="true"/>
+
+  <!-- Required by Solr -->
+  <uniqueKey>id</uniqueKey>
+</schema>
diff --git 
a/solr/core/src/test/org/apache/solr/schema/numericrange/IntRangeFieldTest.java 
b/solr/core/src/test/org/apache/solr/schema/numericrange/IntRangeFieldTest.java
new file mode 100644
index 00000000000..60f8d3b19ab
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/schema/numericrange/IntRangeFieldTest.java
@@ -0,0 +1,352 @@
+/*
+ * 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.IntRange;
+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 IntRangeField} */
+public class IntRangeFieldTest extends SolrTestCase {
+
+  @BeforeClass
+  public static void ensureAssumptions() {
+    assumeWorkingMockito();
+  }
+
+  public void test1DRangeParsing() {
+    IntRangeField fieldType = createFieldType(1);
+
+    // Valid 1D range
+    IntRangeField.RangeValue range = fieldType.parseRangeValue("[10 TO 20]");
+    assertEquals(1, range.getDimensions());
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.maxs[0]);
+
+    // With extra whitespace
+    range = fieldType.parseRangeValue("[  10   TO   20  ]");
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.maxs[0]);
+
+    // Negative numbers
+    range = fieldType.parseRangeValue("[-100 TO -50]");
+    assertEquals(-100, range.mins[0]);
+    assertEquals(-50, range.maxs[0]);
+
+    // Point range (min == max)
+    range = fieldType.parseRangeValue("[5 TO 5]");
+    assertEquals(5, range.mins[0]);
+    assertEquals(5, range.maxs[0]);
+  }
+
+  public void test2DRangeParsing() {
+    IntRangeField fieldType = createFieldType(2);
+
+    // Valid 2D range (bounding box)
+    IntRangeField.RangeValue range = fieldType.parseRangeValue("[10,20 TO 
30,40]");
+    assertEquals(2, range.getDimensions());
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.mins[1]);
+    assertEquals(30, range.maxs[0]);
+    assertEquals(40, range.maxs[1]);
+
+    // With extra whitespace
+    range = fieldType.parseRangeValue("[  10 , 20   TO   30 , 40  ]");
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.mins[1]);
+    assertEquals(30, range.maxs[0]);
+    assertEquals(40, range.maxs[1]);
+  }
+
+  public void test3DRangeParsing() {
+    IntRangeField fieldType = createFieldType(3);
+
+    // Valid 3D range (bounding cube)
+    IntRangeField.RangeValue range = fieldType.parseRangeValue("[10,20,30 TO 
40,50,60]");
+    assertEquals(3, range.getDimensions());
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.mins[1]);
+    assertEquals(30, range.mins[2]);
+    assertEquals(40, range.maxs[0]);
+    assertEquals(50, range.maxs[1]);
+    assertEquals(60, range.maxs[2]);
+  }
+
+  public void test4DRangeParsing() {
+    IntRangeField fieldType = createFieldType(4);
+
+    // Valid 4D range (tesseract)
+    IntRangeField.RangeValue range = fieldType.parseRangeValue("[10,20,30,40 
TO 50,60,70,80]");
+    assertEquals(4, range.getDimensions());
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.mins[1]);
+    assertEquals(30, range.mins[2]);
+    assertEquals(40, range.mins[3]);
+    assertEquals(50, range.maxs[0]);
+    assertEquals(60, range.maxs[1]);
+    assertEquals(70, range.maxs[2]);
+    assertEquals(80, range.maxs[3]);
+  }
+
+  public void testInvalidRangeFormat() {
+    IntRangeField fieldType = createFieldType(1);
+
+    // Missing brackets
+    SolrException e1 =
+        expectThrows(SolrException.class, () -> fieldType.parseRangeValue("10 
TO 20"));
+    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("[10 
20]"));
+    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() {
+    IntRangeField 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 
ints"));
+
+    // Partially numeric
+    SolrException e2 =
+        expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[10 
TO xyz]"));
+    assertThat(e2.getMessage(), containsString("Invalid range"));
+    assertThat(e2.getMessage(), containsString("where min and max values are 
ints"));
+
+    // Floating point (should fail for IntRange)
+    SolrException e3 =
+        expectThrows(SolrException.class, () -> 
fieldType.parseRangeValue("[10.5 TO 20.5]"));
+    assertThat(e3.getMessage(), containsString("Invalid range"));
+    assertThat(e3.getMessage(), containsString("where min and max values are 
ints"));
+  }
+
+  public void testDimensionMismatch() {
+    IntRangeField fieldType1D = createFieldType(1);
+    IntRangeField fieldType2D = createFieldType(2);
+
+    // 2D value on 1D field
+    SolrException e1 =
+        expectThrows(SolrException.class, () -> 
fieldType1D.parseRangeValue("[10,20 TO 30,40]"));
+    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("[10 TO 20]"));
+    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("[10,20 TO 30]")); // 2D mins, 
1D maxs
+    assertThat(e3.getMessage(), containsString("Min and max dimensions must 
match"));
+  }
+
+  public void testMinGreaterThanMax() {
+    IntRangeField fieldType = createFieldType(1);
+
+    // Min > max should fail
+    SolrException e1 =
+        expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[20 
TO 10]"));
+    assertThat(e1.getMessage(), containsString("Min value must be <= max 
value"));
+    assertThat(e1.getMessage(), containsString("dimension 0"));
+
+    // For 2D
+    IntRangeField fieldType2D = createFieldType(2);
+    SolrException e2 =
+        expectThrows(
+            SolrException.class,
+            () -> fieldType2D.parseRangeValue("[30,20 TO 10,40]")); // First 
dimension invalid
+    assertThat(e2.getMessage(), containsString("Min value must be <= max 
value"));
+    assertThat(e2.getMessage(), containsString("dimension 0"));
+  }
+
+  public void testFieldCreation1D() {
+    IntRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "price_range");
+
+    IndexableField field = fieldType.createField(schemaField, "[100 TO 200]");
+    assertNotNull(field);
+    assertTrue(field instanceof IntRange);
+    assertEquals("price_range", field.name());
+  }
+
+  public void testFieldCreation2D() {
+    IntRangeField fieldType = createFieldType(2);
+    SchemaField schemaField = createSchemaField(fieldType, "bbox");
+
+    IndexableField field = fieldType.createField(schemaField, "[0,0 TO 
10,10]");
+    assertNotNull(field);
+    assertTrue(field instanceof IntRange);
+    assertEquals("bbox", field.name());
+  }
+
+  public void testStoredField() {
+    IntRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "price_range");
+
+    String value = "[100 TO 200]";
+    IndexableField storedField = fieldType.getStoredField(schemaField, value);
+    assertNotNull(storedField);
+    assertEquals("price_range", storedField.name());
+    assertEquals(value, storedField.stringValue());
+  }
+
+  public void testToInternal() {
+    IntRangeField fieldType = createFieldType(1);
+
+    // Valid value should pass through after validation
+    String value = "[10 TO 20]";
+    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() {
+    IntRangeField fieldType = createFieldType(1);
+
+    // String input
+    Object nativeType = fieldType.toNativeType("[10 TO 20]");
+    assertTrue(nativeType instanceof IntRangeField.RangeValue);
+    IntRangeField.RangeValue range = (IntRangeField.RangeValue) nativeType;
+    assertEquals(10, range.mins[0]);
+    assertEquals(20, range.maxs[0]);
+
+    // RangeValue input (should pass through)
+    IntRangeField.RangeValue inputRange =
+        new IntRangeField.RangeValue(new int[] {5}, new int[] {15});
+    Object result = fieldType.toNativeType(inputRange);
+    assertSame(inputRange, result);
+
+    // Null input
+    assertNull(fieldType.toNativeType(null));
+  }
+
+  public void testSortFieldThrowsException() {
+    IntRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "price_range");
+
+    // Sorting should not be supported
+    SolrException e =
+        expectThrows(SolrException.class, () -> 
fieldType.getSortField(schemaField, true));
+    assertThat(e.getMessage(), containsString("Cannot sort on IntRangeField"));
+    assertThat(e.getMessage(), containsString("price_range"));
+  }
+
+  public void testUninversionType() {
+    IntRangeField fieldType = createFieldType(1);
+    SchemaField schemaField = createSchemaField(fieldType, "price_range");
+
+    // Should return null (no field cache support)
+    assertNull(fieldType.getUninversionType(schemaField));
+  }
+
+  public void testInvalidNumDimensions() {
+    IntRangeField field = new IntRangeField();
+    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");
+    IntRangeField field2 = new IntRangeField();
+    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");
+    IntRangeField field3 = new IntRangeField();
+    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() {
+    IntRangeField fieldType = createFieldType(2);
+    IntRangeField.RangeValue range = fieldType.parseRangeValue("[10,20 TO 
30,40]");
+
+    String str = range.toString();
+    assertEquals("[10,20 TO 30,40]", str);
+  }
+
+  public void testExtremeValues() {
+    IntRangeField fieldType = createFieldType(1);
+
+    // Test with Integer.MIN_VALUE and Integer.MAX_VALUE
+    IntRangeField.RangeValue range =
+        fieldType.parseRangeValue("[" + Integer.MIN_VALUE + " TO " + 
Integer.MAX_VALUE + "]");
+    assertEquals(Integer.MIN_VALUE, range.mins[0]);
+    assertEquals(Integer.MAX_VALUE, range.maxs[0]);
+  }
+
+  private IndexSchema createMockSchema() {
+    final var schema = mock(IndexSchema.class);
+    when(schema.getVersion()).thenReturn(1.7f);
+    return schema;
+  }
+
+  private IntRangeField createFieldType(int numDimensions) {
+    IntRangeField field = new IntRangeField();
+    Map<String, String> args = new HashMap<>();
+    args.put("numDimensions", String.valueOf(numDimensions));
+
+    field.init(createMockSchema(), args);
+
+    return field;
+  }
+
+  private SchemaField createSchemaField(IntRangeField 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/schema/numericrange/package-info.java 
b/solr/core/src/test/org/apache/solr/schema/numericrange/package-info.java
new file mode 100644
index 00000000000..053e0c13bc7
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/schema/numericrange/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Tests for code in the corresponding 'main' package */
+package org.apache.solr.schema.numericrange;
diff --git 
a/solr/core/src/test/org/apache/solr/search/numericrange/IntRangeQParserPluginTest.java
 
b/solr/core/src/test/org/apache/solr/search/numericrange/IntRangeQParserPluginTest.java
new file mode 100644
index 00000000000..40fb2e6396d
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/IntRangeQParserPluginTest.java
@@ -0,0 +1,499 @@
+/*
+ * 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 IntRangeQParserPlugin} */
+public class IntRangeQParserPluginTest extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig.xml", "schema-intrange.xml");
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    clearIndex();
+    assertU(commit());
+  }
+
+  @Test
+  public void test1DIntersectsQuery() {
+    // Index documents with 1D ranges
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(adoc("id", "2", "price_range", "[150 TO 250]"));
+    assertU(adoc("id", "3", "price_range", "[50 TO 80]"));
+    assertU(adoc("id", "4", "price_range", "[200 TO 300]"));
+    assertU(commit());
+
+    // Query: find ranges intersecting [120 TO 180]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[120 TO 
180]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']",
+        "//result/doc/str[@name='price_range'][.='[100 TO 200]']",
+        "//result/doc/str[@name='price_range'][.='[150 TO 250]']");
+
+    // Query: find ranges intersecting [0 TO 100]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[0 TO 
100]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='3']");
+
+    // Query: find ranges intersecting [175 TO 225]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[175 TO 
225]"),
+        "//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", "price_range", "[100 TO 200]"));
+    assertU(adoc("id", "2", "price_range", "[150 TO 250]"));
+    assertU(adoc("id", "3", "price_range", "[50 TO 80]"));
+    assertU(commit());
+
+    // Query: find ranges within [0 TO 300]
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=price_range}[0 TO 
300]"),
+        "//result[@numFound='3']");
+
+    // Query: find ranges within [100 TO 200]
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=price_range}[100 TO 
200]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+
+    // Query: find ranges within [0 TO 100]
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=price_range}[0 TO 
100]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='3']");
+  }
+
+  @Test
+  public void test1DContainsQuery() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(adoc("id", "2", "price_range", "[150 TO 250]"));
+    assertU(adoc("id", "3", "price_range", "[50 TO 300]"));
+    assertU(commit());
+
+    // Query: find ranges containing [160 TO 170]
+    assertQ(
+        req("q", "{!numericRange criteria=\"contains\" field=price_range}[160 
TO 170]"),
+        "//result[@numFound='3']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']",
+        "//result/doc/str[@name='id'][.='3']");
+
+    // Query: find ranges containing [0 TO 400]
+    assertQ(
+        req("q", "{!numericRange criteria=\"contains\" field=price_range}[0 TO 
400]"),
+        "//result[@numFound='0']");
+
+    // Query: find ranges containing [100 TO 200]
+    assertQ(
+        req("q", "{!numericRange criteria=\"contains\" field=price_range}[100 
TO 200]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='3']");
+  }
+
+  @Test
+  public void test1DCrossesQuery() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(adoc("id", "2", "price_range", "[150 TO 250]"));
+    assertU(adoc("id", "3", "price_range", "[50 TO 80]"));
+    assertU(adoc("id", "4", "price_range", "[120 TO 180]"));
+    assertU(commit());
+
+    // Query: find ranges crossing [150 TO 250]
+    // Should match ranges that intersect but are not within
+    assertQ(
+        req("q", "{!numericRange criteria=\"crosses\" field=price_range}[150 
TO 250]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='4']");
+  }
+
+  @Test
+  public void test2DIntersectsQuery() {
+    // Index documents with 2D ranges (bounding boxes)
+    assertU(adoc("id", "1", "bbox", "[0,0 TO 10,10]"));
+    assertU(adoc("id", "2", "bbox", "[5,5 TO 15,15]"));
+    assertU(adoc("id", "3", "bbox", "[20,20 TO 30,30]"));
+    assertU(commit());
+
+    // Query: find bboxes intersecting [8,8 TO 12,12]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=bbox}[8,8 TO 
12,12]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']",
+        "//result/doc/str[@name='bbox'][.='[0,0 TO 10,10]']",
+        "//result/doc/str[@name='bbox'][.='[5,5 TO 15,15]']");
+
+    // Query: find bboxes intersecting [25,25 TO 35,35]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=bbox}[25,25 TO 
35,35]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='3']");
+
+    // Query: find bboxes intersecting [100,100 TO 200,200]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=bbox}[100,100 TO 
200,200]"),
+        "//result[@numFound='0']");
+  }
+
+  @Test
+  public void test2DWithinQuery() {
+    assertU(adoc("id", "1", "bbox", "[5,5 TO 10,10]"));
+    assertU(adoc("id", "2", "bbox", "[0,0 TO 20,20]"));
+    assertU(adoc("id", "3", "bbox", "[15,15 TO 25,25]"));
+    assertU(commit());
+
+    // Query: find bboxes within [0,0 TO 20,20]
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=bbox}[0,0 TO 
20,20]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']");
+
+    // Query: find bboxes within [0,0 TO 30,30]
+    assertQ(
+        req("q", "{!numericRange criteria=\"within\" field=bbox}[0,0 TO 
30,30]"),
+        "//result[@numFound='3']");
+  }
+
+  @Test
+  public void test3DQuery() {
+    // Index documents with 3D ranges (bounding cubes)
+    assertU(adoc("id", "1", "cube", "[0,0,0 TO 10,10,10]"));
+    assertU(adoc("id", "2", "cube", "[5,5,5 TO 15,15,15]"));
+    assertU(commit());
+
+    // Query: find cubes intersecting [8,8,8 TO 12,12,12]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=cube}[8,8,8 TO 
12,12,12]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void test4DQuery() {
+    // Index documents with 4D ranges (tesseracts)
+    assertU(adoc("id", "1", "tesseract", "[0,0,0,0 TO 10,10,10,10]"));
+    assertU(adoc("id", "2", "tesseract", "[5,5,5,5 TO 15,15,15,15]"));
+    assertU(commit());
+
+    // Query: find tesseracts intersecting [8,8,8,8 TO 12,12,12,12]
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=tesseract}[8,8,8,8 
TO 12,12,12,12]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void testMultiValuedField() {
+    // Index document with multiple ranges
+    assertU(
+        adoc("id", "1", "price_range_multi", "[100 TO 200]", 
"price_range_multi", "[300 TO 400]"));
+    assertU(adoc("id", "2", "price_range_multi", "[150 TO 250]"));
+    assertU(commit());
+
+    // Query should match doc 1 via first range
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=price_range_multi}[110 TO 120]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/arr[@name='price_range_multi']/str[1][.='[100 TO 200]']",
+        "//result/doc/arr[@name='price_range_multi']/str[2][.='[300 TO 
400]']");
+
+    // Query should match doc 1 via second range
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=price_range_multi}[310 TO 320]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+
+    // Query should match both docs
+    assertQ(
+        req("q", "{!numericRange criteria=intersects 
field=price_range_multi}[150 TO 250]"),
+        "//result[@numFound='2']");
+  }
+
+  @Test
+  public void testMissingFieldParameter() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(commit());
+
+    // Query without field parameter should fail
+    assertQEx(
+        "Missing field parameter should fail",
+        "Missing required parameter: field",
+        req("q", "{!numericRange criteria=intersects}[100 TO 200]"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testMissingCriteriaParameter() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(commit());
+
+    // Query without field parameter should fail
+    assertQEx(
+        "Missing criteria parameter should fail",
+        "Missing required parameter: criteria",
+        req("q", "{!numericRange field=asdf}[100 TO 200]"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testInvalidFieldType() {
+    assertU(adoc("id", "1", "title", "test"));
+    assertU(commit());
+
+    // Query on non-IntRangeField should fail
+    assertQEx(
+        "Query on wrong field type should fail",
+        "must be of type IntRangeField",
+        req("q", "{!numericRange criteria=intersects field=title}[100 TO 
200]"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testInvalidQueryType() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(commit());
+
+    // Query with invalid criteria parameter should fail
+    assertQEx(
+        "Invalid query criteria should fail",
+        "Unknown query criteria",
+        req("q", "{!numericRange criteria=\"invalid\" field=price_range}[100 
TO 200]"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testInvalidRangeValue() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(commit());
+
+    // Query with invalid range format should fail
+    assertQEx(
+        "Invalid range format should fail",
+        "Invalid range",
+        req("q", "{!numericRange criteria=intersects 
field=price_range}invalid"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testEmptyRangeValue() {
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(commit());
+
+    // Query with empty range should fail
+    assertQEx(
+        "Empty range value should fail",
+        req("q", "{!numericRange criteria=intersects field=price_range}"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testNegativeValues() {
+    assertU(adoc("id", "1", "price_range", "[-100 TO -50]"));
+    assertU(adoc("id", "2", "price_range", "[-75 TO -25]"));
+    assertU(commit());
+
+    // Query with negative range
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[-80 TO 
-60]"),
+        "//result[@numFound='2']");
+  }
+
+  @Test
+  public void testExtremeValues() {
+    int min = Integer.MIN_VALUE;
+    int max = Integer.MAX_VALUE;
+
+    assertU(adoc("id", "1", "price_range", "[" + min + " TO " + max + "]"));
+    assertU(commit());
+
+    // Query should match the extreme range
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[0 TO 
100]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+  }
+
+  @Test
+  public void testPointRange() {
+    // Point range where min == max
+    assertU(adoc("id", "1", "price_range", "[100 TO 100]"));
+    assertU(commit());
+
+    // Intersects query with point
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[100 TO 
100]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+
+    // Intersects query containing point
+    assertQ(
+        req("q", "{!numericRange criteria=intersects field=price_range}[50 TO 
150]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+  }
+
+  // ------------------------------------------
+  // These tests don't use the IntRangeQParser, but rather the use of 
IntRangeField through the more
+  // standard query parser(s).  Not really the best place, but it didn't seem 
worth creating a whole
+  // new test class for this.  Maybe worth a refactor later. When used with 
parsers other than
+  // {!numericRange} we default to "contains" semantics.  In other words: 
"match only those
+  // documents with a field-val that fully contains the query value.
+
+  @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", "price_range", "[130 TO 160]")); // No match
+    assertU(adoc("id", "2", "price_range", "[100 TO 200]")); // Match!
+    assertU(adoc("id", "3", "price_range", "[150 TO 250]")); // No match
+    assertU(commit());
+
+    // Contains semantics: find indexed ranges that fully contain [120 TO 180]
+    assertQ(
+        req("q", "price_range:[120 TO 180]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void testGetFieldQueryFullRangeMultipleMatches() {
+    assertU(adoc("id", "1", "price_range", "[0 TO 1000]")); // Match!
+    assertU(adoc("id", "2", "price_range", "[100 TO 200]")); // Match!
+    assertU(adoc("id", "3", "price_range", "[100 TO 199]")); // No match - max 
too low
+    assertU(adoc("id", "4", "price_range", "[101 TO 200]")); // No match - min 
too high
+    assertU(commit());
+
+    assertQ(
+        req("q", "price_range:[100 TO 200]"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void testGetFieldQuerySingleBound() {
+    // Single-bound syntax: price_range:150 is sugar for contains([150 TO 150])
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]")); // Match!
+    assertU(adoc("id", "2", "price_range", "[150 TO 150]")); // Match!
+    assertU(adoc("id", "3", "price_range", "[100 TO 149]")); // No match - max 
below 150
+    assertU(adoc("id", "4", "price_range", "[151 TO 300]")); // No match - min 
above 150
+    assertU(commit());
+
+    assertQ(
+        req("q", "price_range:150"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void testGetFieldQuerySingleBound2D() {
+    // 2D single-bound: bbox:5,5 is sugar for contains([5,5 TO 5,5])
+    assertU(adoc("id", "1", "bbox", "[0,0 TO 10,10]")); // Match!
+    assertU(adoc("id", "2", "bbox", "[5,5 TO 5,5]")); // Match!
+    assertU(adoc("id", "3", "bbox", "[0,0 TO 4,10]")); // No match - X 
dimension ends too low
+    assertU(adoc("id", "4", "bbox", "[6,0 TO 10,10]")); // No match - X 
dimension starts too high
+    assertU(commit());
+
+    assertQ(
+        req("q", "bbox:5,5"),
+        "//result[@numFound='2']",
+        "//result/doc/str[@name='id'][.='1']",
+        "//result/doc/str[@name='id'][.='2']");
+  }
+
+  @Test
+  public void testGetFieldQueryFieldFormatting() {
+    // Test 1D field formatting
+    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    // Test 2D field formatting
+    assertU(adoc("id", "2", "bbox", "[10,20 TO 30,40]"));
+    // Test 3D field formatting
+    assertU(adoc("id", "3", "cube", "[5,10,15 TO 25,30,35]"));
+    // Test 4D field formatting
+    assertU(adoc("id", "4", "tesseract", "[1,2,3,4 TO 11,12,13,14]"));
+    // Test multi-valued field formatting
+    assertU(
+        adoc(
+            "id",
+            "5",
+            "price_range_multi",
+            "[50 TO 100]",
+            "price_range_multi",
+            "[200 TO 300]",
+            "price_range_multi",
+            "[400 TO 500]"));
+    assertU(commit());
+
+    // Verify 1D field returns correctly formatted value
+    assertQ(
+        req("q", "id:1"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='price_range'][.='[100 TO 200]']");
+
+    // Verify 2D field returns correctly formatted value
+    assertQ(
+        req("q", "id:2"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='bbox'][.='[10,20 TO 30,40]']");
+
+    // Verify 3D field returns correctly formatted value
+    assertQ(
+        req("q", "id:3"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='cube'][.='[5,10,15 TO 25,30,35]']");
+
+    // Verify 4D field returns correctly formatted value
+    assertQ(
+        req("q", "id:4"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='tesseract'][.='[1,2,3,4 TO 11,12,13,14]']");
+
+    // Verify multi-valued field returns all values correctly formatted
+    assertQ(
+        req("q", "id:5"),
+        "//result[@numFound='1']",
+        "//result/doc/arr[@name='price_range_multi']/str[1][.='[50 TO 100]']",
+        "//result/doc/arr[@name='price_range_multi']/str[2][.='[200 TO 300]']",
+        "//result/doc/arr[@name='price_range_multi']/str[3][.='[400 TO 
500]']");
+  }
+}
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 4eaed0e0475..d530a4993f4 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
@@ -18,7 +18,7 @@
 
 The following table lists the field types that are available in Solr and are 
recommended.
 The page further down, lists all the deprecated types for those migrating from 
older version of Solr.
-The 
{solr-javadocs}/core/org/apache/solr/schema/package-summary.html[`org.apache.solr.schema`]
 package includes all the classes listed in this table.
+The 
{solr-javadocs}/core/org/apache/solr/schema/package-summary.html[`org.apache.solr.schema`]
 package includes all the classes listed in this table, unless otherwise 
specified.
 
 == Recommended Field Types
 
@@ -51,6 +51,8 @@ The 
{solr-javadocs}/core/org/apache/solr/schema/package-summary.html[`org.apache
 
 |IntPointField |Integer field (32-bit signed integer). This class encodes int 
values using a "Dimensional Points" based data structure that allows for very 
efficient searches for specific values, or ranges of values. For single valued 
fields, `docValues="true"` must be used to enable sorting.
 
+|IntRangeField |Stores single or multi-dimensional ranges, using syntax like 
`[1 TO 4]` 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="intrange1D" class="solr.numericrange.Int 
[...]
+
 |LatLonPointSpatialField |A latitude/longitude coordinate pair; possibly 
multi-valued for multiple points. Usually it's specified as "lat,lon" order 
with a comma. See the section xref:query-guide:spatial-search.adoc[] for more 
information.
 
 |LongPointField |Long field (64-bit signed integer). This class encodes foo 
values using a "Dimensional Points" based data structure that allows for very 
efficient searches for specific values, or ranges of values. For single valued 
fields, `docValues="true"` must be used to enable sorting.
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 59f55acfedf..2bd6dac3e7a 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
@@ -1002,6 +1002,72 @@ These parameters would be defined in `solrconfig.xml`, 
in the `defaults` section
 
 For more information about the possibilities of nested queries, see Yonik 
Seeley's blog post 
https://lucidworks.com/2009/03/31/nested-queries-in-solr/[Nested Queries in 
Solr].
 
+== Numeric Range Query Parser
+
+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`) using a specified 
query-range.
+Multiple match semantics supported, see the `criteria` parameter below for 
more details.
+
+=== Numeric Range Parameters
+
+`field`::
++
+[%autowidth,frame=none]
+|===
+|Required |Default: none
+|===
++
+The field name to operate on.
+Must be a "range" field (e.g. `IntRangeField`)
+`criteria`::
++
+[%autowidth,frame=none]
+|===
+|Required |Default: none
+|===
++
+The criteria or semantics used to determine where a document matches the 
query-range.
+One of: `contains`, `within`, `crosses` or `intersects`.
+
+* `contains` if matching documents should entirely contain or enclose the 
query-range (i.e. doc-ranges that are a superset of query-range),
+* `within` if matching documents should be entirely within the query-range 
(i.e. doc-ranges that are sub-set of query-range)
+* `crosses` if matching documents should be partially **but not fully** 
contained within the query-range
+* `intersects` treats a document as a match if it meets any of the criteria 
above (i.e. `contains || within || crosses`)
+`v`::
++
+[%autowidth,frame=none]
+|===
+|Required |Default: none
+|===
++
+The query-range to be compared against field values.
+The specified range must have the same number of dimensions as the targeted 
field.
+Usually provided implicitly by specifying a range after the local-params block 
(see examples below).
+
+=== Numeric Range Examples
+
+Find products whose `price_range` is fully bounded between 100 and 120.
+
+[source,text]
+----
+{!numericRange field="price_range" criteria="within"}[100 TO 120]
+----
+
+Find products whose `price_range` contains 100.
+
+[source,text]
+----
+{!numericRange field="price_range" criteria="contains"}[100 TO 100]
+----
+
+Find products whose `price_range` overlaps at all with a user's desired price 
range (e.g. 100-120).
+
+[source,text]
+----
+{!numericRange field="price_range" criteria="intersects" v="[100 TO 120]"}
+----
+
 == Vector Query Parsers
 
 

Reply via email to