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 000000000000..ab6aa7627bd1
--- /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 000000000000..078f3570edb6
--- /dev/null
+++ b/solr/benchmark/src/java/org/apache/solr/bench/search/RangeFieldSearch.java
@@ -0,0 +1,247 @@
+/*
+ * 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.SolrBenchState;
+import org.apache.solr.bench.generators.SolrGen;
+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.request.SolrQuery;
+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.
+ *
+ *
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.
+ *
+ *
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 -> docValues branch), {@code
+ * broad_points} (~{@value #BROAD_LEAD_DF} docs -> points branch), {@code none_points}
+ * (standalone -> 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) -> docValues branch. */
+ static final int SELECTIVE_LEAD_DF = 2_000;
+
+ /** "Broad" lead df: above {@code rangeCost/8} but below {@code rangeCost} -> 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 selectiveTerms;
+ // Rotated broad-lead terms (each ~BROAD_LEAD_DF docs).
+ Iterator broadTerms;
+
+ @Setup(Level.Trial)
+ public void setupTrial(SolrBenchState solrBenchState) throws Exception {
+ solrBenchState.setUseHttp1(true);
+ solrBenchState.startSolr(1);
+ solrBenchState.createCollection(COLLECTION, 1, 1);
+
+ // Controlled lead selectivity: ~numDocs/DF distinct values -> each term matches ~DF docs.
+ SolrGen selectiveLeadTerms =
+ integers().between(0, NUM_DOCS / SELECTIVE_LEAD_DF - 1).map(i -> "s" + i, String.class);
+ SolrGen 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 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
+
+ solrBenchState.index(COLLECTION, docs, NUM_DOCS, false);
+
+ // Discover the real term values (and their counts) so queries hit existing terms.
+ String basePath = solrBenchState.nodes.getFirst();
+ 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(solrBenchState.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(solrBenchState.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 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(SolrBenchState solrBenchState)
+ throws SolrServerException, IOException {
+ CollectionAdminRequest.Reload reload = CollectionAdminRequest.reloadCollection(COLLECTION);
+ solrBenchState.client.requestWithBaseUrl(solrBenchState.nodes.getFirst(), 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 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, SolrBenchState solrBenchState)
+ throws SolrServerException, IOException {
+ // Sink the result exactly once, via Blackhole
+ blackhole.consume(benchState.query().process(solrBenchState.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 e3f77a826047..79b26fc9b3cd 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"/>
+
@@ -58,5 +59,11 @@
- id
+
+
+
+
+
+
+ id
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 1452233beee3..791ebe559438 100644
--- a/solr/core/src/java/org/apache/solr/schema/FieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/FieldType.java
@@ -351,6 +351,36 @@ public List createFields(SchemaField field, Object value) {
return f == null ? List.of() : List.of(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 all 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 createFieldsFromAllValues(
+ SchemaField field, Collection values) {
+ List 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 a05a766ac52d..5dd9f140c32f 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 @@
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 @@ protected Pattern getSingleBoundPattern() {
@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 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 @@ protected void init(IndexSchema schema, Map args) {
+ 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 createFields(SchemaField field, Object value) {
- IndexableField indexedField = createField(field, value);
List 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,86 @@ public List createFields(SchemaField field, Object value) {
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 createFieldsFromAllValues(
+ SchemaField field, Collection values) {
+ List 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}.
+ */
+ protected BytesRef encodePackedValues(String field, Collection values) {
+ BytesRefBuilder builder = new BytesRefBuilder();
+ for (Object value : values) {
+ builder.append(encodePackedValue(field, parseRangeValue(value.toString())));
+ }
+ 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 +383,7 @@ public Object toNativeType(Object val) {
* @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 intersects
@@ -288,7 +393,7 @@ public Object toNativeType(Object val) {
* @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 within the
@@ -298,7 +403,7 @@ public Object toNativeType(Object val) {
* @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 crosses the
@@ -308,7 +413,7 @@ public Object toNativeType(Object val) {
* @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 +441,7 @@ public Query getFieldQuery(QParser parser, SchemaField field, String externalVal
// 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 +458,8 @@ public Query getFieldQuery(QParser parser, SchemaField field, String externalVal
+ ")");
}
- 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 c7eaeb5a82b8..5b98d7139fb6 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 @@
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 IndexableField createField(SchemaField field, Object value) {
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 @@ private double[] parseDoubleArray(String str, String description) {
}
@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 @@ protected Query getSpecializedRangeQuery(
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 0d4e4f31d583..edae9f93dd95 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 @@
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 IndexableField createField(SchemaField field, Object value) {
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 @@ private float[] parseFloatArray(String str, String description) {
}
@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 @@ protected Query getSpecializedRangeQuery(
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 b8e76d0a680b..b2faec3534e9 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 @@
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 IndexableField createField(SchemaField field, Object value) {
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 @@ private int[] parseIntArray(String str, String description) {
}
@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 @@ protected Query getSpecializedRangeQuery(
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 5004f3affb31..6057a9f81ef0 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 @@
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 IndexableField createField(SchemaField field, Object value) {
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 @@ private long[] parseLongArray(String str, String description) {
}
@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 @@ protected Query getSpecializedRangeQuery(
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 000000000000..74608b011128
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/MultiBinaryRangeDocValuesQuery.java
@@ -0,0 +1,179 @@
+/*
+ * 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.ScorerSupplier;
+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).
+ *
+ * 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}.
+ *
+ *
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.
+ */
+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 ScorerSupplier scorerSupplier(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
+ }
+ };
+
+ Scorer scorer = new ConstantScoreScorer(score(), scoreMode, iterator);
+ return new DefaultScorerSupplier(scorer);
+ }
+
+ @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 94e113d805e4..fce0cb8ee6dc 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
@@ -194,10 +194,10 @@ public Query parse() throws SyntaxError {
throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid range value: " + rangeValue, e);
}
return switch (criteria) {
- case INTERSECTS -> rangeField.newIntersectsQuery(fieldName, range);
- case WITHIN -> rangeField.newWithinQuery(fieldName, range);
- case CONTAINS -> rangeField.newContainsQuery(fieldName, range);
- case CROSSES -> rangeField.newCrossesQuery(fieldName, range);
+ case INTERSECTS -> rangeField.newIntersectsQuery(schemaField, range);
+ case WITHIN -> rangeField.newWithinQuery(schemaField, range);
+ case CONTAINS -> rangeField.newContainsQuery(schemaField, range);
+ case CROSSES -> rangeField.newCrossesQuery(schemaField, range);
};
} else {
throw new SolrException(
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 0a13b967989d..390d86161812 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 static Document toDocument(
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 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 660acc1bfb1a..43ff7db2fbd5 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id
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 000000000000..c9250ac2d8f9
--- /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.
+ *
+ * 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.
+ *
+ *
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 not 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 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 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", "[1 TO 10]", "[5 TO 20]");
+ 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 e57334779c8e..06bf94895d31 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 @@
/**
* Tests for {@link NumericRangeQParserPlugin} using {@link
* org.apache.solr.schema.numericrange.DoubleRangeField} fields.
+ *
+ * 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 void setUp() throws Exception {
@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 void test1DIntersectsQuery() {
@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 void test1DContainsQuery() {
@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 void testMultiValuedField() {
@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 void testEmptyRangeValue() {
@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 void testGetFieldQueryFullRangeMultipleMatches() {
@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 void testGetFieldQuerySingleBound2D() {
@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 void testGetFieldQueryFieldFormatting() {
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 32b1e1200bdd..031396c811f0 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 @@
/**
* Tests for {@link NumericRangeQParserPlugin} using {@link
* org.apache.solr.schema.numericrange.FloatRangeField} fields.
+ *
+ *
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 void setUp() throws Exception {
@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 void test1DIntersectsQuery() {
@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 void test1DContainsQuery() {
@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 void testMultiValuedField() {
@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 void testEmptyRangeValue() {
@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 void testGetFieldQueryFullRangeMultipleMatches() {
@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 void testGetFieldQuerySingleBound2D() {
@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 void testGetFieldQueryFieldFormatting() {
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 b76db8acf82c..b93c0993edb4 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 @@
/**
* Tests for {@link NumericRangeQParserPlugin} using {@link
* org.apache.solr.schema.numericrange.IntRangeField} fields.
+ *
+ *
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 void setUp() throws Exception {
@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 void test1DIntersectsQuery() {
@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 void test1DContainsQuery() {
// 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 void test1DContainsQuery() {
@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 void test1DCrossesQuery() {
@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 void testEmptyRangeValue() {
@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 void testPointRange() {
@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 void testGetFieldQueryFullRangeMultipleMatches() {
@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: :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 void testGetFieldQuerySingleBound() {
@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: :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 void testGetFieldQuerySingleBound2D() {
@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 void testGetFieldQueryFieldFormatting() {
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 1b774e88e90f..3953920968bd 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 @@
/**
* Tests for {@link NumericRangeQParserPlugin} using {@link
* org.apache.solr.schema.numericrange.LongRangeField} fields.
+ *
+ * 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 void setUp() throws Exception {
@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 void test1DIntersectsQuery() {
@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 void test1DContainsQuery() {
@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 void testEmptyRangeValue() {
@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 void testPointRange() {
@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 void testGetFieldQueryFullRangeMultipleMatches() {
@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: :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 void testGetFieldQuerySingleBound() {
@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 void testGetFieldQuerySingleBound2D() {
@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 void testGetFieldQueryFieldFormatting() {
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 7a9508e083f8..25120c056132 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
@@ -51,17 +51,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 other query parsers also support this field by assuming "contains" semantics for searches.
+|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_ enable sorting or value (`facet.field`) faceting. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
|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 Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
+|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-enabled types, this does _not_ enable sorting or value (`facet.field`) faceting. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
-|DoubleRangeField |Stores single or multi-dimensional ranges of double-precision floating-point numbers, using syntax like `[1.5 TO 2.5]` or `[1.0,2.0 TO 3.0,4.0]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type does not support docValues. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
+|DoubleRangeField |Stores single or multi-dimensional ranges of double-precision floating-point numbers, using syntax like `[1.5 TO 2.5]` or `[1.0,2.0 TO 3.0,4.0]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type supports `docValues="true"` (with `multiValued="true"` or `"false"`) as a query-time filter optimization. Note that, unlike most docValues-enabled types, this does _not_ enable sorting or value (`facet.field`) faceting. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
-|FloatRangeField |Stores single or multi-dimensional ranges of floating-point numbers, using syntax like `[1.5 TO 4.5]` or `[1.0,2.0 TO 3.0,4.0]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type does not support docValues. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
+|FloatRangeField |Stores single or multi-dimensional ranges of floating-point numbers, using syntax like `[1.5 TO 4.5]` or `[1.0,2.0 TO 3.0,4.0]`. Up to 4 dimensions are supported. Dimensionality is specified on new field-types using a `numDimensions` property, and all values for a particular field must have exactly this number of dimensions. Field type supports `docValues="true"` (with `multiValued="true"` or `"false"`) as a query-time filter optimization. Note that, unlike most docValues-enabled types, this does _not_ enable sorting or value (`facet.field`) faceting. Typically queried using the xref:query-guide:other-parsers.adoc#numeric-range-query-parser[Numeric Range Query Parser], though the Lucene and other query parsers also support this field by assuming "contains" semantics for searches.
|NestPathField | Specialized field type storing enhanced information, when xref:indexing-nested-documents.adoc#schema-configuration[working with nested documents].