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 7052fc598b12e154c49630031fff52c926297c01
Author: Sonu Sharma <[email protected]>
AuthorDate: Fri Aug 7 17:24:04 2026 +0530

    SOLR-18201: Add docValue support for RangeField types (#4645)
    
    DocValues support in these field types can be used to make certain
    types of queries more efficient (i.e. when a selective filter and rangefield
    query are clauses in a larger boolean query).
    
    Currently sorting and faceting are NOT supported, but the docValues
    data can be used to support this down the road if use-cases arise.
    
    (cherry picked from commit 5e3a1a6a42e144b4515814076cb78f0a56dee648)
    (cherry picked from commit 686a74202e8b14f51e0c75fd11ae5667fc7f15d3)
---
 ...LR-18201-range-field-docvalues-optimization.yml |  10 +
 .../apache/solr/bench/search/RangeFieldSearch.java | 251 +++++++++++++++++
 .../configs/cloud-minimal/conf/schema.xml          |   9 +-
 .../src/java/org/apache/solr/schema/FieldType.java |  30 ++
 .../numericrange/AbstractNumericRangeField.java    | 151 ++++++++--
 .../solr/schema/numericrange/DoubleRangeField.java |  58 +++-
 .../solr/schema/numericrange/FloatRangeField.java  |  58 +++-
 .../solr/schema/numericrange/IntRangeField.java    |  58 +++-
 .../solr/schema/numericrange/LongRangeField.java   |  58 +++-
 .../MultiBinaryRangeDocValuesQuery.java            | 180 ++++++++++++
 .../numericrange/NumericRangeQParserPlugin.java    |   8 +-
 .../org/apache/solr/update/DocumentBuilder.java    |  36 +++
 .../solr/collection1/conf/schema-numericrange.xml  |  18 ++
 .../schema/numericrange/DoubleRangeFieldTest.java  |  44 +++
 .../schema/numericrange/IntRangeFieldTest.java     |  28 ++
 .../numericrange/NumericRangeDocValuesTest.java    | 310 +++++++++++++++++++++
 .../NumericRangeQParserPluginDoubleTest.java       | 118 ++++----
 .../NumericRangeQParserPluginFloatTest.java        | 118 ++++----
 .../NumericRangeQParserPluginIntTest.java          | 189 ++++++++-----
 .../NumericRangeQParserPluginLongTest.java         | 133 +++++----
 .../pages/field-types-included-with-solr.adoc      |   8 +-
 21 files changed, 1573 insertions(+), 300 deletions(-)

diff --git 
a/changelog/unreleased/SOLR-18201-range-field-docvalues-optimization.yml 
b/changelog/unreleased/SOLR-18201-range-field-docvalues-optimization.yml
new file mode 100644
index 00000000000..ab6aa7627bd
--- /dev/null
+++ b/changelog/unreleased/SOLR-18201-range-field-docvalues-optimization.yml
@@ -0,0 +1,10 @@
+title: >
+  Range field types (Int/Long/Float/DoubleRangeField) now support 
docValues="true"
+  as a query-time filter optimization.
+type: added
+authors:
+  - name: Sonu Sharma
+    nick: ercsonusharma
+links:
+  - name: SOLR-18201
+    url: https://issues.apache.org/jira/browse/SOLR-18201
diff --git 
a/solr/benchmark/src/java/org/apache/solr/bench/search/RangeFieldSearch.java 
b/solr/benchmark/src/java/org/apache/solr/bench/search/RangeFieldSearch.java
new file mode 100644
index 00000000000..5667a0ef5a2
--- /dev/null
+++ b/solr/benchmark/src/java/org/apache/solr/bench/search/RangeFieldSearch.java
@@ -0,0 +1,251 @@
+/*
+ * 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.bench.search;
+
+import static org.apache.solr.bench.BaseBenchState.log;
+import static org.apache.solr.bench.Docs.docs;
+import static org.apache.solr.bench.generators.SourceDSL.integers;
+import static org.apache.solr.bench.generators.SourceDSL.lists;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.solr.bench.CircularIterator;
+import org.apache.solr.bench.Docs;
+import org.apache.solr.bench.MiniClusterState;
+import org.apache.solr.bench.generators.SolrGen;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.response.FacetField;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks the docValues filter optimization for range field types.
+ *
+ * <p>Docs hold wide ranges {@code [m, m+SPAN]} in indexed-only (BKD) and 
indexed+docValues fields,
+ * single-valued ({@code r_ir}/{@code r_ir_dv}) and multiValued ({@code 
r_mv_ir}/{@code
+ * r_mv_ir_dv}). A central window matches ~half the docs, so the range clause 
is costly to
+ * materialize but cheap to verify per-doc. It is AND-ed with a selective term 
filter whose
+ * doc-frequency is controlled, so we know and the {@code [range-bench]} setup 
log confirms, which
+ * {@link org.apache.lucene.search.IndexOrDocValuesQuery} branch runs.
+ *
+ * <p>Params: {@code criteria} (the {@code {!numericRange criteria=...}} 
relation), {@code field}
+ * (the DV / non-DV pairs), and {@code scenario} - the lead's selectivity vs 
the {@code rangeCost/8}
+ * cutover: {@code selective_dv} (~{@value #SELECTIVE_LEAD_DF} docs -&gt; 
docValues branch), {@code
+ * broad_points} (~{@value #BROAD_LEAD_DF} docs -&gt; points branch), {@code 
none_points}
+ * (standalone -&gt; points).
+ */
+@Fork(value = 1)
+@Warmup(time = 5, iterations = 5)
+@Measurement(time = 5, iterations = 5)
+@Threads(value = 1)
+public class RangeFieldSearch {
+
+  static final String COLLECTION = "c1";
+
+  static final int NUM_DOCS = 1_000_000;
+
+  /** Width of each indexed document range: docs store {@code [m, m+SPAN]}. */
+  static final int SPAN = 500_000;
+
+  /**
+   * Narrow window for {@code contains}/{@code intersects}/{@code crosses}: 
contained by ~half the
+   * docs.
+   */
+  static final String OTHER_QUERY = "[500000 TO 500100]";
+
+  /**
+   * Wide window for {@code within} (wider than {@code SPAN}), so ~half the 
docs' ranges fit inside.
+   */
+  static final String WITHIN_QUERY = "[0 TO 1000000]";
+
+  /** "Selective" lead df: far below {@code rangeCost/8} (~62.5k) -&gt; 
docValues branch. */
+  static final int SELECTIVE_LEAD_DF = 2_000;
+
+  /** "Broad" lead df: above {@code rangeCost/8} but below {@code rangeCost} 
-&gt; points branch. */
+  static final int BROAD_LEAD_DF = 250_000;
+
+  @State(Scope.Benchmark)
+  public static class BenchState {
+
+    @Param({"contains", "intersects", "within", "crosses"})
+    public String criteria;
+
+    @Param({"r_ir", "r_ir_dv", "r_mv_ir", "r_mv_ir_dv"})
+    public String field;
+
+    @Param({"selective_dv", "broad_points", "none_points"})
+    public String scenario;
+
+    // Rotated so repeated queries are distinct top-level queries (each 
~SELECTIVE_LEAD_DF docs).
+    Iterator<String> selectiveTerms;
+    // Rotated broad-lead terms (each ~BROAD_LEAD_DF docs).
+    Iterator<String> broadTerms;
+
+    @Setup(Level.Trial)
+    public void setupTrial(MiniClusterState.MiniClusterBenchState 
miniClusterState)
+        throws Exception {
+      miniClusterState.setUseHttp1(true);
+      miniClusterState.startMiniCluster(1);
+      miniClusterState.createCollection(COLLECTION, 1, 1);
+
+      // Controlled lead selectivity: ~numDocs/DF distinct values -> each term 
matches ~DF docs.
+      SolrGen<String> selectiveLeadTerms =
+          integers().between(0, NUM_DOCS / SELECTIVE_LEAD_DF - 1).map(i -> "s" 
+ i, String.class);
+      SolrGen<String> broadLeadTerms =
+          integers().between(0, NUM_DOCS / BROAD_LEAD_DF - 1).map(i -> "b" + 
i, String.class);
+      // m in [0, NUM_DOCS] so that m + SPAN never overflows int; range string 
is "[m TO m+SPAN]".
+      SolrGen<String> rangeValues =
+          integers()
+              .between(0, NUM_DOCS)
+              .map(m -> "[" + m + " TO " + (m + SPAN) + "]", String.class);
+      // multiValued docs hold 2-3 ranges each, drawn from the same 
distribution.
+      var mvRangeValues = lists().of(rangeValues).ofSizeBetween(2, 3);
+
+      Docs docs =
+          docs()
+              .field("id", integers().incrementing())
+              .field("term_high_s", selectiveLeadTerms)
+              .field("term_low_s", broadLeadTerms)
+              .field("r_ir", rangeValues) // single-valued, indexed-only (BKD)
+              .field("r_ir_dv", rangeValues) // single-valued, indexed + 
docValues
+              .field("r_mv_ir", mvRangeValues) // multiValued, indexed-only 
(BKD)
+              .field("r_mv_ir_dv", mvRangeValues); // multiValued, indexed + 
docValues
+
+      miniClusterState.index(COLLECTION, docs, NUM_DOCS, false);
+
+      // Discover the real term values (and their counts) so queries hit 
existing terms.
+      String basePath = miniClusterState.nodes.get(0);
+      SolrQuery q = new SolrQuery("*:*");
+      q.setParam("facet", "true");
+      q.setParam("rows", "0");
+      q.setParam("facet.field", "term_high_s", "term_low_s");
+      q.setParam("facet.limit", "2000");
+      q.setParam("facet.mincount", "1");
+      QueryResponse response =
+          new QueryRequest(q).processWithBaseUrl(miniClusterState.client, 
basePath, COLLECTION);
+      selectiveTerms = new CircularIterator<>(facetValues(response, 
"term_high_s"));
+      broadTerms = new CircularIterator<>(facetValues(response, "term_low_s"));
+
+      // Measure rangeCost (the range clause's own match count) and derive the 
branch the same way
+      // IndexOrDocValuesQuery does: rangeCost >>> 3 <= leadCost -> points, 
else docValues. leadCost
+      // is
+      // the actual doc-frequency of the term this scenario leads with.
+      String window = criteria.equals("within") ? WITHIN_QUERY : OTHER_QUERY;
+      SolrQuery rangeOnly =
+          new SolrQuery(
+              "q", "{!numericRange criteria=" + criteria + " field=" + field + 
"}" + window);
+      rangeOnly.setParam("rows", "0");
+      long rangeCost =
+          new QueryRequest(rangeOnly)
+              .processWithBaseUrl(miniClusterState.client, basePath, 
COLLECTION)
+              .getResults()
+              .getNumFound();
+      long threshold = rangeCost >>> 3;
+      boolean hasDocValues = field.endsWith("_dv");
+      long leadCost;
+      long dvBranch; // 1 = docValues verify branch, 0 = points/BKD
+      if (scenario.startsWith("none")) {
+        leadCost = 0; // standalone: no lead -> points
+        dvBranch = 0;
+      } else {
+        String facetField = scenario.startsWith("selective") ? "term_high_s" : 
"term_low_s";
+        leadCost = firstFacetCount(response, facetField);
+        // IndexOrDocValuesQuery picks docValues only when the range is >8x 
the lead (threshold >
+        // leadCost); otherwise points. A docValues=false field never gets 
that choice.
+        dvBranch = (hasDocValues && threshold > leadCost) ? 1 : 0;
+      }
+      log(
+          "[range-bench] criteria="
+              + criteria
+              + " field="
+              + field
+              + " scenario="
+              + scenario
+              + " rangeCost="
+              + rangeCost
+              + " threshold="
+              + threshold
+              + " leadCost="
+              + leadCost
+              + " -> "
+              + (dvBranch == 1 ? "docValues" : "points"));
+    }
+
+    private Set<String> facetValues(QueryResponse response, String fieldName) {
+      return response.getFacetField(fieldName).getValues().stream()
+          .map(FacetField.Count::getName)
+          .collect(Collectors.toSet());
+    }
+
+    private long firstFacetCount(QueryResponse response, String fieldName) {
+      return response.getFacetField(fieldName).getValues().stream()
+          .mapToLong(FacetField.Count::getCount)
+          .findFirst()
+          .orElse(0L);
+    }
+
+    @Setup(Level.Iteration)
+    public void setupIteration(MiniClusterState.MiniClusterBenchState 
miniClusterState)
+        throws SolrServerException, IOException {
+      CollectionAdminRequest.Reload reload = 
CollectionAdminRequest.reloadCollection(COLLECTION);
+      
miniClusterState.client.requestWithBaseUrl(miniClusterState.nodes.get(0), 
reload, null);
+    }
+
+    /** Builds the query for the current (criteria, field, scenario) parameter 
combination. */
+    QueryRequest query() {
+      // within needs a window WIDER than the doc ranges (so they can fit 
inside it); the other
+      // relations need a narrow window that the doc ranges contain / overlap.
+      String window = criteria.equals("within") ? WITHIN_QUERY : OTHER_QUERY;
+      String rangeExpr = "{!numericRange criteria=" + criteria + " field=" + 
field + "}" + window;
+      if (scenario.startsWith("none")) {
+        return new QueryRequest(new SolrQuery("q", rangeExpr)); // standalone: 
no lead -> points
+      }
+
+      boolean selective = scenario.startsWith("selective");
+      Iterator<String> terms = selective ? selectiveTerms : broadTerms;
+      String termField = selective ? "term_high_s" : "term_low_s";
+      // Nest the {!numericRange} subquery via _query_ so it joins the term 
clause's BooleanQuery.
+      String query = "+" + termField + ":\"" + terms.next() + "\" +_query_:\"" 
+ rangeExpr + "\"";
+      return new QueryRequest(new SolrQuery("q", query));
+    }
+  }
+
+  @Benchmark
+  public void rangeQuery(
+      Blackhole blackhole,
+      BenchState benchState,
+      MiniClusterState.MiniClusterBenchState miniClusterState)
+      throws SolrServerException, IOException {
+    // Sink the result exactly once, via Blackhole
+    blackhole.consume(benchState.query().process(miniClusterState.client, 
COLLECTION));
+  }
+}
diff --git a/solr/benchmark/src/resources/configs/cloud-minimal/conf/schema.xml 
b/solr/benchmark/src/resources/configs/cloud-minimal/conf/schema.xml
index e3f77a82604..79b26fc9b3c 100644
--- a/solr/benchmark/src/resources/configs/cloud-minimal/conf/schema.xml
+++ b/solr/benchmark/src/resources/configs/cloud-minimal/conf/schema.xml
@@ -28,6 +28,7 @@
                positionIncrementGap="0"/>
     <fieldType name="date" class="org.apache.solr.schema.DatePointField" 
docValues="false" omitNorms="true"
                positionIncrementGap="0"/>
+    <fieldType name="intrange" class="solr.IntRangeField" numDimensions="1"/>
     <fieldType name="text" class="solr.TextField">
         <analyzer>
             <tokenizer class="solr.StandardTokenizerFactory"/>
@@ -58,5 +59,11 @@
     <dynamicField name="*_dt_dv" type="date" indexed="true" docValues="true" 
stored="false"/>
     <dynamicField name="*_s_dv" type="string" indexed="true" docValues="true" 
stored="false"/>
 
-    <uniqueKey>id</uniqueKey>
+    <!-- Range fields: *_ir is indexed-only (BKD); *_ir_dv adds docValues 
(IndexOrDocValuesQuery). -->
+    <dynamicField name="*_ir" type="intrange" indexed="true" stored="false"/>
+    <dynamicField name="*_ir_dv" type="intrange" indexed="true" 
docValues="true" stored="false"/>
+    <dynamicField name="*_mv_ir" type="intrange" indexed="true" 
multiValued="true" stored="false"/>
+    <dynamicField name="*_mv_ir_dv" type="intrange" indexed="true" 
multiValued="true" docValues="true" stored="false"/>
+
+  <uniqueKey>id</uniqueKey>
 </schema>
diff --git a/solr/core/src/java/org/apache/solr/schema/FieldType.java 
b/solr/core/src/java/org/apache/solr/schema/FieldType.java
index 0017dd06613..cf9f2d96fc5 100644
--- a/solr/core/src/java/org/apache/solr/schema/FieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/FieldType.java
@@ -352,6 +352,36 @@ public abstract class FieldType extends FieldProperties {
     return f == null ? Collections.<IndexableField>emptyList() : 
Collections.singletonList(f);
   }
 
+  /**
+   * Whether this type needs all values of a multiValued field together 
(rather than one value at a
+   * time) in order to build its indexable fields - for example to pack 
several ranges into a single
+   * {@code BinaryDocValues} blob. When {@code true}, {@link 
org.apache.solr.update.DocumentBuilder}
+   * calls {@link #createFieldsFromAllValues(SchemaField, Collection)} once 
per (multiValued) field
+   * instead of {@link #createFields(SchemaField, Object)} per value.
+   */
+  public boolean shouldCreateFieldsFromAllValues() {
+    return false;
+  }
+
+  /**
+   * Builds the indexable fields for <em>all</em> values of {@code field} in a 
single document at
+   * once. Only invoked when {@link #shouldCreateFieldsFromAllValues()} 
returns {@code true}; the
+   * default simply concatenates {@link #createFields(SchemaField, Object)} 
over each value,
+   * preserving per-value behavior.
+   *
+   * @param field the schema field
+   * @param values all (non-null) values for this field in the current document
+   * @return the indexable fields to add to the document
+   */
+  public List<IndexableField> createFieldsFromAllValues(
+      SchemaField field, Collection<Object> values) {
+    List<IndexableField> fields = new ArrayList<>();
+    for (Object value : values) {
+      fields.addAll(createFields(field, value));
+    }
+    return fields;
+  }
+
   /**
    * Convert an external value (from XML update command or from query string) 
into the internal
    * format for both storing and indexing (which can be modified by any 
analyzers).
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 a05a766ac52..d8f39f46bee 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
@@ -18,13 +18,20 @@ package org.apache.solr.schema.numericrange;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
+import org.apache.lucene.document.BinaryDocValuesField;
+import org.apache.lucene.document.RangeFieldQuery.QueryType;
 import org.apache.lucene.document.StoredField;
 import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.IndexOrDocValuesQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.SortField;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.response.TextResponseWriter;
@@ -134,7 +141,19 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
 
   @Override
   protected boolean enableDocValuesByDefault() {
-    return false; // Range fields do not support docValues
+    // DocValues are supported for both single and multiValued range fields, 
enabled by default.
+    return true;
+  }
+
+  @Override
+  protected void setArgs(IndexSchema schema, Map<String, String> args) {
+    // A range field's docValues are an opaque packed BINARY blob not the 
external "[min TO max]"
+    // form, so they can't stand in for the stored value: useDocValuesAsStored 
would make fl=*
+    // return that raw blob. It otherwise defaults to off here and reject an 
explicit
+    // useDocValuesAsStored="true" rather than silently ignoring what was 
requested.
+    args.putIfAbsent("useDocValuesAsStored", "false");
+    super.setArgs(schema, args);
+    restrictProps(USE_DOCVALUES_AS_STORED);
   }
 
   @Override
@@ -153,27 +172,33 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
                 + typeName);
       }
     }
+  }
 
-    // Range fields do not support docValues - validate this wasn't explicitly 
enabled
-    if (hasProperty(DOC_VALUES)) {
-      throw new SolrException(
-          ErrorCode.SERVER_ERROR,
-          "docValues=true enabled but "
-              + getClass().getSimpleName()
-              + " does not support docValues for field type "
-              + typeName);
-    }
+  @Override
+  protected void checkSupportsDocValues() {
+    // DocValues are supported for both single and multiValued range fields 
(backed by binary
+    // docValues).
   }
 
   @Override
   public List<IndexableField> createFields(SchemaField field, Object value) {
-    IndexableField indexedField = createField(field, value);
     List<IndexableField> fields = new ArrayList<>();
 
+    IndexableField indexedField = createField(field, value);
     if (indexedField != null) {
       fields.add(indexedField);
     }
 
+    if (field.hasDocValues() && !field.multiValued()) {
+      // Single-valued: one flat BinaryDocValues blob (read directly, no 
dictionary). multiValued
+      // docValues are built from all values at once in 
createFieldsFromAllValues (one blob holding
+      // every range), since BinaryDocValues holds only one value per document.
+      NumericRangeValue rv = parseRangeValue(value.toString());
+      fields.add(
+          new BinaryDocValuesField(
+              field.getName(), 
BytesRef.deepCopyOf(encodePackedValue(field.getName(), rv))));
+    }
+
     if (field.stored()) {
       fields.add(getStoredField(field, value.toString()));
     }
@@ -181,6 +206,97 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
     return fields;
   }
 
+  @Override
+  public boolean shouldCreateFieldsFromAllValues() {
+    // multiValued docValues range fields pack every range of a document into 
ONE BinaryDocValues
+    // blob (flat, no dictionary), so the type needs all values together to 
build it.
+    return true;
+  }
+
+  @Override
+  public List<IndexableField> createFieldsFromAllValues(
+      SchemaField field, Collection<Object> values) {
+    List<IndexableField> fields = new ArrayList<>();
+    // Indexed (BKD) and stored fields: one per value (same as the per-value 
path).
+    for (Object value : values) {
+      IndexableField indexedField = createField(field, value);
+      if (indexedField != null) {
+        fields.add(indexedField);
+      }
+      if (field.stored()) {
+        fields.add(getStoredField(field, value.toString()));
+      }
+    }
+    // docValues: a single BinaryDocValues value holding all of the document's 
ranges.
+    if (field.hasDocValues()) {
+      fields.add(
+          new BinaryDocValuesField(field.getName(), 
encodePackedValues(field.getName(), values)));
+    }
+    return fields;
+  }
+
+  /**
+   * Encodes several ranges into one {@code BinaryDocValues} blob: each 
range's fixed-width {@code
+   * [min... | max...]} bytes concatenated. All ranges of a field share the 
same width, so the query
+   * recovers the count as {@code blob.length / stride}.
+   *
+   * <p><b>Ordering invariant:</b> ranges are packed in ascending 
unsigned-byte order of their
+   * encoding (min-first, so numeric ascending; 1D = ascending by min). It 
also makes the encoding
+   * canonical and lets a future query short-circuit a doc's ranges.
+   */
+  protected BytesRef encodePackedValues(String field, Collection<Object> 
values) {
+    // Encode each range into its own fixed-width chunk so the ranges can be 
ordered before packing.
+    List<byte[]> encoded = new ArrayList<>(values.size());
+    for (Object value : values) {
+      BytesRef ref = encodePackedValue(field, 
parseRangeValue(value.toString()));
+      encoded.add(Arrays.copyOfRange(ref.bytes, ref.offset, ref.offset + 
ref.length));
+    }
+    encoded.sort(Arrays::compareUnsigned);
+    BytesRefBuilder builder = new BytesRefBuilder();
+    for (byte[] chunk : encoded) {
+      builder.append(chunk, 0, chunk.length);
+    }
+    return builder.toBytesRef();
+  }
+
+  /**
+   * Encodes a range value into the packed {@code [min... | max...]} byte 
representation used by
+   * both the indexed docValues and the query. Reuses Lucene's own encoder 
(via the type-specific
+   * {@code *RangeDocValuesField}) so the docValues and BKD encodings stay 
identical.
+   */
+  protected abstract BytesRef encodePackedValue(String field, 
NumericRangeValue rangeValue);
+
+  /** Number of bytes per dimension value for this type (e.g. {@code 
Integer.BYTES}). */
+  protected abstract int bytesPerDimension();
+
+  /**
+   * If the field has docValues, wraps the BKD query in an {@link 
IndexOrDocValuesQuery} whose
+   * docValues clause ({@link MultiBinaryRangeDocValuesQuery}) can cheaply 
verify candidates when a
+   * more selective clause leads iteration; otherwise returns the BKD query 
unchanged. It reads a
+   * flat per-doc blob of one (single-valued) or several (multiValued) ranges, 
avoiding the
+   * dictionary/ordinal overhead of SortedSet docValues.
+   */
+  protected Query maybeWrapWithDocValues(
+      SchemaField field, QueryType type, NumericRangeValue rangeValue, Query 
bkdQuery) {
+    if (!field.hasDocValues()) {
+      return bkdQuery;
+    }
+    BytesRef packed = encodePackedValue(field.getName(), rangeValue);
+    byte[] queryPackedValue =
+        Arrays.copyOfRange(packed.bytes, packed.offset, packed.offset + 
packed.length);
+    Query dv =
+        new MultiBinaryRangeDocValuesQuery(
+            field.getName(),
+            queryPackedValue,
+            rangeValue.getDimensions(),
+            bytesPerDimension(),
+            type);
+    if (!field.indexed()) {
+      return dv;
+    }
+    return new IndexOrDocValuesQuery(bkdQuery, dv);
+  }
+
   protected StoredField getStoredField(SchemaField sf, Object value) {
     return new StoredField(sf.getName(), value.toString());
   }
@@ -278,7 +394,7 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
    * @param rangeValue a pre-parsed range value produced by this field type
    * @return a contains query for the given field and range
    */
-  public abstract Query newContainsQuery(String field, NumericRangeValue 
rangeValue);
+  public abstract Query newContainsQuery(SchemaField field, NumericRangeValue 
rangeValue);
 
   /**
    * Creates a Lucene query that matches indexed documents whose stored range 
<em>intersects</em>
@@ -288,7 +404,7 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
    * @param rangeValue a pre-parsed range value produced by this field type
    * @return an intersects query for the given field and range
    */
-  public abstract Query newIntersectsQuery(String field, NumericRangeValue 
rangeValue);
+  public abstract Query newIntersectsQuery(SchemaField field, 
NumericRangeValue rangeValue);
 
   /**
    * Creates a Lucene query that matches indexed documents whose stored range 
is <em>within</em> the
@@ -298,7 +414,7 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
    * @param rangeValue a pre-parsed range value produced by this field type
    * @return a within query for the given field and range
    */
-  public abstract Query newWithinQuery(String field, NumericRangeValue 
rangeValue);
+  public abstract Query newWithinQuery(SchemaField field, NumericRangeValue 
rangeValue);
 
   /**
    * Creates a Lucene query that matches indexed documents whose stored range 
<em>crosses</em> the
@@ -308,7 +424,7 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
    * @param rangeValue a pre-parsed range value produced by this field type
    * @return a crosses query for the given field and range
    */
-  public abstract Query newCrossesQuery(String field, NumericRangeValue 
rangeValue);
+  public abstract Query newCrossesQuery(SchemaField field, NumericRangeValue 
rangeValue);
 
   /**
    * Creates a query for this field that matches docs where the query-range is 
fully contained by
@@ -336,7 +452,7 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
     // Check if it's the full range syntax: [min1,min2 TO max1,max2]
     if (getRangePattern().matcher(trimmed).matches()) {
       final var rangeValue = parseRangeValue(trimmed);
-      return newContainsQuery(field.getName(), rangeValue);
+      return newContainsQuery(field, rangeValue);
     }
 
     // Syntax sugar: also accept a single-bound (i.e pX,pY,pZ)
@@ -353,7 +469,8 @@ public abstract class AbstractNumericRangeField extends 
PrimitiveFieldType {
                 + ")");
       }
 
-      return newContainsQuery(field.getName(), singleBoundRange);
+      // A single bound is the degenerate range [p,p].
+      return newContainsQuery(field, singleBoundRange);
     }
 
     throw new SolrException(
diff --git 
a/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java 
b/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java
index c7eaeb5a82b..5b98d7139fb 100644
--- 
a/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java
+++ 
b/solr/core/src/java/org/apache/solr/schema/numericrange/DoubleRangeField.java
@@ -19,8 +19,11 @@ package org.apache.solr.schema.numericrange;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.lucene.document.DoubleRange;
+import org.apache.lucene.document.DoubleRangeDocValuesField;
+import org.apache.lucene.document.RangeFieldQuery.QueryType;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.util.BytesRef;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.schema.SchemaField;
@@ -101,6 +104,17 @@ public class DoubleRangeField extends 
AbstractNumericRangeField {
     return new DoubleRange(field.getName(), rangeValue.mins, rangeValue.maxs);
   }
 
+  @Override
+  protected BytesRef encodePackedValue(String field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return new DoubleRangeDocValuesField(field, rv.mins, 
rv.maxs).binaryValue();
+  }
+
+  @Override
+  protected int bytesPerDimension() {
+    return Double.BYTES;
+  }
+
   /**
    * Parse a range value string into a RangeValue object.
    *
@@ -196,27 +210,43 @@ public class DoubleRangeField extends 
AbstractNumericRangeField {
   }
 
   @Override
-  public Query newContainsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return DoubleRange.newContainsQuery(fieldName, rv.mins, rv.maxs);
+  public Query newContainsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CONTAINS,
+        rangeValue,
+        DoubleRange.newContainsQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newIntersectsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return DoubleRange.newIntersectsQuery(fieldName, rv.mins, rv.maxs);
+  public Query newIntersectsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.INTERSECTS,
+        rangeValue,
+        DoubleRange.newIntersectsQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newWithinQuery(String fieldName, NumericRangeValue rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return DoubleRange.newWithinQuery(fieldName, rv.mins, rv.maxs);
+  public Query newWithinQuery(SchemaField field, NumericRangeValue rangeValue) 
{
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.WITHIN,
+        rangeValue,
+        DoubleRange.newWithinQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue) 
{
-    final var rv = (RangeValue) rangeValue;
-    return DoubleRange.newCrossesQuery(fieldName, rv.mins, rv.maxs);
+  public Query newCrossesQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CROSSES,
+        rangeValue,
+        DoubleRange.newCrossesQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
@@ -261,7 +291,9 @@ public class DoubleRangeField extends 
AbstractNumericRangeField {
     if (numDimensions == 1) {
       mins[0] = min;
       maxs[0] = max;
-      return DoubleRange.newContainsQuery(field.getName(), mins, maxs);
+      // Route through newContainsQuery so the standard field:[X TO Y] syntax 
also picks up the
+      // docValues optimization when the field has docValues.
+      return newContainsQuery(field, new RangeValue(mins, maxs));
     } else {
       throw new SolrException(
           ErrorCode.BAD_REQUEST,
diff --git 
a/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java 
b/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java
index 0d4e4f31d58..edae9f93dd9 100644
--- 
a/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java
+++ 
b/solr/core/src/java/org/apache/solr/schema/numericrange/FloatRangeField.java
@@ -19,8 +19,11 @@ 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.document.FloatRangeDocValuesField;
+import org.apache.lucene.document.RangeFieldQuery.QueryType;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.util.BytesRef;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.schema.SchemaField;
@@ -101,6 +104,17 @@ public class FloatRangeField extends 
AbstractNumericRangeField {
     return new FloatRange(field.getName(), rangeValue.mins, rangeValue.maxs);
   }
 
+  @Override
+  protected BytesRef encodePackedValue(String field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return new FloatRangeDocValuesField(field, rv.mins, rv.maxs).binaryValue();
+  }
+
+  @Override
+  protected int bytesPerDimension() {
+    return Float.BYTES;
+  }
+
   /**
    * Parse a range value string into a RangeValue object.
    *
@@ -196,27 +210,43 @@ public class FloatRangeField extends 
AbstractNumericRangeField {
   }
 
   @Override
-  public Query newContainsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return FloatRange.newContainsQuery(fieldName, rv.mins, rv.maxs);
+  public Query newContainsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CONTAINS,
+        rangeValue,
+        FloatRange.newContainsQuery(field.getName(), 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);
+  public Query newIntersectsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.INTERSECTS,
+        rangeValue,
+        FloatRange.newIntersectsQuery(field.getName(), 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);
+  public Query newWithinQuery(SchemaField field, NumericRangeValue rangeValue) 
{
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.WITHIN,
+        rangeValue,
+        FloatRange.newWithinQuery(field.getName(), 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);
+  public Query newCrossesQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CROSSES,
+        rangeValue,
+        FloatRange.newCrossesQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
@@ -261,7 +291,9 @@ public class FloatRangeField extends 
AbstractNumericRangeField {
     if (numDimensions == 1) {
       mins[0] = min;
       maxs[0] = max;
-      return FloatRange.newContainsQuery(field.getName(), mins, maxs);
+      // Route through newContainsQuery so the standard field:[X TO Y] syntax 
also picks up the
+      // docValues optimization when the field has docValues.
+      return newContainsQuery(field, new RangeValue(mins, maxs));
     } else {
       throw new SolrException(
           ErrorCode.BAD_REQUEST,
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
index b8e76d0a680..b2faec3534e 100644
--- a/solr/core/src/java/org/apache/solr/schema/numericrange/IntRangeField.java
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/IntRangeField.java
@@ -18,8 +18,11 @@ package org.apache.solr.schema.numericrange;
 
 import java.util.regex.Matcher;
 import org.apache.lucene.document.IntRange;
+import org.apache.lucene.document.IntRangeDocValuesField;
+import org.apache.lucene.document.RangeFieldQuery.QueryType;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.util.BytesRef;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.schema.SchemaField;
@@ -89,6 +92,17 @@ public class IntRangeField extends AbstractNumericRangeField 
{
     return new IntRange(field.getName(), rangeValue.mins, rangeValue.maxs);
   }
 
+  @Override
+  protected BytesRef encodePackedValue(String field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return new IntRangeDocValuesField(field, rv.mins, rv.maxs).binaryValue();
+  }
+
+  @Override
+  protected int bytesPerDimension() {
+    return Integer.BYTES;
+  }
+
   /**
    * Parse a range value string into a RangeValue object.
    *
@@ -184,27 +198,43 @@ public class IntRangeField extends 
AbstractNumericRangeField {
   }
 
   @Override
-  public Query newContainsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rangeValueTyped = (RangeValue) rangeValue;
-    return IntRange.newContainsQuery(fieldName, rangeValueTyped.mins, 
rangeValueTyped.maxs);
+  public Query newContainsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CONTAINS,
+        rangeValue,
+        IntRange.newContainsQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newIntersectsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return IntRange.newIntersectsQuery(fieldName, rv.mins, rv.maxs);
+  public Query newIntersectsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.INTERSECTS,
+        rangeValue,
+        IntRange.newIntersectsQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newWithinQuery(String fieldName, NumericRangeValue rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return IntRange.newWithinQuery(fieldName, rv.mins, rv.maxs);
+  public Query newWithinQuery(SchemaField field, NumericRangeValue rangeValue) 
{
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.WITHIN,
+        rangeValue,
+        IntRange.newWithinQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue) 
{
-    final var rv = (RangeValue) rangeValue;
-    return IntRange.newCrossesQuery(fieldName, rv.mins, rv.maxs);
+  public Query newCrossesQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CROSSES,
+        rangeValue,
+        IntRange.newCrossesQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
@@ -248,7 +278,9 @@ public class IntRangeField extends 
AbstractNumericRangeField {
     if (numDimensions == 1) {
       mins[0] = min;
       maxs[0] = max;
-      return IntRange.newContainsQuery(field.getName(), mins, maxs);
+      // Route through newContainsQuery so the standard field:[X TO Y] syntax 
also picks up the
+      // docValues optimization when the field has docValues.
+      return newContainsQuery(field, new RangeValue(mins, maxs));
     } else {
       throw new SolrException(
           ErrorCode.BAD_REQUEST,
diff --git 
a/solr/core/src/java/org/apache/solr/schema/numericrange/LongRangeField.java 
b/solr/core/src/java/org/apache/solr/schema/numericrange/LongRangeField.java
index 5004f3affb3..6057a9f81ef 100644
--- a/solr/core/src/java/org/apache/solr/schema/numericrange/LongRangeField.java
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/LongRangeField.java
@@ -18,8 +18,11 @@ package org.apache.solr.schema.numericrange;
 
 import java.util.regex.Matcher;
 import org.apache.lucene.document.LongRange;
+import org.apache.lucene.document.LongRangeDocValuesField;
+import org.apache.lucene.document.RangeFieldQuery.QueryType;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.search.Query;
+import org.apache.lucene.util.BytesRef;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.schema.SchemaField;
@@ -90,6 +93,17 @@ public class LongRangeField extends 
AbstractNumericRangeField {
     return new LongRange(field.getName(), rangeValue.mins, rangeValue.maxs);
   }
 
+  @Override
+  protected BytesRef encodePackedValue(String field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return new LongRangeDocValuesField(field, rv.mins, rv.maxs).binaryValue();
+  }
+
+  @Override
+  protected int bytesPerDimension() {
+    return Long.BYTES;
+  }
+
   /**
    * Parse a range value string into a RangeValue object.
    *
@@ -185,27 +199,43 @@ public class LongRangeField extends 
AbstractNumericRangeField {
   }
 
   @Override
-  public Query newContainsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rangeValueTyped = (RangeValue) rangeValue;
-    return LongRange.newContainsQuery(fieldName, rangeValueTyped.mins, 
rangeValueTyped.maxs);
+  public Query newContainsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CONTAINS,
+        rangeValue,
+        LongRange.newContainsQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newIntersectsQuery(String fieldName, NumericRangeValue 
rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return LongRange.newIntersectsQuery(fieldName, rv.mins, rv.maxs);
+  public Query newIntersectsQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.INTERSECTS,
+        rangeValue,
+        LongRange.newIntersectsQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newWithinQuery(String fieldName, NumericRangeValue rangeValue) {
-    final var rv = (RangeValue) rangeValue;
-    return LongRange.newWithinQuery(fieldName, rv.mins, rv.maxs);
+  public Query newWithinQuery(SchemaField field, NumericRangeValue rangeValue) 
{
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.WITHIN,
+        rangeValue,
+        LongRange.newWithinQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
-  public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue) 
{
-    final var rv = (RangeValue) rangeValue;
-    return LongRange.newCrossesQuery(fieldName, rv.mins, rv.maxs);
+  public Query newCrossesQuery(SchemaField field, NumericRangeValue 
rangeValue) {
+    RangeValue rv = (RangeValue) rangeValue;
+    return maybeWrapWithDocValues(
+        field,
+        QueryType.CROSSES,
+        rangeValue,
+        LongRange.newCrossesQuery(field.getName(), rv.mins, rv.maxs));
   }
 
   @Override
@@ -249,7 +279,9 @@ public class LongRangeField extends 
AbstractNumericRangeField {
     if (numDimensions == 1) {
       mins[0] = min;
       maxs[0] = max;
-      return LongRange.newContainsQuery(field.getName(), mins, maxs);
+      // Route through newContainsQuery so the standard field:[X TO Y] syntax 
also picks up the
+      // docValues optimization when the field has docValues.
+      return newContainsQuery(field, new RangeValue(mins, maxs));
     } else {
       throw new SolrException(
           ErrorCode.BAD_REQUEST,
diff --git 
a/solr/core/src/java/org/apache/solr/schema/numericrange/MultiBinaryRangeDocValuesQuery.java
 
b/solr/core/src/java/org/apache/solr/schema/numericrange/MultiBinaryRangeDocValuesQuery.java
new file mode 100644
index 00000000000..d23caa6fa5c
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/schema/numericrange/MultiBinaryRangeDocValuesQuery.java
@@ -0,0 +1,180 @@
+/*
+ * 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.Arrays;
+import java.util.Objects;
+import org.apache.lucene.document.RangeFieldQuery;
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.ConstantScoreScorer;
+import org.apache.lucene.search.ConstantScoreWeight;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
+import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.TwoPhaseIterator;
+import org.apache.lucene.search.Weight;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.ArrayUtil.ByteArrayComparator;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * A docValues-backed range query over {@link BinaryDocValues} for numeric 
range field types
+ * (single- or multiValued).
+ *
+ * <p>A document's range(s) are packed into a single {@code BinaryDocValues} 
value as one or more
+ * concatenated fixed-width {@code [min... | max...]} blobs (see {@code
+ * AbstractNumericRangeField.encodePackedValues}). Because every range for a 
field is the same width
+ * ({@code stride = 2 * numDims * bytesPerDim}), the count is recovered as 
{@code value.length /
+ * stride}.
+ *
+ * <p>Reading the flat per-doc blob avoids the dictionary/ordinal dereference 
a SortedSet-backed
+ * docValues range query would pay, so it stays cheap even for 
high-cardinality range data. It is a
+ * candidate to push down to Lucene, which currently lacks a multi-valued 
binary range docValues
+ * field.
+ *
+ * <p>The packed ranges are stored in ascending unsigned-byte (i.e. min-first, 
numeric) order. see
+ * the ordering invariant on {@code 
AbstractNumericRangeField.encodePackedValues}.
+ */
+final class MultiBinaryRangeDocValuesQuery extends Query {
+
+  private final String field;
+  private final byte[] queryPackedValue;
+  private final int numDims;
+  private final int bytesPerDim;
+  private final RangeFieldQuery.QueryType queryType;
+  private final ByteArrayComparator comparator;
+
+  /**
+   * @param field the field name
+   * @param queryPackedValue the query range, encoded as {@code [min... | 
max...]} the same way the
+   *     indexed ranges are
+   * @param numDims number of dimensions (1-4)
+   * @param bytesPerDim bytes per dimension value (e.g. {@code Integer.BYTES})
+   * @param queryType the relation to test ({@code INTERSECTS}, {@code 
CONTAINS}, {@code WITHIN},
+   *     {@code CROSSES})
+   */
+  MultiBinaryRangeDocValuesQuery(
+      String field,
+      byte[] queryPackedValue,
+      int numDims,
+      int bytesPerDim,
+      RangeFieldQuery.QueryType queryType) {
+    this.field = Objects.requireNonNull(field, "field must not be null");
+    this.queryPackedValue =
+        Objects.requireNonNull(queryPackedValue, "queryPackedValue must not be 
null");
+    this.numDims = numDims;
+    this.bytesPerDim = bytesPerDim;
+    this.queryType = Objects.requireNonNull(queryType, "queryType must not be 
null");
+    this.comparator = ArrayUtil.getUnsignedComparator(bytesPerDim);
+  }
+
+  @Override
+  public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, 
float boost) {
+    return new ConstantScoreWeight(this, boost) {
+      @Override
+      public Scorer scorer(LeafReaderContext context) throws IOException {
+        LeafReader reader = context.reader();
+        if (reader.getFieldInfos().fieldInfo(field) == null) {
+          return null;
+        }
+        final BinaryDocValues values = DocValues.getBinary(reader, field);
+        final int stride = 2 * numDims * bytesPerDim;
+        final byte[] docPacked = new byte[stride];
+        final TwoPhaseIterator iterator =
+            new TwoPhaseIterator(values) {
+              @Override
+              public boolean matches() throws IOException {
+                // The doc's value is N concatenated stride-byte ranges; match 
if ANY satisfies.
+                // Copy each range to a 0-based buffer because 
QueryType.matches indexes from 0.
+                BytesRef b = values.binaryValue();
+                for (int off = b.offset; off < b.offset + b.length; off += 
stride) {
+                  System.arraycopy(b.bytes, off, docPacked, 0, stride);
+                  if (queryType.matches(
+                      queryPackedValue, docPacked, numDims, bytesPerDim, 
comparator)) {
+                    return true;
+                  }
+                }
+                return false;
+              }
+
+              @Override
+              public float matchCost() {
+                return stride; // ~ one packed-value comparison per range
+              }
+            };
+
+        return new ConstantScoreScorer(this, score(), scoreMode, iterator);
+      }
+
+      @Override
+      public boolean isCacheable(LeafReaderContext ctx) {
+        return DocValues.isCacheable(ctx, field);
+      }
+    };
+  }
+
+  @Override
+  public void visit(QueryVisitor visitor) {
+    if (visitor.acceptField(field)) {
+      visitor.visitLeaf(this);
+    }
+  }
+
+  @Override
+  public String toString(String f) {
+    return getClass().getSimpleName()
+        + "(field="
+        + field
+        + ", type="
+        + queryType
+        + ", value="
+        + Arrays.toString(queryPackedValue)
+        + ")";
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!sameClassAs(obj)) {
+      return false;
+    }
+    MultiBinaryRangeDocValuesQuery that = (MultiBinaryRangeDocValuesQuery) obj;
+    // queryType MUST be part of identity: the same field/range under 
different relations are
+    // different queries and must not collide in the query cache.
+    return numDims == that.numDims
+        && bytesPerDim == that.bytesPerDim
+        && queryType == that.queryType
+        && field.equals(that.field)
+        && Arrays.equals(queryPackedValue, that.queryPackedValue);
+  }
+
+  @Override
+  public int hashCode() {
+    int h = classHash();
+    h = 31 * h + field.hashCode();
+    h = 31 * h + Arrays.hashCode(queryPackedValue);
+    h = 31 * h + numDims;
+    h = 31 * h + bytesPerDim;
+    h = 31 * h + queryType.hashCode();
+    return h;
+  }
+}
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 299f6d41867..5c9e7ef64f8 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
@@ -196,13 +196,13 @@ public class NumericRangeQParserPlugin extends 
QParserPlugin {
           }
           switch (criteria) {
             case INTERSECTS:
-              return rangeField.newIntersectsQuery(fieldName, range);
+              return rangeField.newIntersectsQuery(schemaField, range);
             case WITHIN:
-              return rangeField.newWithinQuery(fieldName, range);
+              return rangeField.newWithinQuery(schemaField, range);
             case CONTAINS:
-              return rangeField.newContainsQuery(fieldName, range);
+              return rangeField.newContainsQuery(schemaField, range);
             case CROSSES:
-              return rangeField.newCrossesQuery(fieldName, range);
+              return rangeField.newCrossesQuery(schemaField, range);
             default:
               throw new AssertionError("Unhandled QueryCriteria: " + criteria);
           }
diff --git a/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java 
b/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
index 8508ce8472f..e8e530df157 100644
--- a/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
+++ b/solr/core/src/java/org/apache/solr/update/DocumentBuilder.java
@@ -207,6 +207,42 @@ public class DocumentBuilder {
                       usedFields);
             }
           }
+        } else if (sfield != null
+            && sfield.multiValued()
+            && !forInPlaceUpdate
+            && sfield.getType().shouldCreateFieldsFromAllValues()) {
+          // Types that must combine a field's values into a single indexable 
field (e.g. all of a
+          // doc's ranges into one BinaryDocValues blob) receive them 
together. copyFields stay
+          // per-value.
+          List<Object> values = new ArrayList<>(field.getValueCount());
+          for (Object v : field) {
+            if (v != null) {
+              values.add(v);
+            }
+          }
+          if (!values.isEmpty()) {
+            hasField = true;
+            for (IndexableField f : 
sfield.getType().createFieldsFromAllValues(sfield, values)) {
+              if (f != null) {
+                out.add(f);
+              }
+            }
+            usedFields.add(sfield.getName());
+            used = true;
+            if (copyFields != null) {
+              for (Object v : values) {
+                addCopyFields(
+                    schema,
+                    v,
+                    sfield.getType(),
+                    copyFields,
+                    forInPlaceUpdate,
+                    uniqueKeyFieldName,
+                    out,
+                    usedFields);
+              }
+            }
+          }
         } else {
           Iterator<?> it = field.iterator();
           while (it.hasNext()) {
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 660acc1bfb1..43ff7db2fbd 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
@@ -105,6 +105,24 @@
   <!-- 4D DoubleRangeField (tesseract) -->
   <field name="double_range_4d" type="doublerange4d" indexed="true" 
stored="true"/>
 
+  <!-- docValues-enabled variants (single-valued) exercising the 
IndexOrDocValuesQuery optimization -->
+  <field name="price_range_dv" type="intrange" indexed="true" stored="true" 
docValues="true"/>
+  <field name="bbox_dv" type="intrange2d" indexed="true" stored="true" 
docValues="true"/>
+  <field name="long_range_dv" type="longrange" indexed="true" stored="true" 
docValues="true"/>
+  <field name="float_range_dv" type="floatrange" indexed="true" stored="true" 
docValues="true"/>
+  <field name="double_range_dv" type="doublerange" indexed="true" 
stored="true" docValues="true"/>
+
+  <!-- non-indexed intrange: exercises the DV-only query path (no BKD to fall 
back on). -->
+  <field name="price_range_dvonly" type="intrange" indexed="false" 
stored="true" docValues="true"/>
+
+  <!-- multiValued docValues variants: exercise multiple ranges per document 
-->
+  <field name="price_range_mv_dv" type="intrange" indexed="true" 
multiValued="true" docValues="true"/>
+  <field name="float_range_mv_dv" type="floatrange" indexed="true" 
multiValued="true" docValues="true"/>
+  <field name="long_range_mv_dv_2d" type="longrange2d" indexed="true" 
multiValued="true" docValues="true"/>
+  <field name="double_range_mv_dv_2d" type="doublerange2d" indexed="true" 
multiValued="true" docValues="true"/>
+  <field name="long_range_multi_2d" type="longrange2d" indexed="true" 
stored="true" multiValued="true" docValues="false"/>
+  <field name="double_range_multi_2d" type="doublerange2d" indexed="true" 
stored="true" multiValued="true" docValues="false"/>
+
   <!-- Required by Solr -->
   <uniqueKey>id</uniqueKey>
 </schema>
diff --git 
a/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java
 
b/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java
index fa31205d511..482e27addfe 100644
--- 
a/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java
+++ 
b/solr/core/src/test/org/apache/solr/schema/numericrange/DoubleRangeFieldTest.java
@@ -22,9 +22,12 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import org.apache.lucene.document.DoubleRange;
 import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.NumericUtils;
 import org.apache.solr.SolrTestCase;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.schema.IndexSchema;
@@ -379,6 +382,47 @@ public class DoubleRangeFieldTest extends SolrTestCase {
     assertEquals(1.0e200, range.maxs[0], 0.0);
   }
 
+  public void test2dMultiValuedDocValuesEncodingIsSortedAndCanonical() {
+    final int numDims = 2;
+    DoubleRangeField fieldType = createFieldType(numDims);
+    String field = "double_range_mv_dv";
+
+    // The same set of 2D ranges in two different insertion orders. Includes 
negatives (to exercise
+    // the floating-point sortable transform).
+    List<Object> order1 =
+        List.of(
+            "[0.5,4.0 TO 1.5,5.0]",
+            "[-10.0,5.0 TO -9.0,6.0]",
+            "[0.5,-3.0 TO 1.5,-2.0]",
+            "[-2.5,1.0 TO -1.0,2.0]");
+    List<Object> order2 =
+        List.of(
+            "[-2.5,1.0 TO -1.0,2.0]",
+            "[0.5,-3.0 TO 1.5,-2.0]",
+            "[-10.0,5.0 TO -9.0,6.0]",
+            "[0.5,4.0 TO 1.5,5.0]");
+
+    BytesRef packed1 = fieldType.encodePackedValues(field, order1);
+    BytesRef packed2 = fieldType.encodePackedValues(field, order2);
+
+    // Canonical: insertion order must not change the packed docValues blob.
+    assertEquals(packed1, packed2);
+
+    // Packed ascending by encoded min bytes, dimension-major: primary key is 
the dim-0 min, ties
+    // broken by the dim-1 min. Expected order of (min_d0, min_d1):
+    final double[][] expectedMins = {{-10.0, 5.0}, {-2.5, 1.0}, {0.5, -3.0}, 
{0.5, 4.0}};
+    final int stride = 2 * numDims * Double.BYTES;
+    assertEquals(expectedMins.length, packed1.length / stride);
+    for (int i = 0; i < expectedMins.length; i++) {
+      for (int d = 0; d < numDims; d++) {
+        int offset = packed1.offset + i * stride + d * Double.BYTES;
+        long sortable = NumericUtils.sortableBytesToLong(packed1.bytes, 
offset);
+        double min = NumericUtils.sortableLongToDouble(sortable);
+        assertEquals("min[" + i + "][" + d + "]", expectedMins[i][d], min, 
0.0);
+      }
+    }
+  }
+
   private IndexSchema createMockSchema() {
     final var schema = mock(IndexSchema.class);
     when(schema.getVersion()).thenReturn(1.7f);
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
index 60f8d3b19ab..0898ad376f9 100644
--- 
a/solr/core/src/test/org/apache/solr/schema/numericrange/IntRangeFieldTest.java
+++ 
b/solr/core/src/test/org/apache/solr/schema/numericrange/IntRangeFieldTest.java
@@ -22,9 +22,12 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import org.apache.lucene.document.IntRange;
 import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.NumericUtils;
 import org.apache.solr.SolrTestCase;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.schema.IndexSchema;
@@ -328,6 +331,31 @@ public class IntRangeFieldTest extends SolrTestCase {
     assertEquals(Integer.MAX_VALUE, range.maxs[0]);
   }
 
+  public void testMultiValuedDocValuesEncodingIsSortedAndCanonical() {
+    IntRangeField fieldType = createFieldType(1);
+    String field = "price_range_mv_dv";
+
+    // The same set of ranges supplied in two different insertion orders 
(including a negative, to
+    // exercise the sign-aware sortable-byte ordering).
+    List<Object> order1 = List.of("[7 TO 8]", "[-100 TO -50]", "[3 TO 4]", 
"[11 TO 12]");
+    List<Object> order2 = List.of("[11 TO 12]", "[3 TO 4]", "[7 TO 8]", "[-100 
TO -50]");
+
+    BytesRef packed1 = fieldType.encodePackedValues(field, order1);
+    BytesRef packed2 = fieldType.encodePackedValues(field, order2);
+
+    // Canonical: insertion order must not change the packed docValues blob.
+    assertEquals(packed1, packed2);
+
+    // And the ranges are packed ascending by their (sortable-encoded) min.
+    final int stride = 2 * Integer.BYTES;
+    final int[] expectedMins = {-100, 3, 7, 11};
+    assertEquals(expectedMins.length, packed1.length / stride);
+    for (int i = 0; i < expectedMins.length; i++) {
+      int min = NumericUtils.sortableBytesToInt(packed1.bytes, packed1.offset 
+ i * stride);
+      assertEquals("min at position " + i, expectedMins[i], min);
+    }
+  }
+
   private IndexSchema createMockSchema() {
     final var schema = mock(IndexSchema.class);
     when(schema.getVersion()).thenReturn(1.7f);
diff --git 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeDocValuesTest.java
 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeDocValuesTest.java
new file mode 100644
index 00000000000..7dc4202daf0
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeDocValuesTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests for docValues-specific behaviors (specially multiValued) of the range 
field types.
+ *
+ * <p>Plain query parity between the docValues and non-docValues fields 
(intersects / contains /
+ * within / crosses / single-bound, across int/long/float/double and the 
docValues-only field) is
+ * covered there instead: each of those tests randomizes its field over the 
indexed-only,
+ * indexed+docValues, and (for int) docValues-only variants, so choosing the 
non-indexed
+ * docValues-only field also exercises the pure {@code 
MultiBinaryRangeDocValuesQuery} path.
+ *
+ * <p>This also adds multiValued parity (several ranges packed into one 
docValues blob), the {@link
+ * org.apache.lucene.search.IndexOrDocValuesQuery} clause inside a selective 
conjunction (where the
+ * docValues clause leads iteration), and the fact that enabling docValues 
does <em>not</em> enable
+ * sorting or value ({@code facet.field}) faceting even though {@code 
facet.query} still works.
+ */
+public class NumericRangeDocValuesTest 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());
+  }
+
+  /**
+   * Index the same int range into the indexed-only, indexed+docValues, and 
docValues-only fields.
+   */
+  private void addIntDoc(String id, String range) {
+    assertU(
+        adoc(
+            "id", id,
+            "price_range", range,
+            "price_range_dv", range,
+            "price_range_dvonly", range));
+  }
+
+  @Test
+  public void testSelectiveConjunction() {
+    // Exercises IndexOrDocValuesQuery inside a conjunction: the selective 
title clause leads and
+    // the docValues range clause verifies. Must match the equivalent 
non-docValues query exactly.
+    assertU(
+        adoc(
+            "id",
+            "1",
+            "title",
+            "match",
+            "price_range",
+            "[100 TO 200]",
+            "price_range_dv",
+            "[100 TO 200]"));
+    assertU(
+        adoc(
+            "id",
+            "2",
+            "title",
+            "match",
+            "price_range",
+            "[300 TO 400]",
+            "price_range_dv",
+            "[300 TO 400]"));
+    assertU(
+        adoc(
+            "id",
+            "3",
+            "title",
+            "other",
+            "price_range",
+            "[100 TO 200]",
+            "price_range_dv",
+            "[100 TO 200]"));
+    assertU(commit());
+
+    for (String f : new String[] {"price_range", "price_range_dv"}) {
+      assertQ(
+          "selective AND contains on " + f,
+          req("q", "+title:match +" + f + ":[120 TO 180]"),
+          "//result[@numFound='1']",
+          "//result/doc/str[@name='id'][.='1']");
+    }
+  }
+
+  /** Indexes the given ranges into both a BKD-only and a docValues 
multiValued field. */
+  private void addMvDoc(String id, String bkdField, String dvField, String... 
ranges) {
+    List<String> fieldsAndValues = new ArrayList<>();
+    fieldsAndValues.add("id");
+    fieldsAndValues.add(id);
+    for (String r : ranges) {
+      fieldsAndValues.add(bkdField);
+      fieldsAndValues.add(r);
+      fieldsAndValues.add(dvField);
+      fieldsAndValues.add(r);
+    }
+    assertU(adoc(fieldsAndValues.toArray(new String[0])));
+  }
+
+  /**
+   * Asserts the BKD field and its docValues sibling both match exactly {@code 
expectedIds} (and
+   * only those) for the query. Checks the matched ids, not just the count.
+   */
+  private void assertSameMatches(
+      String criteria, String range, String bkdField, String dvField, 
String... expectedIds) {
+    for (String f : new String[] {bkdField, dvField}) {
+      List<String> tests = new ArrayList<>();
+      // numFound + presence of each (unique) id together pin the exact match 
set.
+      tests.add("//result[@numFound='" + expectedIds.length + "']");
+      for (String id : expectedIds) {
+        tests.add("//result/doc/str[@name='id'][.='" + id + "']");
+      }
+      assertQ(
+          "criteria=" + criteria + " field=" + f + " " + range,
+          req("q", "{!numericRange criteria=" + criteria + " field=" + f + "}" 
+ range),
+          tests.toArray(new String[0]));
+    }
+  }
+
+  @Test
+  public void testContainsPerRangeNotUnion() {
+    // doc 1: two overlapping ranges whose *union* is [1,20]
+    addMvDoc("1", "price_range_multi", "price_range_mv_dv", "[1 TO 10]", "[5 
TO 20]");
+    // doc 2: two disjoint ranges
+    addMvDoc("2", "price_range_multi", "price_range_mv_dv", "[0 TO 5]", "[100 
TO 200]");
+    assertU(commit());
+
+    // Neither individual range of doc 1 contains [1,15]
+    assertSameMatches("contains", "[1 TO 15]", "price_range_multi", 
"price_range_mv_dv");
+
+    // doc 2's [0,5] and [100,200] each satisfy one half-open bound of a 
CONTAINS([3,150]) split, so
+    // a decomposition would falsely match -- native per-range matching must 
not.
+    assertSameMatches("contains", "[3 TO 150]", "price_range_multi", 
"price_range_mv_dv");
+
+    // doc 1's [1,10] actually contains [1,10] (any range suffices).
+    assertSameMatches("contains", "[1 TO 10]", "price_range_multi", 
"price_range_mv_dv", "1");
+  }
+
+  @Test
+  public void testAllRelationsMultiValued() {
+    addMvDoc("1", "price_range_multi", "price_range_mv_dv", "[5 TO 20]", "[1 
TO 10]");
+    addMvDoc("2", "price_range_multi", "price_range_mv_dv", "[0 TO 5]", "[100 
TO 200]");
+    addMvDoc("3", "price_range_multi", "price_range_mv_dv", "[130 TO 160]");
+    assertU(commit());
+
+    // INTERSECTS [12,14]: only doc 1 (via its [5,20] range).
+    assertSameMatches("intersects", "[12 TO 14]", "price_range_multi", 
"price_range_mv_dv", "1");
+    // WITHIN [0,25]: doc 1 ([1,10]) and doc 2 ([0,5]) each have a range 
within it.
+    assertSameMatches("within", "[0 TO 25]", "price_range_multi", 
"price_range_mv_dv", "1", "2");
+    // CROSSES [8,50]: doc 1 crosses (its ranges overlap but aren't within); 
docs 2,3 are disjoint.
+    assertSameMatches("crosses", "[8 TO 50]", "price_range_multi", 
"price_range_mv_dv", "1");
+  }
+
+  @Test
+  public void test2dLongMultiValued() {
+    addMvDoc(
+        "1",
+        "long_range_multi_2d",
+        "long_range_mv_dv_2d",
+        "[100,100 TO 200,200]",
+        "[500,500 TO 600,600]");
+    addMvDoc("2", "long_range_multi_2d", "long_range_mv_dv_2d", "[1,3000000000 
TO 5,4000000000]");
+    assertU(commit());
+
+    // Overlaps doc 1's [100,100 TO 200,200] box in both dims.
+    assertSameMatches(
+        "intersects", "[150,100 TO 550,150]", "long_range_multi_2d", 
"long_range_mv_dv_2d", "1");
+    // Overlaps doc 2's box (whose D2 spans 3e9..4e9).
+    assertSameMatches(
+        "intersects",
+        "[2,3500000000 TO 5,3600000000]",
+        "long_range_multi_2d",
+        "long_range_mv_dv_2d",
+        "2");
+  }
+
+  @Test
+  public void testFloatMultiValued() {
+    addMvDoc("1", "float_range_multi", "float_range_mv_dv", "[1.0 TO 2.0]", 
"[5.0 TO 6.0]");
+    addMvDoc("2", "float_range_multi", "float_range_mv_dv", "[10.0 TO 20.0]");
+    assertU(commit());
+
+    // INTERSECTS [1.5,1.8]: only doc 1 (via its [1.0,2.0] range).
+    assertSameMatches("intersects", "[1.5 TO 1.8]", "float_range_multi", 
"float_range_mv_dv", "1");
+    // WITHIN [0.0,100.0]: both docs' ranges fit inside.
+    assertSameMatches(
+        "within", "[0.0 TO 100.0]", "float_range_multi", "float_range_mv_dv", 
"1", "2");
+  }
+
+  @Test
+  public void test2dDoubleMultiValued() {
+    addMvDoc(
+        "1",
+        "double_range_multi_2d",
+        "double_range_mv_dv_2d",
+        "[1.0,-3.1 TO 5.0,-1.1]",
+        "[-5.5,10.0 TO 1.5,15.0]");
+    addMvDoc("2", "double_range_multi_2d", "double_range_mv_dv_2d", "[-10,2.5 
TO 0,3.5]");
+    assertU(commit());
+
+    // Ranges per doc (each a 2D bounding box):
+    //   doc1 A: D1 [1.0, 5.0]    D2 [-3.1, -1.1]
+    //   doc1 B: D1 [-5.5, 1.5]   D2 [10.0, 15.0]
+    //   doc2 C: D1 [-10.0, 0.0]  D2 [2.5, 3.5]
+
+    // CONTAINS: a doc range must wrap the query box in *both* dims. doc1's A 
wraps
+    // [2,4]x[-3.0,-1.15] (1<=2, 5>=4 and -3.1<=-3.0, -1.1>=-1.15).
+    // doc2's C does not (its D1 max 0 < 4). -> only doc 1.
+    assertSameMatches(
+        "contains",
+        "[2.0,-3.0 TO 4.0,-1.15]",
+        "double_range_multi_2d",
+        "double_range_mv_dv_2d",
+        "1");
+
+    // WITHIN: a doc range must fit *inside* the query box in both dims. 
doc1's A fits inside
+    // [0,6]x[-4,-1]; doc2's C sticks out on D1 (min -10 < 0). -> only doc 1.
+    assertSameMatches(
+        "within", "[0.0,-4.0 TO 6.0,-1.0]", "double_range_multi_2d", 
"double_range_mv_dv_2d", "1");
+
+    // INTERSECTS: overlap in both dims is enough. The central box 
[-2,3]x[-2,3] overlaps doc1's A
+    // and doc2's C, so both match (any-range / any-doc "OR" semantics). -> 2 
docs.
+    assertSameMatches(
+        "intersects",
+        "[-2.0,-2.0 TO 3.0,3.0]",
+        "double_range_multi_2d",
+        "double_range_mv_dv_2d",
+        "1",
+        "2");
+
+    // CROSSES = intersects AND NOT within. For [-1,6]x[-4,3], doc1's A is 
fully *within* it (so
+    // does not cross) and its B is disjoint -> doc 1 excluded; doc2's C 
overlaps but pokes out on
+    // D1 (min -10) -> it crosses. -> only doc 2, which distinguishes crosses 
from within.
+    assertSameMatches(
+        "crosses", "[-1.0,-4.0 TO 6.0,3.0]", "double_range_multi_2d", 
"double_range_mv_dv_2d", "2");
+  }
+
+  @Test
+  public void testSortingUnsupportedEvenWithDocValues() {
+    addIntDoc("1", "[100 TO 200]");
+    assertU(commit());
+    // Range fields don't implement sorting: there's no canonical key for 
ordering intervals (by
+    // min? max? width? midpoint?), multi-dimensional ranges 
(bbox/cube/tesseract) have no sensible
+    // order at all, and the packed BINARY docValues isn't a sortable scalar. 
Enabling docValues
+    // does not change that, getSortField still throws.
+    assertQEx(
+        "sorting on a range field must fail even with docValues",
+        "Cannot sort on",
+        req("q", "*:*", "sort", "price_range_dv asc"),
+        SolrException.ErrorCode.BAD_REQUEST);
+  }
+
+  @Test
+  public void testFieldFacetingUnsupportedEvenWithDocValues() {
+    addIntDoc("1", "[100 TO 200]");
+    addIntDoc("2", "[150 TO 250]");
+    assertU(commit());
+    // Value faceting (facet.field) isn't supported on range fields: Use 
facet.query
+    // for ranges instead (see testQueryFacetingWithDocValues).
+    assertQ(
+        "value faceting on a range field yields no buckets even with 
docValues",
+        req("q", "*:*", "rows", "0", "facet", "true", "facet.field", 
"price_range_dv"),
+        "//lst[@name='facet_fields']/lst[@name='price_range_dv']",
+        
"count(//lst[@name='facet_fields']/lst[@name='price_range_dv']/int)=0");
+  }
+
+  @Test
+  public void testQueryFacetingWithDocValues() {
+    // Range fields support facet.query (count docs matching a range query), 
the realistic way to
+    // facet ranges in Solr. (facet.field / value faceting is NOT supported: 
range docValues are
+    // BINARY, not the SortedSet docValues that term-faceting needs)
+    addIntDoc("1", "[100 TO 200]");
+    addIntDoc("2", "[150 TO 250]");
+    addIntDoc("3", "[50 TO 80]");
+    assertU(commit());
+
+    assertQ(
+        req(
+            "q", "*:*",
+            "facet", "true",
+            "facet.query",
+                "{!numericRange criteria=intersects field=price_range_dv 
key=pr}[120 TO 180]"),
+        "//lst[@name='facet_queries']/int[@name='pr'][.='2']");
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java
 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java
index e57334779c8..06bf94895d3 100644
--- 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginDoubleTest.java
@@ -24,9 +24,21 @@ import org.junit.Test;
 /**
  * Tests for {@link NumericRangeQParserPlugin} using {@link
  * org.apache.solr.schema.numericrange.DoubleRangeField} fields.
+ *
+ * <p>Each 1D scenario picks its field at random (via {@link #randomField}) 
from the equivalent
+ * indexed-only and indexed+docValues variants, so both the non-docValues and 
docValues code paths
+ * get incidental coverage across seeds while the assertions stay identical.
  */
 public class NumericRangeQParserPluginDoubleTest extends SolrTestCaseJ4 {
 
+  /** 1D double range: indexed-only field and its indexed+docValues sibling 
(both stored). */
+  private static final String[] ONE_D = {"double_range", "double_range_dv"};
+
+  /** Randomly returns one of the given field variants so DV / non-DV both get 
covered. */
+  private static String randomField(String... variants) {
+    return variants[random().nextInt(variants.length)];
+  }
+
   @BeforeClass
   public static void beforeClass() throws Exception {
     initCore("solrconfig.xml", "schema-numericrange.xml");
@@ -41,28 +53,29 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DIntersectsQuery() {
-    assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
-    assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
-    assertU(adoc("id", "3", "double_range", "[0.5 TO 0.8]"));
-    assertU(adoc("id", "4", "double_range", "[2.0 TO 3.0]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 0.8]"));
+    assertU(adoc("id", "4", f, "[2.0 TO 3.0]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=double_range}[1.2 
TO 1.8]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[1.2 TO 
1.8]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
-        "//result/doc/str[@name='double_range'][.='[1.0 TO 2.0]']",
-        "//result/doc/str[@name='double_range'][.='[1.5 TO 2.5]']");
+        "//result/doc/str[@name='" + f + "'][.='[1.0 TO 2.0]']",
+        "//result/doc/str[@name='" + f + "'][.='[1.5 TO 2.5]']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=double_range}[0.0 
TO 1.0]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[0.0 TO 
1.0]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='3']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=double_range}[1.75 
TO 2.25]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[1.75 TO 
2.25]"),
         "//result[@numFound='3']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
@@ -71,46 +84,48 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DWithinQuery() {
-    assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
-    assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
-    assertU(adoc("id", "3", "double_range", "[0.5 TO 0.8]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 0.8]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=double_range}[0.0 
TO 3.0]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[0.0 TO 
3.0]"),
         "//result[@numFound='3']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=double_range}[1.0 
TO 2.0]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[1.0 TO 
2.0]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=double_range}[0.0 
TO 1.0]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[0.0 TO 
1.0]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='3']");
   }
 
   @Test
   public void test1DContainsQuery() {
-    assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
-    assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
-    assertU(adoc("id", "3", "double_range", "[0.5 TO 3.0]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 3.0]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=double_range}[1.6 
TO 1.7]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[1.6 TO 
1.7]"),
         "//result[@numFound='3']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
         "//result/doc/str[@name='id'][.='3']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=double_range}[0.0 
TO 4.0]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[0.0 TO 
4.0]"),
         "//result[@numFound='0']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=double_range}[1.0 
TO 2.0]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[1.0 TO 
2.0]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='3']");
@@ -118,14 +133,15 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DCrossesQuery() {
-    assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
-    assertU(adoc("id", "2", "double_range", "[1.5 TO 2.5]"));
-    assertU(adoc("id", "3", "double_range", "[0.5 TO 0.8]"));
-    assertU(adoc("id", "4", "double_range", "[1.2 TO 1.8]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 0.8]"));
+    assertU(adoc("id", "4", f, "[1.2 TO 1.8]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"crosses\" field=double_range}[1.5 
TO 2.5]"),
+        req("q", "{!numericRange criteria=\"crosses\" field=" + f + "}[1.5 TO 
2.5]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='4']");
@@ -213,27 +229,29 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testNegativeValues() {
-    assertU(adoc("id", "1", "double_range", "[-1.0 TO -0.5]"));
-    assertU(adoc("id", "2", "double_range", "[-0.75 TO -0.25]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[-1.0 TO -0.5]"));
+    assertU(adoc("id", "2", f, "[-0.75 TO -0.25]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=double_range}[-0.8 
TO -0.6]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[-0.8 TO 
-0.6]"),
         "//result[@numFound='2']");
   }
 
   @Test
   public void testPointRange() {
-    assertU(adoc("id", "1", "double_range", "[1.5 TO 1.5]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.5 TO 1.5]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=double_range}[1.5 
TO 1.5]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[1.5 TO 
1.5]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=double_range}[0.5 
TO 1.75]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[0.5 TO 
1.75]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
@@ -307,31 +325,33 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFullRange() {
+    String f = randomField(ONE_D);
     // doc 1: narrow range, fully inside the query range  → should NOT match 
(doc contains query)
     // doc 2: wide range that fully contains the query range → should match
     // doc 3: range that only partially overlaps            → should NOT match
-    assertU(adoc("id", "1", "double_range", "[1.3 TO 1.6]")); // No match
-    assertU(adoc("id", "2", "double_range", "[1.0 TO 2.0]")); // Match!
-    assertU(adoc("id", "3", "double_range", "[1.5 TO 2.5]")); // No match
+    assertU(adoc("id", "1", f, "[1.3 TO 1.6]")); // No match
+    assertU(adoc("id", "2", f, "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "3", f, "[1.5 TO 2.5]")); // No match
     assertU(commit());
 
     // Contains semantics: find indexed ranges that fully contain [1.2 TO 1.8]
     assertQ(
-        req("q", "double_range:[1.2 TO 1.8]"),
+        req("q", f + ":[1.2 TO 1.8]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='2']");
   }
 
   @Test
   public void testGetFieldQueryFullRangeMultipleMatches() {
-    assertU(adoc("id", "1", "double_range", "[0.0 TO 10.0]")); // Match!
-    assertU(adoc("id", "2", "double_range", "[1.0 TO 2.0]")); // Match!
-    assertU(adoc("id", "3", "double_range", "[1.0 TO 1.99]")); // No match - 
max too low
-    assertU(adoc("id", "4", "double_range", "[1.01 TO 2.0]")); // No match - 
min too high
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[0.0 TO 10.0]")); // Match!
+    assertU(adoc("id", "2", f, "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "3", f, "[1.0 TO 1.99]")); // No match - max too low
+    assertU(adoc("id", "4", f, "[1.01 TO 2.0]")); // No match - min too high
     assertU(commit());
 
     assertQ(
-        req("q", "double_range:[1.0 TO 2.0]"),
+        req("q", f + ":[1.0 TO 2.0]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -339,15 +359,16 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQuerySingleBound() {
+    String f = randomField(ONE_D);
     // Single-bound syntax: double_range:1.5 is sugar for contains([1.5 TO 
1.5])
-    assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]")); // Match!
-    assertU(adoc("id", "2", "double_range", "[1.5 TO 1.5]")); // Match!
-    assertU(adoc("id", "3", "double_range", "[1.0 TO 1.49]")); // No match - 
max below 1.5
-    assertU(adoc("id", "4", "double_range", "[1.51 TO 3.0]")); // No match - 
min above 1.5
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "2", f, "[1.5 TO 1.5]")); // Match!
+    assertU(adoc("id", "3", f, "[1.0 TO 1.49]")); // No match - max below 1.5
+    assertU(adoc("id", "4", f, "[1.51 TO 3.0]")); // No match - min above 1.5
     assertU(commit());
 
     assertQ(
-        req("q", "double_range:1.5"),
+        req("q", f + ":1.5"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -371,7 +392,8 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFieldFormatting() {
-    assertU(adoc("id", "1", "double_range", "[1.0 TO 2.0]"));
+    String f1 = randomField(ONE_D);
+    assertU(adoc("id", "1", f1, "[1.0 TO 2.0]"));
     assertU(adoc("id", "2", "double_range_2d", "[1.0,2.0 TO 3.0,4.0]"));
     assertU(adoc("id", "3", "double_range_3d", "[0.5,1.0,1.5 TO 
2.5,3.0,3.5]"));
     assertU(adoc("id", "4", "double_range_4d", "[0.1,0.2,0.3,0.4 TO 
1.1,1.2,1.3,1.4]"));
@@ -391,7 +413,7 @@ public class NumericRangeQParserPluginDoubleTest extends 
SolrTestCaseJ4 {
     assertQ(
         req("q", "id:1"),
         "//result[@numFound='1']",
-        "//result/doc/str[@name='double_range'][.='[1.0 TO 2.0]']");
+        "//result/doc/str[@name='" + f1 + "'][.='[1.0 TO 2.0]']");
 
     // Verify 2D field returns correctly formatted value
     assertQ(
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
index 32b1e1200bd..031396c811f 100644
--- 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginFloatTest.java
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginFloatTest.java
@@ -24,9 +24,21 @@ import org.junit.Test;
 /**
  * Tests for {@link NumericRangeQParserPlugin} using {@link
  * org.apache.solr.schema.numericrange.FloatRangeField} fields.
+ *
+ * <p>Each 1D scenario picks its field at random (via {@link #randomField}) 
from the equivalent
+ * indexed-only and indexed+docValues variants, so both the non-docValues and 
docValues code paths
+ * get incidental coverage across seeds while the assertions stay identical.
  */
 public class NumericRangeQParserPluginFloatTest extends SolrTestCaseJ4 {
 
+  /** 1D float range: indexed-only field and its indexed+docValues sibling 
(both stored). */
+  private static final String[] ONE_D = {"float_range", "float_range_dv"};
+
+  /** Randomly returns one of the given field variants so DV / non-DV both get 
covered. */
+  private static String randomField(String... variants) {
+    return variants[random().nextInt(variants.length)];
+  }
+
   @BeforeClass
   public static void beforeClass() throws Exception {
     initCore("solrconfig.xml", "schema-numericrange.xml");
@@ -41,28 +53,29 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 0.8]"));
+    assertU(adoc("id", "4", f, "[2.0 TO 3.0]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=float_range}[1.2 TO 
1.8]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]']");
+        "//result/doc/str[@name='" + f + "'][.='[1.0 TO 2.0]']",
+        "//result/doc/str[@name='" + f + "'][.='[1.5 TO 2.5]']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=float_range}[0.0 TO 
1.0]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[1.75 TO 
2.25]"),
         "//result[@numFound='3']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
@@ -71,46 +84,48 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 0.8]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=float_range}[0.0 TO 
3.0]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[0.0 TO 
3.0]"),
         "//result[@numFound='3']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=float_range}[1.0 TO 
2.0]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 3.0]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=float_range}[1.6 
TO 1.7]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[0.0 TO 
4.0]"),
         "//result[@numFound='0']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=float_range}[1.0 
TO 2.0]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[1.0 TO 
2.0]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='3']");
@@ -118,14 +133,15 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.0 TO 2.0]"));
+    assertU(adoc("id", "2", f, "[1.5 TO 2.5]"));
+    assertU(adoc("id", "3", f, "[0.5 TO 0.8]"));
+    assertU(adoc("id", "4", f, "[1.2 TO 1.8]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"crosses\" field=float_range}[1.5 
TO 2.5]"),
+        req("q", "{!numericRange criteria=\"crosses\" field=" + f + "}[1.5 TO 
2.5]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='4']");
@@ -212,27 +228,29 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[-1.0 TO -0.5]"));
+    assertU(adoc("id", "2", f, "[-0.75 TO -0.25]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=float_range}[-0.8 
TO -0.6]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[-0.8 TO 
-0.6]"),
         "//result[@numFound='2']");
   }
 
   @Test
   public void testPointRange() {
-    assertU(adoc("id", "1", "float_range", "[1.5 TO 1.5]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[1.5 TO 1.5]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=float_range}[1.5 TO 
1.5]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[0.5 TO 
1.75]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
@@ -306,31 +324,33 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFullRange() {
+    String f = randomField(ONE_D);
     // 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(adoc("id", "1", f, "[1.3 TO 1.6]")); // No match
+    assertU(adoc("id", "2", f, "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "3", f, "[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]"),
+        req("q", f + ":[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
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[0.0 TO 10.0]")); // Match!
+    assertU(adoc("id", "2", f, "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "3", f, "[1.0 TO 1.99]")); // No match - max too low
+    assertU(adoc("id", "4", f, "[1.01 TO 2.0]")); // No match - min too high
     assertU(commit());
 
     assertQ(
-        req("q", "float_range:[1.0 TO 2.0]"),
+        req("q", f + ":[1.0 TO 2.0]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -338,15 +358,16 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQuerySingleBound() {
+    String f = randomField(ONE_D);
     // 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(adoc("id", "1", f, "[1.0 TO 2.0]")); // Match!
+    assertU(adoc("id", "2", f, "[1.5 TO 1.5]")); // Match!
+    assertU(adoc("id", "3", f, "[1.0 TO 1.49]")); // No match - max below 1.5
+    assertU(adoc("id", "4", f, "[1.51 TO 3.0]")); // No match - min above 1.5
     assertU(commit());
 
     assertQ(
-        req("q", "float_range:1.5"),
+        req("q", f + ":1.5"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -370,7 +391,8 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFieldFormatting() {
-    assertU(adoc("id", "1", "float_range", "[1.0 TO 2.0]"));
+    String f1 = randomField(ONE_D);
+    assertU(adoc("id", "1", f1, "[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]"));
@@ -390,7 +412,7 @@ public class NumericRangeQParserPluginFloatTest extends 
SolrTestCaseJ4 {
     assertQ(
         req("q", "id:1"),
         "//result[@numFound='1']",
-        "//result/doc/str[@name='float_range'][.='[1.0 TO 2.0]']");
+        "//result/doc/str[@name='" + f1 + "'][.='[1.0 TO 2.0]']");
 
     // Verify 2D field returns correctly formatted value
     assertQ(
diff --git 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
index b76db8acf82..b93c0993edb 100644
--- 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
@@ -24,9 +24,25 @@ import org.junit.Test;
 /**
  * Tests for {@link NumericRangeQParserPlugin} using {@link
  * org.apache.solr.schema.numericrange.IntRangeField} fields.
+ *
+ * <p>Each scenario picks its field at random (via {@link #randomField}) from 
the equivalent field
+ * variants for 1D: indexed-only, indexed+docValues, and the (non-indexed) 
docValues-only field so
+ * all of those code paths, including the pure docValues query, get incidental 
coverage across seeds
+ * while the assertions stay identical.
  */
 public class NumericRangeQParserPluginIntTest extends SolrTestCaseJ4 {
 
+  /** 1D int range variants: indexed-only, indexed+docValues, and 
(non-indexed) docValues-only. */
+  private static final String[] ONE_D = {"price_range", "price_range_dv", 
"price_range_dvonly"};
+
+  /** 2D int range (bounding box): indexed-only field and its 
indexed+docValues sibling. */
+  private static final String[] TWO_D = {"bbox", "bbox_dv"};
+
+  /** Randomly returns one of the given field variants so DV / non-DV both get 
covered. */
+  private static String randomField(String... variants) {
+    return variants[random().nextInt(variants.length)];
+  }
+
   @BeforeClass
   public static void beforeClass() throws Exception {
     initCore("solrconfig.xml", "schema-numericrange.xml");
@@ -41,32 +57,33 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DIntersectsQuery() {
+    String f = randomField(ONE_D);
     // 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(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 80]"));
+    assertU(adoc("id", "4", f, "[200 TO 300]"));
     assertU(commit());
 
     // Query: find ranges intersecting [120 TO 180]
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=price_range}[120 TO 
180]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]']");
+        "//result/doc/str[@name='" + f + "'][.='[100 TO 200]']",
+        "//result/doc/str[@name='" + f + "'][.='[150 TO 250]']");
 
     // Query: find ranges intersecting [0 TO 100]
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=price_range}[0 TO 
100]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[175 TO 
225]"),
         "//result[@numFound='3']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
@@ -75,39 +92,41 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 80]"));
     assertU(commit());
 
     // Query: find ranges within [0 TO 300]
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=price_range}[0 TO 
300]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 300]"));
     assertU(commit());
 
     // Query: find ranges containing [160 TO 170]
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=price_range}[160 
TO 170]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[160 TO 
170]"),
         "//result[@numFound='3']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
@@ -115,12 +134,12 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
     // Query: find ranges containing [0 TO 400]
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=price_range}[0 TO 
400]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[100 TO 
200]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='3']");
@@ -128,16 +147,17 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @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]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 80]"));
+    assertU(adoc("id", "4", f, "[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]"),
+        req("q", "{!numericRange criteria=\"crosses\" field=" + f + "}[150 TO 
250]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='4']");
@@ -145,53 +165,69 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test2DIntersectsQuery() {
+    String f = randomField(TWO_D);
     // 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(adoc("id", "1", f, "[0,0 TO 10,10]"));
+    assertU(adoc("id", "2", f, "[5,5 TO 15,15]"));
+    assertU(adoc("id", "3", f, "[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]']");
+        "//result/doc/str[@name='" + f + "'][.='[0,0 TO 10,10]']",
+        "//result/doc/str[@name='" + f + "'][.='[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]"));
+    String f = randomField(TWO_D);
+    assertU(adoc("id", "1", f, "[5,5 TO 10,10]"));
+    assertU(adoc("id", "2", f, "[0,0 TO 20,20]"));
+    assertU(adoc("id", "3", f, "[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]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[0,0 TO 
30,30]"),
         "//result[@numFound='3']");
   }
 
+  @Test
+  public void test2DContainsQuery() {
+    String f = randomField(TWO_D);
+    assertU(adoc("id", "1", f, "[0,0 TO 10,10]")); // contains [3,3 TO 7,7]
+    assertU(adoc("id", "2", f, "[4,4 TO 6,6]")); // within it, does NOT contain
+    assertU(commit());
+
+    // Query: find bboxes containing [3,3 TO 7,7]
+    assertQ(
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[3,3 TO 
7,7]"),
+        "//result[@numFound='1']",
+        "//result/doc/str[@name='id'][.='1']");
+  }
+
   @Test
   public void test3DQuery() {
     // Index documents with 3D ranges (bounding cubes)
@@ -329,46 +365,49 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testNegativeValues() {
-    assertU(adoc("id", "1", "price_range", "[-100 TO -50]"));
-    assertU(adoc("id", "2", "price_range", "[-75 TO -25]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[-100 TO -50]"));
+    assertU(adoc("id", "2", f, "[-75 TO -25]"));
     assertU(commit());
 
     // Query with negative range
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=price_range}[-80 TO 
-60]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[-80 TO 
-60]"),
         "//result[@numFound='2']");
   }
 
   @Test
   public void testExtremeValues() {
+    String f = randomField(ONE_D);
     int min = Integer.MIN_VALUE;
     int max = Integer.MAX_VALUE;
 
-    assertU(adoc("id", "1", "price_range", "[" + min + " TO " + max + "]"));
+    assertU(adoc("id", "1", f, "[" + min + " TO " + max + "]"));
     assertU(commit());
 
     // Query should match the extreme range
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=price_range}[0 TO 
100]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[0 TO 
100]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
 
   @Test
   public void testPointRange() {
+    String f = randomField(ONE_D);
     // Point range where min == max
-    assertU(adoc("id", "1", "price_range", "[100 TO 100]"));
+    assertU(adoc("id", "1", f, "[100 TO 100]"));
     assertU(commit());
 
     // Intersects query with point
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=price_range}[100 TO 
100]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[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]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[50 TO 
150]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
@@ -382,31 +421,33 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFullRange() {
+    String f = randomField(ONE_D);
     // 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(adoc("id", "1", f, "[130 TO 160]")); // No match
+    assertU(adoc("id", "2", f, "[100 TO 200]")); // Match!
+    assertU(adoc("id", "3", f, "[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]"),
+        req("q", f + ":[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
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[0 TO 1000]")); // Match!
+    assertU(adoc("id", "2", f, "[100 TO 200]")); // Match!
+    assertU(adoc("id", "3", f, "[100 TO 199]")); // No match - max too low
+    assertU(adoc("id", "4", f, "[101 TO 200]")); // No match - min too high
     assertU(commit());
 
     assertQ(
-        req("q", "price_range:[100 TO 200]"),
+        req("q", f + ":[100 TO 200]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -414,15 +455,16 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @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
+    String f = randomField(ONE_D);
+    // Single-bound syntax: <field>:150 is sugar for contains([150 TO 150])
+    assertU(adoc("id", "1", f, "[100 TO 200]")); // Match!
+    assertU(adoc("id", "2", f, "[150 TO 150]")); // Match!
+    assertU(adoc("id", "3", f, "[100 TO 149]")); // No match - max below 150
+    assertU(adoc("id", "4", f, "[151 TO 300]")); // No match - min above 150
     assertU(commit());
 
     assertQ(
-        req("q", "price_range:150"),
+        req("q", f + ":150"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -430,15 +472,16 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @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
+    String f = randomField(TWO_D);
+    // 2D single-bound: <field>:5,5 is sugar for contains([5,5 TO 5,5])
+    assertU(adoc("id", "1", f, "[0,0 TO 10,10]")); // Match!
+    assertU(adoc("id", "2", f, "[5,5 TO 5,5]")); // Match!
+    assertU(adoc("id", "3", f, "[0,0 TO 4,10]")); // No match - X dimension 
ends too low
+    assertU(adoc("id", "4", f, "[6,0 TO 10,10]")); // No match - X dimension 
starts too high
     assertU(commit());
 
     assertQ(
-        req("q", "bbox:5,5"),
+        req("q", f + ":5,5"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -446,10 +489,12 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFieldFormatting() {
+    String f1 = randomField(ONE_D);
+    String f2 = randomField(TWO_D);
     // Test 1D field formatting
-    assertU(adoc("id", "1", "price_range", "[100 TO 200]"));
+    assertU(adoc("id", "1", f1, "[100 TO 200]"));
     // Test 2D field formatting
-    assertU(adoc("id", "2", "bbox", "[10,20 TO 30,40]"));
+    assertU(adoc("id", "2", f2, "[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
@@ -471,13 +516,13 @@ public class NumericRangeQParserPluginIntTest extends 
SolrTestCaseJ4 {
     assertQ(
         req("q", "id:1"),
         "//result[@numFound='1']",
-        "//result/doc/str[@name='price_range'][.='[100 TO 200]']");
+        "//result/doc/str[@name='" + f1 + "'][.='[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]']");
+        "//result/doc/str[@name='" + f2 + "'][.='[10,20 TO 30,40]']");
 
     // Verify 3D field returns correctly formatted value
     assertQ(
diff --git 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginLongTest.java
 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginLongTest.java
index 1b774e88e90..3953920968b 100644
--- 
a/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginLongTest.java
+++ 
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginLongTest.java
@@ -24,9 +24,21 @@ import org.junit.Test;
 /**
  * Tests for {@link NumericRangeQParserPlugin} using {@link
  * org.apache.solr.schema.numericrange.LongRangeField} fields.
+ *
+ * <p>Each 1D scenario picks its field at random (via {@link #randomField}) 
from the equivalent
+ * indexed-only and indexed+docValues variants, so both the non-docValues and 
docValues code paths
+ * get incidental coverage across seeds while the assertions stay identical.
  */
 public class NumericRangeQParserPluginLongTest extends SolrTestCaseJ4 {
 
+  /** 1D long range: indexed-only field and its indexed+docValues sibling 
(both stored). */
+  private static final String[] ONE_D = {"long_range", "long_range_dv"};
+
+  /** Randomly returns one of the given field variants so DV / non-DV both get 
covered. */
+  private static String randomField(String... variants) {
+    return variants[random().nextInt(variants.length)];
+  }
+
   @BeforeClass
   public static void beforeClass() throws Exception {
     initCore("solrconfig.xml", "schema-numericrange.xml");
@@ -41,28 +53,29 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DIntersectsQuery() {
-    assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
-    assertU(adoc("id", "2", "long_range", "[150 TO 250]"));
-    assertU(adoc("id", "3", "long_range", "[50 TO 80]"));
-    assertU(adoc("id", "4", "long_range", "[200 TO 300]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 80]"));
+    assertU(adoc("id", "4", f, "[200 TO 300]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[120 TO 
180]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[120 TO 
180]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
-        "//result/doc/str[@name='long_range'][.='[100 TO 200]']",
-        "//result/doc/str[@name='long_range'][.='[150 TO 250]']");
+        "//result/doc/str[@name='" + f + "'][.='[100 TO 200]']",
+        "//result/doc/str[@name='" + f + "'][.='[150 TO 250]']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[0 TO 
100]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[0 TO 
100]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='3']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[175 TO 
225]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[175 TO 
225]"),
         "//result[@numFound='3']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']",
@@ -71,46 +84,48 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DWithinQuery() {
-    assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
-    assertU(adoc("id", "2", "long_range", "[150 TO 250]"));
-    assertU(adoc("id", "3", "long_range", "[50 TO 80]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 80]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=long_range}[0 TO 
300]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[0 TO 
300]"),
         "//result[@numFound='3']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=long_range}[100 TO 
200]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[100 TO 
200]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"within\" field=long_range}[0 TO 
100]"),
+        req("q", "{!numericRange criteria=\"within\" field=" + f + "}[0 TO 
100]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='3']");
   }
 
   @Test
   public void test1DContainsQuery() {
-    assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
-    assertU(adoc("id", "2", "long_range", "[150 TO 250]"));
-    assertU(adoc("id", "3", "long_range", "[50 TO 300]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 300]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=long_range}[160 
TO 170]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[160 TO 
170]"),
         "//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=long_range}[0 TO 
400]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[0 TO 
400]"),
         "//result[@numFound='0']");
 
     assertQ(
-        req("q", "{!numericRange criteria=\"contains\" field=long_range}[100 
TO 200]"),
+        req("q", "{!numericRange criteria=\"contains\" field=" + f + "}[100 TO 
200]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='3']");
@@ -118,14 +133,15 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void test1DCrossesQuery() {
-    assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
-    assertU(adoc("id", "2", "long_range", "[150 TO 250]"));
-    assertU(adoc("id", "3", "long_range", "[50 TO 80]"));
-    assertU(adoc("id", "4", "long_range", "[120 TO 180]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 200]"));
+    assertU(adoc("id", "2", f, "[150 TO 250]"));
+    assertU(adoc("id", "3", f, "[50 TO 80]"));
+    assertU(adoc("id", "4", f, "[120 TO 180]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=\"crosses\" field=long_range}[150 TO 
250]"),
+        req("q", "{!numericRange criteria=\"crosses\" field=" + f + "}[150 TO 
250]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='4']");
@@ -269,56 +285,60 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testNegativeValues() {
-    assertU(adoc("id", "1", "long_range", "[-100 TO -50]"));
-    assertU(adoc("id", "2", "long_range", "[-75 TO -25]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[-100 TO -50]"));
+    assertU(adoc("id", "2", f, "[-75 TO -25]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[-80 TO 
-60]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[-80 TO 
-60]"),
         "//result[@numFound='2']");
   }
 
   @Test
   public void testValuesOutsideIntRange() {
+    String f = randomField(ONE_D);
     // Values that cannot be stored in an int but are valid longs
     long min = 3_000_000_000L;
     long max = 4_000_000_000L;
 
-    assertU(adoc("id", "1", "long_range", "[" + min + " TO " + max + "]"));
+    assertU(adoc("id", "1", f, "[" + min + " TO " + max + "]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects 
field=long_range}[3500000000 TO 3600000000]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + 
"}[3500000000 TO 3600000000]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
 
   @Test
   public void testExtremeValues() {
+    String f = randomField(ONE_D);
     long min = Long.MIN_VALUE;
     long max = Long.MAX_VALUE;
 
-    assertU(adoc("id", "1", "long_range", "[" + min + " TO " + max + "]"));
+    assertU(adoc("id", "1", f, "[" + min + " TO " + max + "]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[0 TO 
100]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[0 TO 
100]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
 
   @Test
   public void testPointRange() {
-    assertU(adoc("id", "1", "long_range", "[100 TO 100]"));
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[100 TO 100]"));
     assertU(commit());
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[100 TO 
100]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[100 TO 
100]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
 
     assertQ(
-        req("q", "{!numericRange criteria=intersects field=long_range}[50 TO 
150]"),
+        req("q", "{!numericRange criteria=intersects field=" + f + "}[50 TO 
150]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='1']");
   }
@@ -329,31 +349,33 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFullRange() {
+    String f = randomField(ONE_D);
     // 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", "long_range", "[130 TO 160]")); // No match
-    assertU(adoc("id", "2", "long_range", "[100 TO 200]")); // Match!
-    assertU(adoc("id", "3", "long_range", "[150 TO 250]")); // No match
+    assertU(adoc("id", "1", f, "[130 TO 160]")); // No match
+    assertU(adoc("id", "2", f, "[100 TO 200]")); // Match!
+    assertU(adoc("id", "3", f, "[150 TO 250]")); // No match
     assertU(commit());
 
     // Contains semantics: find indexed ranges that fully contain [120 TO 180]
     assertQ(
-        req("q", "long_range:[120 TO 180]"),
+        req("q", f + ":[120 TO 180]"),
         "//result[@numFound='1']",
         "//result/doc/str[@name='id'][.='2']");
   }
 
   @Test
   public void testGetFieldQueryFullRangeMultipleMatches() {
-    assertU(adoc("id", "1", "long_range", "[0 TO 1000]")); // Match!
-    assertU(adoc("id", "2", "long_range", "[100 TO 200]")); // Match!
-    assertU(adoc("id", "3", "long_range", "[100 TO 199]")); // No match - max 
too low
-    assertU(adoc("id", "4", "long_range", "[101 TO 200]")); // No match - min 
too high
+    String f = randomField(ONE_D);
+    assertU(adoc("id", "1", f, "[0 TO 1000]")); // Match!
+    assertU(adoc("id", "2", f, "[100 TO 200]")); // Match!
+    assertU(adoc("id", "3", f, "[100 TO 199]")); // No match - max too low
+    assertU(adoc("id", "4", f, "[101 TO 200]")); // No match - min too high
     assertU(commit());
 
     assertQ(
-        req("q", "long_range:[100 TO 200]"),
+        req("q", f + ":[100 TO 200]"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -361,15 +383,16 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQuerySingleBound() {
-    // Single-bound syntax: long_range:150 is sugar for contains([150 TO 150])
-    assertU(adoc("id", "1", "long_range", "[100 TO 200]")); // Match!
-    assertU(adoc("id", "2", "long_range", "[150 TO 150]")); // Match!
-    assertU(adoc("id", "3", "long_range", "[100 TO 149]")); // No match - max 
below 150
-    assertU(adoc("id", "4", "long_range", "[151 TO 300]")); // No match - min 
above 150
+    String f = randomField(ONE_D);
+    // Single-bound syntax: <field>:150 is sugar for contains([150 TO 150])
+    assertU(adoc("id", "1", f, "[100 TO 200]")); // Match!
+    assertU(adoc("id", "2", f, "[150 TO 150]")); // Match!
+    assertU(adoc("id", "3", f, "[100 TO 149]")); // No match - max below 150
+    assertU(adoc("id", "4", f, "[151 TO 300]")); // No match - min above 150
     assertU(commit());
 
     assertQ(
-        req("q", "long_range:150"),
+        req("q", f + ":150"),
         "//result[@numFound='2']",
         "//result/doc/str[@name='id'][.='1']",
         "//result/doc/str[@name='id'][.='2']");
@@ -377,7 +400,8 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQuerySingleBound2D() {
-    // 2D single-bound: bbox:5,5 is sugar for contains([5,5 TO 5,5])
+    // 2D single-bound: bbox:5,5 is sugar for contains([5,5 TO 5,5]). Uses the 
int bbox field (no
+    // long 2D docValues variant exists), so this is left on the indexed-only 
field.
     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
@@ -393,8 +417,9 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
 
   @Test
   public void testGetFieldQueryFieldFormatting() {
+    String f1 = randomField(ONE_D);
     // Test 1D field formatting
-    assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
+    assertU(adoc("id", "1", f1, "[100 TO 200]"));
     // Test 2D field formatting
     assertU(adoc("id", "2", "bbox", "[10,20 TO 30,40]"));
     // Test 3D field formatting
@@ -418,7 +443,7 @@ public class NumericRangeQParserPluginLongTest extends 
SolrTestCaseJ4 {
     assertQ(
         req("q", "id:1"),
         "//result[@numFound='1']",
-        "//result/doc/str[@name='long_range'][.='[100 TO 200]']");
+        "//result/doc/str[@name='" + f1 + "'][.='[100 TO 200]']");
 
     // Verify 2D field returns correctly formatted value
     assertQ(
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 d75cedd4cd5..3f26b6b155d 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
@@ -53,17 +53,17 @@ 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 does not support docValues. Typically queried using 
the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric 
Range Query Parser], though the Lucene and othe [...]
+|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 supports `docValues="true"` (with 
`multiValued="true"` or `"false"`) as a query-time filter optimization. Note 
that, unlike most docValues-enabled types, this does _not_ enab [...]
 
 |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.
 
-|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 does not support docValues. 
Typically queried using the 
xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range 
Query [...]
+|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 supports `docValues="true"` (with 
`multiValued="true"` or `"false"`) as a query-time filter optimization. Note 
that, unlike most docValues [...]
 
-|DoubleRangeField |Stores single or multi-dimensional ranges of 
double-precision floating-point numbers, using syntax like `[1.5 TO 2.5]` or 
`[1.0,2.0 TO 3.0,4.0]`.  Up to 4 dimensions are supported.  Dimensionality is 
specified on new field-types using a `numDimensions` property, and all values 
for a particular field must have exactly this number of dimensions. Field type 
does not support docValues. Typically queried using the 
xref:query-guide:other-parsers.adoc#numeric-range-query-pars [...]
+|DoubleRangeField |Stores single or multi-dimensional ranges of 
double-precision floating-point numbers, using syntax like `[1.5 TO 2.5]` or 
`[1.0,2.0 TO 3.0,4.0]`.  Up to 4 dimensions are supported.  Dimensionality is 
specified on new field-types using a `numDimensions` property, and all values 
for a particular field must have exactly this number of dimensions. Field type 
supports `docValues="true"` (with `multiValued="true"` or `"false"`) as a 
query-time filter optimization. Note that, [...]
 
-|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 does not support docValues. 
Typically queried using the 
xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Q 
[...]
+|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 supports `docValues="true"` (with 
`multiValued="true"` or `"false"`) as a query-time filter optimization. Note 
that, unlike most docVa [...]
 
 |NestPathField | Specialized field type storing enhanced information, when 
xref:indexing-nested-documents.adoc#schema-configuration[working with nested 
documents].
 

Reply via email to