This is an automated email from the ASF dual-hosted git repository.
gerlowskija pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new 51b45f71279 SOLR-13309: Introduce LongRangeField to expose Lucene
'LongRange' (#4192)
51b45f71279 is described below
commit 51b45f712794d2e59c76cd303d46ece63feacbf8
Author: Jason Gerlowski <[email protected]>
AuthorDate: Wed Mar 11 13:08:51 2026 -0400
SOLR-13309: Introduce LongRangeField to expose Lucene 'LongRange' (#4192)
This commit adds a new field type, LongRangeField, that can be used to
hold singular or multi-dimensional (up to 4) ranges of longs.
LongRangeField is compatible with the previously added
`{!numericRange}` and supports similar syntax.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
.../unreleased/SOLR-13309-long-range-field.yml | 7 +
.../numericrange/AbstractNumericRangeField.java | 317 +++++++++++++++
.../solr/schema/numericrange/IntRangeField.java | 185 ++-------
.../solr/schema/numericrange/LongRangeField.java | 292 ++++++++++++++
.../java/org/apache/solr/search/QParserPlugin.java | 4 +-
...rPlugin.java => NumericRangeQParserPlugin.java} | 92 ++---
...schema-intrange.xml => schema-numericrange.xml} | 23 +-
.../schema/numericrange/LongRangeFieldTest.java | 357 ++++++++++++++++
.../org/apache/solr/search/QueryEqualityTest.java | 6 +-
....java => NumericRangeQParserPluginIntTest.java} | 11 +-
.../NumericRangeQParserPluginLongTest.java | 449 +++++++++++++++++++++
.../pages/field-types-included-with-solr.adoc | 2 +
.../modules/query-guide/pages/other-parsers.adoc | 11 +-
13 files changed, 1527 insertions(+), 229 deletions(-)
diff --git a/changelog/unreleased/SOLR-13309-long-range-field.yml
b/changelog/unreleased/SOLR-13309-long-range-field.yml
new file mode 100644
index 00000000000..78dab312b6f
--- /dev/null
+++ b/changelog/unreleased/SOLR-13309-long-range-field.yml
@@ -0,0 +1,7 @@
+title: Introduce new `LongRangeField` field type and (experimental)
`{!numericRange}` query parser for storing and querying long ranges
+type: added
+authors:
+ - name: Jason Gerlowski
+links:
+ - name: SOLR-13309
+ url: https://issues.apache.org/jira/browse/SOLR-13309
diff --git
a/solr/core/src/java/org/apache/solr/schema/numericrange/AbstractNumericRangeField.java
b/solr/core/src/java/org/apache/solr/schema/numericrange/AbstractNumericRangeField.java
new file mode 100644
index 00000000000..8d61af15df8
--- /dev/null
+++
b/solr/core/src/java/org/apache/solr/schema/numericrange/AbstractNumericRangeField.java
@@ -0,0 +1,317 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.response.TextResponseWriter;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.PrimitiveFieldType;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.QParser;
+import org.apache.solr.uninverting.UninvertingReader.Type;
+
+/**
+ * Abstract base class for numeric range field types that wrap Lucene's
multi-dimensional range
+ * fields (e.g., {@link org.apache.lucene.document.IntRange}, {@link
+ * org.apache.lucene.document.LongRange}).
+ *
+ * <p>Provides common infrastructure for range field types including:
+ *
+ * <ul>
+ * <li>Configurable number of dimensions (1–4) via the {@code numDimensions}
schema attribute
+ * <li>Shared regex patterns for parsing range value strings
+ * <li>Standard field lifecycle methods (init, createFields, write, etc.)
+ * </ul>
+ *
+ * <p>Concrete subclasses must implement {@link #parseRangeValue(String)} to
parse the string
+ * representation into a type-specific range value, and {@link
#createField(SchemaField, Object)} to
+ * produce the underlying Lucene {@link IndexableField}.
+ *
+ * @see IntRangeField
+ * @see LongRangeField
+ */
+public abstract class AbstractNumericRangeField extends PrimitiveFieldType {
+
+ /**
+ * Marker interface for parsed range value objects. Implemented by the inner
{@code RangeValue}
+ * classes of concrete subclasses so that {@link #toNativeType(Object)} can
identify already-
+ * parsed values without knowing the concrete type.
+ *
+ * <p>Concrete subclasses override {@link #parseRangeValue(String)} with a
covariant return type
+ * so callers within the subclass receive the concrete type directly (e.g.
{@code
+ * IntRangeField.RangeValue}) with no casting required.
+ */
+ public interface NumericRangeValue {
+ int getDimensions();
+ }
+
+ /** Regex fragment matching a comma-separated list of signed integers (no
decimal points). */
+ protected static final String COMMA_DELIMITED_NUMS =
"-?\\d+(?:\\s*,\\s*-?\\d+)*";
+
+ private static final String RANGE_PATTERN_STR =
+ "\\[\\s*(" + COMMA_DELIMITED_NUMS + ")\\s+TO\\s+(" +
COMMA_DELIMITED_NUMS + ")\\s*\\]";
+
+ /** Pre-compiled pattern matching {@code [min1,min2,... TO max1,max2,...]}
range syntax. */
+ protected static final Pattern RANGE_PATTERN_REGEX =
Pattern.compile(RANGE_PATTERN_STR);
+
+ /** Pre-compiled pattern matching a single (multi-dimensional) bound, e.g.
{@code 1,2,3}. */
+ protected static final Pattern SINGLE_BOUND_PATTERN =
+ Pattern.compile("^" + COMMA_DELIMITED_NUMS + "$");
+
+ /** Configured number of dimensions for this field type; defaults to 1. */
+ protected int numDimensions = 1;
+
+ @Override
+ protected boolean enableDocValuesByDefault() {
+ return false; // Range fields do not support docValues
+ }
+
+ @Override
+ protected void init(IndexSchema schema, Map<String, String> args) {
+ super.init(schema, args);
+
+ String numDimensionsStr = args.remove("numDimensions");
+ if (numDimensionsStr != null) {
+ numDimensions = Integer.parseInt(numDimensionsStr);
+ if (numDimensions < 1 || numDimensions > 4) {
+ throw new SolrException(
+ ErrorCode.SERVER_ERROR,
+ "numDimensions must be between 1 and 4, but was ["
+ + numDimensions
+ + "] for field type "
+ + typeName);
+ }
+ }
+
+ // 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
+ public List<IndexableField> createFields(SchemaField field, Object value) {
+ IndexableField indexedField = createField(field, value);
+ List<IndexableField> fields = new ArrayList<>();
+
+ if (indexedField != null) {
+ fields.add(indexedField);
+ }
+
+ if (field.stored()) {
+ fields.add(getStoredField(field, value.toString()));
+ }
+
+ return fields;
+ }
+
+ protected StoredField getStoredField(SchemaField sf, Object value) {
+ return new StoredField(sf.getName(), value.toString());
+ }
+
+ @Override
+ public void write(TextResponseWriter writer, String name, IndexableField f)
throws IOException {
+ writer.writeStr(name, toExternal(f), false);
+ }
+
+ @Override
+ public SortField getSortField(SchemaField field, boolean top) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Cannot sort on " + getClass().getSimpleName() + ": " +
field.getName());
+ }
+
+ @Override
+ public Type getUninversionType(SchemaField sf) {
+ return null; // No field cache support
+ }
+
+ @Override
+ public String toInternal(String val) {
+ // Validate format and return as-is
+ parseRangeValue(val);
+ return val;
+ }
+
+ @Override
+ public String toExternal(IndexableField f) {
+ return f.stringValue();
+ }
+
+ @Override
+ public Object toNativeType(Object val) {
+ if (val == null) return null;
+ if (val instanceof NumericRangeValue) return val;
+ return parseRangeValue(val.toString());
+ }
+
+ /**
+ * Parse a range value string into a type-specific range value object.
+ *
+ * <p>Implementations should accept the {@code [min1,min2,... TO
max1,max2,...]} bracket notation
+ * (using {@link #RANGE_PATTERN_REGEX}) and validate that:
+ *
+ * <ul>
+ * <li>The format matches the expected pattern
+ * <li>The number of dimensions in the value matches {@link #numDimensions}
+ * <li>Each min value is less than or equal to the corresponding max value
+ * </ul>
+ *
+ * <p>Subclasses should override this with a covariant return type (their
concrete inner {@code
+ * RangeValue} class) so that internal callers receive the fully-typed value
without casting.
+ *
+ * @param value the string value in bracket notation
+ * @return a {@link NumericRangeValue} holding the parsed min/max arrays
+ * @throws SolrException if value format is invalid
+ */
+ public abstract NumericRangeValue parseRangeValue(String value);
+
+ /**
+ * Parses a single N-dimensional point expressed as a comma-separated string
(e.g. {@code "5"} or
+ * {@code "5,10"}) into a {@link NumericRangeValue} where both mins and maxs
are set to the parsed
+ * bound.
+ *
+ * <p>This is used by {@link #getFieldQuery} to support the "single bound"
query shorthand, where
+ * a bare coordinate is treated as a degenerate range {@code [p TO p]}.
Dimension-count validation
+ * against {@link #numDimensions} is performed by the caller and does not
need to be repeated
+ * here.
+ *
+ * <p>Subclasses should override with a covariant return type so that
internal callers receive the
+ * concrete {@code RangeValue} type without casting.
+ *
+ * @param value a comma-separated numeric string (e.g. {@code "5,10"} for a
2D point)
+ * @return a {@link NumericRangeValue} with mins and maxs both equal to the
parsed bound
+ * @throws SolrException if the string contains non-numeric values
+ */
+ public abstract NumericRangeValue parseSingleBound(String value);
+
+ /**
+ * Creates a Lucene query that matches indexed documents whose stored range
<em>contains</em> the
+ * query range described by {@code rangeValue}.
+ *
+ * <p>This is the default query semantics used by {@link #getFieldQuery}.
Queries with other match
+ * semantics (intersects, within, crosses) are available via {@link
+ * org.apache.solr.search.numericrange.NumericRangeQParserPlugin}.
+ *
+ * <p>The {@code rangeValue} argument may originate from either {@link
#parseRangeValue} (full
+ * {@code [min TO max]} syntax) or {@link #parseSingleBound} (point query
shorthand). In the point
+ * case, mins and maxs are equal, so the query finds documents whose range
contains that exact
+ * point.
+ *
+ * @param field the name of the field to query
+ * @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);
+
+ /**
+ * Creates a Lucene query that matches indexed documents whose stored range
<em>intersects</em>
+ * the query range described by {@code rangeValue}.
+ *
+ * @param field the name of the field to query
+ * @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);
+
+ /**
+ * Creates a Lucene query that matches indexed documents whose stored range
is <em>within</em> the
+ * query range described by {@code rangeValue}.
+ *
+ * @param field the name of the field to query
+ * @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);
+
+ /**
+ * Creates a Lucene query that matches indexed documents whose stored range
<em>crosses</em> the
+ * boundaries of the query range described by {@code rangeValue}.
+ *
+ * @param field the name of the field to query
+ * @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);
+
+ /**
+ * Creates a query for this field that matches docs where the query-range is
fully contained by
+ * the field value.
+ *
+ * <p>Queries requiring other match semantics can use {@link
+ * org.apache.solr.search.numericrange.NumericRangeQParserPlugin}
+ *
+ * @param parser The {@link org.apache.solr.search.QParser} calling the
method
+ * @param field The {@link org.apache.solr.schema.SchemaField} of the field
to search
+ * @param externalVal The String representation of the value to search.
Supports both a
+ * (multi-)dimensional range of the form [1,2 TO 3,4], or a single
(multi-)dimensional bound
+ * (e.g. 1,2). In the latter case, the single bound will be used as both
the min and max. Both
+ * formats use "contains" query semantics to find indexed ranges that
contain the query range.
+ * @return Query for this field using contains semantics
+ */
+ @Override
+ public Query getFieldQuery(QParser parser, SchemaField field, String
externalVal) {
+ if (externalVal == null || externalVal.trim().isEmpty()) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, "Query value cannot be
null or empty");
+ }
+
+ String trimmed = externalVal.trim();
+
+ // Check if it's the full range syntax: [min1,min2 TO max1,max2]
+ if (RANGE_PATTERN_REGEX.matcher(trimmed).matches()) {
+ final var rangeValue = parseRangeValue(trimmed);
+ return newContainsQuery(field.getName(), rangeValue);
+ }
+
+ // Syntax sugar: also accept a single-bound (i.e pX,pY,pZ)
+ if (SINGLE_BOUND_PATTERN.matcher(trimmed).matches()) {
+ final var singleBoundRange = parseSingleBound(trimmed);
+
+ if (singleBoundRange.getDimensions() != numDimensions) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Single bound dimensions ("
+ + singleBoundRange.getDimensions()
+ + ") do not match field type numDimensions ("
+ + numDimensions
+ + ")");
+ }
+
+ return newContainsQuery(field.getName(), singleBoundRange);
+ }
+
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid query format. Expected either a range [min TO max] or a
single bound to search for, got: "
+ + externalVal);
+ }
+}
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 0dd63b366bb..55a966c312f 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
@@ -16,25 +16,14 @@
*/
package org.apache.solr.schema.numericrange;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.apache.lucene.document.IntRange;
-import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.search.Query;
-import org.apache.lucene.search.SortField;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.response.TextResponseWriter;
-import org.apache.solr.schema.IndexSchema;
-import org.apache.solr.schema.PrimitiveFieldType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.QParser;
-import org.apache.solr.uninverting.UninvertingReader.Type;
/**
* Field type for integer ranges with support for 1-4 dimensions.
@@ -84,49 +73,9 @@ import org.apache.solr.uninverting.UninvertingReader.Type;
* therefore can't be used for sorting, faceting, etc.
*
* @see IntRange
- * @see org.apache.solr.search.numericrange.IntRangeQParserPlugin
+ * @see org.apache.solr.search.numericrange.NumericRangeQParserPlugin
*/
-public class IntRangeField extends PrimitiveFieldType {
-
- private static final String COMMA_DELIMITED_INTS =
"-?\\d+(?:\\s*,\\s*-?\\d+)*";
- private static final String RANGE_PATTERN =
- "\\[\\s*(" + COMMA_DELIMITED_INTS + ")\\s+TO\\s+(" +
COMMA_DELIMITED_INTS + ")\\s*\\]";
- private static final Pattern RANGE_PATTERN_REGEX =
Pattern.compile(RANGE_PATTERN);
- private static final Pattern SINGLE_BOUND_PATTERN =
- Pattern.compile("^" + COMMA_DELIMITED_INTS + "$");
-
- private int numDimensions = 1;
-
- @Override
- protected boolean enableDocValuesByDefault() {
- return false; // IntRange does not support docValues
- }
-
- @Override
- protected void init(IndexSchema schema, Map<String, String> args) {
- super.init(schema, args);
-
- String numDimensionsStr = args.remove("numDimensions");
- if (numDimensionsStr != null) {
- numDimensions = Integer.parseInt(numDimensionsStr);
- if (numDimensions < 1 || numDimensions > 4) {
- throw new SolrException(
- ErrorCode.SERVER_ERROR,
- "numDimensions must be between 1 and 4, but was ["
- + numDimensions
- + "] for field type "
- + typeName);
- }
- }
-
- // IntRange does not support docValues - validate this wasn't explicitly
set
- if (hasProperty(DOC_VALUES)) {
- throw new SolrException(
- ErrorCode.SERVER_ERROR,
- "docValues=true enabled but IntRangeField does not support docValues
for field type "
- + typeName);
- }
- }
+public class IntRangeField extends AbstractNumericRangeField {
@Override
public IndexableField createField(SchemaField field, Object value) {
@@ -140,58 +89,6 @@ public class IntRangeField extends PrimitiveFieldType {
return new IntRange(field.getName(), rangeValue.mins, rangeValue.maxs);
}
- @Override
- public List<IndexableField> createFields(SchemaField field, Object value) {
- IndexableField indexedField = createField(field, value);
- List<IndexableField> fields = new ArrayList<>();
-
- if (indexedField != null) {
- fields.add(indexedField);
- }
-
- if (field.stored()) {
- String valueStr = value.toString();
- fields.add(getStoredField(field, valueStr));
- }
-
- return fields;
- }
-
- @Override
- public void write(TextResponseWriter writer, String name, IndexableField f)
throws IOException {
- writer.writeStr(name, toExternal(f), false);
- }
-
- @Override
- public SortField getSortField(SchemaField field, boolean top) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST, "Cannot sort on IntRangeField: " +
field.getName());
- }
-
- @Override
- public Type getUninversionType(SchemaField sf) {
- return null; // No field cache support
- }
-
- @Override
- public String toInternal(String val) {
- // Validate format and return as-is
- parseRangeValue(val);
- return val;
- }
-
- @Override
- public String toExternal(IndexableField f) {
- return f.stringValue();
- }
-
- @Override
- public Object toNativeType(Object val) {
- if (val == null) return null;
- if (val instanceof RangeValue) return val;
- return parseRangeValue(val.toString());
- }
-
/**
* Parse a range value string into a RangeValue object.
*
@@ -199,6 +96,7 @@ public class IntRangeField extends PrimitiveFieldType {
* @return parsed RangeValue
* @throws SolrException if value format is invalid
*/
+ @Override
public RangeValue parseRangeValue(String value) {
if (value == null || value.trim().isEmpty()) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Range value cannot be
null or empty");
@@ -254,6 +152,12 @@ public class IntRangeField extends PrimitiveFieldType {
return new RangeValue(mins, maxs);
}
+ @Override
+ public NumericRangeValue parseSingleBound(String value) {
+ final var singleBoundTyped = parseIntArray(value, "single bound values");
+ return new RangeValue(singleBoundTyped, singleBoundTyped);
+ }
+
/**
* Parse a comma-separated string of integers into an array.
*
@@ -279,60 +183,28 @@ public class IntRangeField extends PrimitiveFieldType {
return result;
}
- protected StoredField getStoredField(SchemaField sf, Object value) {
- return new StoredField(sf.getName(), value.toString());
+ @Override
+ public Query newContainsQuery(String fieldName, NumericRangeValue
rangeValue) {
+ final var rangeValueTyped = (RangeValue) rangeValue;
+ return IntRange.newContainsQuery(fieldName, rangeValueTyped.mins,
rangeValueTyped.maxs);
}
- /**
- * Creates a query for this field that matches docs where the query-range is
fully contained by
- * the field value.
- *
- * <p>Queries requiring other match semantics can use {@link
- * org.apache.solr.search.numericrange.IntRangeQParserPlugin}
- *
- * @param parser The {@link org.apache.solr.search.QParser} calling the
method
- * @param field The {@link org.apache.solr.schema.SchemaField} of the field
to search
- * @param externalVal The String representation of the value to search.
Supports both a
- * (multi-)dimensional range of the form [1,2 TO 3,4], or a single
(multi-)dimensional bound
- * (e.g. 1,2). In the latter case, the single bound will be used as both
the min and max. Both
- * formats use "contains" query semantics to find indexed ranges that
contain the query range.
- * @return Query for this field using contains semantics
- */
@Override
- public Query getFieldQuery(QParser parser, SchemaField field, String
externalVal) {
- if (externalVal == null || externalVal.trim().isEmpty()) {
- throw new SolrException(ErrorCode.BAD_REQUEST, "Query value cannot be
null or empty");
- }
-
- String trimmed = externalVal.trim();
-
- // Check if it's the full range syntax: [min1,min2 TO max1,max2]
- if (RANGE_PATTERN_REGEX.matcher(trimmed).matches()) {
- RangeValue rangeValue = parseRangeValue(trimmed);
- return IntRange.newContainsQuery(field.getName(), rangeValue.mins,
rangeValue.maxs);
- }
-
- // Syntax sugar: also accept a single-bound (i.e pX,pY,pZ)
- if (SINGLE_BOUND_PATTERN.matcher(trimmed).matches()) {
- int[] bound = parseIntArray(trimmed, "single bound values");
-
- if (bound.length != numDimensions) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Single bound dimensions ("
- + bound.length
- + ") do not match field type numDimensions ("
- + numDimensions
- + ")");
- }
+ public Query newIntersectsQuery(String fieldName, NumericRangeValue
rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return IntRange.newIntersectsQuery(fieldName, rv.mins, rv.maxs);
+ }
- return IntRange.newContainsQuery(field.getName(), bound, bound);
- }
+ @Override
+ public Query newWithinQuery(String fieldName, NumericRangeValue rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return IntRange.newWithinQuery(fieldName, rv.mins, rv.maxs);
+ }
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Invalid query format. Expected either a range [min TO max] or a
single bound to search for, got: "
- + externalVal);
+ @Override
+ public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue)
{
+ final var rv = (RangeValue) rangeValue;
+ return IntRange.newCrossesQuery(fieldName, rv.mins, rv.maxs);
}
@Override
@@ -385,8 +257,8 @@ public class IntRangeField extends PrimitiveFieldType {
}
}
- /** Simple holder class for parsed range values. */
- public static class RangeValue {
+ /** Simple holder class for parsed integer range values. */
+ public static class RangeValue implements
AbstractNumericRangeField.NumericRangeValue {
public final int[] mins;
public final int[] maxs;
@@ -395,6 +267,7 @@ public class IntRangeField extends PrimitiveFieldType {
this.maxs = maxs;
}
+ @Override
public int getDimensions() {
return mins.length;
}
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
new file mode 100644
index 00000000000..e4eaabb768e
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/numericrange/LongRangeField.java
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.schema.numericrange;
+
+import java.util.regex.Matcher;
+import org.apache.lucene.document.LongRange;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.search.Query;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.QParser;
+
+/**
+ * Field type for long ranges with support for 1-4 dimensions.
+ *
+ * <p>This field type wraps Lucene's {@link LongRange} to provide storage and
querying of long range
+ * values. Ranges can be 1-dimensional (simple ranges), 2-dimensional
(bounding boxes),
+ * 3-dimensional (bounding cubes), or 4-dimensional (tesseracts).
+ *
+ * <h2>Value Format</h2>
+ *
+ * Values are specified using bracket notation with a TO keyword separator:
+ *
+ * <ul>
+ * <li>1D: {@code [10 TO 20]}
+ * <li>2D: {@code [10,20 TO 30,40]}
+ * <li>3D: {@code [10,20,30 TO 40,50,60]}
+ * <li>4D: {@code [10,20,30,40 TO 50,60,70,80]}
+ * </ul>
+ *
+ * As the name suggests minimum values (those on the left) must always be less
than or equal to the
+ * maximum value for the corresponding dimension. Long values outside the
range of {@code int} are
+ * fully supported (e.g. {@code [3000000000 TO 4000000000]}).
+ *
+ * <h2>Schema Configuration</h2>
+ *
+ * <pre>
+ * <fieldType name="longrange"
class="org.apache.solr.schema.numericrange.LongRangeField"
numDimensions="1"/>
+ * <fieldType name="longrange2d"
class="org.apache.solr.schema.numericrange.LongRangeField"
numDimensions="2"/>
+ * <field name="long_range" type="longrange" indexed="true"
stored="true"/>
+ * <field name="long_range_2d" type="longrange2d" indexed="true"
stored="true"/>
+ * </pre>
+ *
+ * <h2>Querying</h2>
+ *
+ * Use the {@code numericRange} query parser for range queries with support
for different query
+ * types:
+ *
+ * <ul>
+ * <li>Intersects: {@code {!numericRange criteria="intersects"
field=long_range}[100 TO 200]}
+ * <li>Within: {@code {!numericRange criteria="within" field=long_range}[0
TO 300]}
+ * <li>Contains: {@code {!numericRange criteria="contains"
field=long_range}[150 TO 175]}
+ * <li>Crosses: {@code {!numericRange criteria="crosses"
field=long_range}[150 TO 250]}
+ * </ul>
+ *
+ * <h2>Limitations</h2>
+ *
+ * The main limitation of this field type is that it doesn't support docValues
or uninversion, and
+ * therefore can't be used for sorting, faceting, etc.
+ *
+ * @see LongRange
+ * @see org.apache.solr.search.numericrange.NumericRangeQParserPlugin
+ */
+public class LongRangeField extends AbstractNumericRangeField {
+
+ @Override
+ public IndexableField createField(SchemaField field, Object value) {
+ if (!field.indexed() && !field.stored()) {
+ return null;
+ }
+
+ String valueStr = value.toString();
+ RangeValue rangeValue = parseRangeValue(valueStr);
+
+ return new LongRange(field.getName(), rangeValue.mins, rangeValue.maxs);
+ }
+
+ /**
+ * Parse a range value string into a RangeValue object.
+ *
+ * @param value the string value in format "[min1,min2,... TO max1,max2,...]"
+ * @return parsed RangeValue
+ * @throws SolrException if value format is invalid
+ */
+ @Override
+ public RangeValue parseRangeValue(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, "Range value cannot be
null or empty");
+ }
+
+ Matcher matcher = RANGE_PATTERN_REGEX.matcher(value.trim());
+ if (!matcher.matches()) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid range format. Expected: [min1,min2,... TO max1,max2,...]
where min and max values are longs, but got: "
+ + value);
+ }
+
+ String minPart = matcher.group(1).trim();
+ String maxPart = matcher.group(2).trim();
+
+ long[] mins = parseLongArray(minPart, "min values");
+ long[] maxs = parseLongArray(maxPart, "max values");
+
+ if (mins.length != maxs.length) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Min and max dimensions must match. Min dimensions: "
+ + mins.length
+ + ", max dimensions: "
+ + maxs.length);
+ }
+
+ if (mins.length != numDimensions) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Range dimensions ("
+ + mins.length
+ + ") do not match field type numDimensions ("
+ + numDimensions
+ + ")");
+ }
+
+ // Validate that min <= max for each dimension
+ for (int i = 0; i < mins.length; i++) {
+ if (mins[i] > maxs[i]) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Min value must be <= max value for dimension "
+ + i
+ + ". Min: "
+ + mins[i]
+ + ", Max: "
+ + maxs[i]);
+ }
+ }
+
+ return new RangeValue(mins, maxs);
+ }
+
+ @Override
+ public NumericRangeValue parseSingleBound(String value) {
+ final var singleBoundTyped = parseLongArray(value, "single bound values");
+ return new RangeValue(singleBoundTyped, singleBoundTyped);
+ }
+
+ /**
+ * Parse a comma-separated string of longs into an array.
+ *
+ * @param str the string to parse
+ * @param description description for error messages
+ * @return array of parsed longs
+ */
+ private long[] parseLongArray(String str, String description) {
+ String[] parts = str.split(",");
+ long[] result = new long[parts.length];
+
+ for (int i = 0; i < parts.length; i++) {
+ try {
+ result[i] = Long.parseLong(parts[i].trim());
+ } catch (NumberFormatException e) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid long in " + description + ": '" + parts[i].trim() + "'",
+ e);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public Query newContainsQuery(String fieldName, NumericRangeValue
rangeValue) {
+ final var rangeValueTyped = (RangeValue) rangeValue;
+ return LongRange.newContainsQuery(fieldName, rangeValueTyped.mins,
rangeValueTyped.maxs);
+ }
+
+ @Override
+ public Query newIntersectsQuery(String fieldName, NumericRangeValue
rangeValue) {
+ final var rv = (RangeValue) rangeValue;
+ return LongRange.newIntersectsQuery(fieldName, 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);
+ }
+
+ @Override
+ public Query newCrossesQuery(String fieldName, NumericRangeValue rangeValue)
{
+ final var rv = (RangeValue) rangeValue;
+ return LongRange.newCrossesQuery(fieldName, rv.mins, rv.maxs);
+ }
+
+ @Override
+ protected Query getSpecializedRangeQuery(
+ QParser parser,
+ SchemaField field,
+ String part1,
+ String part2,
+ boolean minInclusive,
+ boolean maxInclusive) {
+ // For standard range syntax field:[value TO value], default to contains
query
+ if (part1 == null || part2 == null) {
+ return super.getSpecializedRangeQuery(
+ parser, field, part1, part2, minInclusive, maxInclusive);
+ }
+
+ // Parse the range bounds as single-dimensional values
+ long min, max;
+ try {
+ min = Long.parseLong(part1.trim());
+ max = Long.parseLong(part2.trim());
+ } catch (NumberFormatException e) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid long values in range query: [" + part1 + " TO " + part2 +
"]",
+ e);
+ }
+
+ if (!minInclusive) {
+ min = (min == Long.MAX_VALUE) ? min : min + 1;
+ }
+ if (!maxInclusive) {
+ max = (max == Long.MIN_VALUE) ? max : max - 1;
+ }
+
+ // Build arrays for the query based on configured dimensions
+ long[] mins = new long[numDimensions];
+ long[] maxs = new long[numDimensions];
+
+ // For now, only support 1D range syntax with field:[X TO Y]
+ if (numDimensions == 1) {
+ mins[0] = min;
+ maxs[0] = max;
+ return LongRange.newContainsQuery(field.getName(), mins, maxs);
+ } else {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Standard range query syntax only supports 1D ranges. "
+ + "Use {!numericRange ...} for multi-dimensional queries.");
+ }
+ }
+
+ /** Simple holder class for parsed long range values. */
+ public static class RangeValue implements
AbstractNumericRangeField.NumericRangeValue {
+ public final long[] mins;
+ public final long[] maxs;
+
+ public RangeValue(long[] mins, long[] maxs) {
+ this.mins = mins;
+ this.maxs = maxs;
+ }
+
+ @Override
+ public int getDimensions() {
+ return mins.length;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < mins.length; i++) {
+ if (i > 0) sb.append(",");
+ sb.append(mins[i]);
+ }
+ sb.append(" TO ");
+ for (int i = 0; i < maxs.length; i++) {
+ if (i > 0) sb.append(",");
+ sb.append(maxs[i]);
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
b/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
index ba27a86b1f2..4504a8086e7 100644
--- a/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
@@ -28,7 +28,7 @@ import org.apache.solr.search.join.GraphQParserPlugin;
import org.apache.solr.search.join.HashRangeQParserPlugin;
import org.apache.solr.search.mlt.MLTContentQParserPlugin;
import org.apache.solr.search.mlt.MLTQParserPlugin;
-import org.apache.solr.search.numericrange.IntRangeQParserPlugin;
+import org.apache.solr.search.numericrange.NumericRangeQParserPlugin;
import org.apache.solr.search.vector.KnnQParserPlugin;
import org.apache.solr.search.vector.VectorSimilarityQParserPlugin;
import org.apache.solr.util.plugin.NamedListInitializedPlugin;
@@ -91,7 +91,7 @@ public abstract class QParserPlugin implements
NamedListInitializedPlugin {
map.put(KnnQParserPlugin.NAME, new KnnQParserPlugin());
map.put(VectorSimilarityQParserPlugin.NAME, new
VectorSimilarityQParserPlugin());
map.put(FuzzyQParserPlugin.NAME, new FuzzyQParserPlugin());
- map.put(IntRangeQParserPlugin.NAME, new IntRangeQParserPlugin());
+ map.put(NumericRangeQParserPlugin.NAME, new NumericRangeQParserPlugin());
standardPlugins = Collections.unmodifiableMap(map);
}
diff --git
a/solr/core/src/java/org/apache/solr/search/numericrange/IntRangeQParserPlugin.java
b/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
similarity index 72%
rename from
solr/core/src/java/org/apache/solr/search/numericrange/IntRangeQParserPlugin.java
rename to
solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
index 5c1e8a6402a..b19be4b5b16 100644
---
a/solr/core/src/java/org/apache/solr/search/numericrange/IntRangeQParserPlugin.java
+++
b/solr/core/src/java/org/apache/solr/search/numericrange/NumericRangeQParserPlugin.java
@@ -17,30 +17,31 @@
package org.apache.solr.search.numericrange;
import java.util.Locale;
-import org.apache.lucene.document.IntRange;
import org.apache.lucene.search.Query;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.numericrange.AbstractNumericRangeField;
+import
org.apache.solr.schema.numericrange.AbstractNumericRangeField.NumericRangeValue;
import org.apache.solr.schema.numericrange.IntRangeField;
-import org.apache.solr.schema.numericrange.IntRangeField.RangeValue;
+import org.apache.solr.schema.numericrange.LongRangeField;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.QueryParsing;
import org.apache.solr.search.SyntaxError;
/**
- * Query parser for IntRangeField with support for different query
relationship types.
+ * Query parser for numeric range fields with support for different query
relationship types.
*
- * <p>This parser enables queries against {@link IntRangeField} fields with
explicit control over
- * the query relationship type (intersects, within, contains, crosses).
+ * <p>This parser enables queries against {@link IntRangeField} and {@link
LongRangeField} fields
+ * with explicit control over the query relationship type (intersects, within,
contains, crosses).
*
* <h2>Parameters</h2>
*
* <ul>
- * <li><b>field</b> (required): The IntRangeField to query
+ * <li><b>field</b> (required): The numeric range field to query
* <li><b>criteria</b> (required): Query relationship criteria. One of:
intersects, within,
* contains, crosses
* </ul>
@@ -58,28 +59,26 @@ import org.apache.solr.search.SyntaxError;
* <h2>Example Usage</h2>
*
* <pre>
- * // 1D range queries
+ * // IntRangeField queries
* {!numericRange criteria="intersects" field=price_range}[100 TO 200]
* {!numericRange criteria="within" field=price_range}[0 TO 300]
* {!numericRange criteria="contains" field=price_range}[150 TO 175]
* {!numericRange criteria="crosses" field=price_range}[150 TO 250]
*
- * // 2D range queries (bounding boxes)
+ * // LongRangeField queries
+ * {!numericRange criteria="intersects" field=long_range}[1000000000 TO
2000000000]
+ * {!numericRange criteria="within" field=long_range}[0 TO 9999999999]
+ *
+ * // Multi-dimensional queries (bounding boxes, cubes, tesseracts)
* {!numericRange criteria="intersects" field=bbox}[0,0 TO 10,10]
* {!numericRange criteria="within" field=bbox}[-10,-10 TO 20,20]
- *
- * // 3D range queries (bounding cubes)
- * {!numericRange criteria="intersects" field=cube}[0,0,0 TO 10,10,10]
- *
- * // 4D range queries (tesseracts)
- * {!numericRange criteria="intersects" field=tesseract}[0,0,0,0 TO
10,10,10,10]
* </pre>
*
* @see IntRangeField
- * @see IntRange
+ * @see LongRangeField
* @lucene.experimental
*/
-public class IntRangeQParserPlugin extends QParserPlugin {
+public class NumericRangeQParserPlugin extends QParserPlugin {
/** Query relationship criteria for range queries. */
public enum QueryCriteria {
@@ -171,7 +170,7 @@ public class IntRangeQParserPlugin extends QParserPlugin {
throw new SolrException(ErrorCode.BAD_REQUEST, "Range value cannot
be empty");
}
- // Validate field exists and is an IntRangeField
+ // Validate field exists and is a supported numeric range field type
SchemaField schemaField;
try {
schemaField = req.getSchema().getField(fieldName);
@@ -179,56 +178,27 @@ public class IntRangeQParserPlugin extends QParserPlugin {
throw new SolrException(ErrorCode.BAD_REQUEST, "Field not found: " +
fieldName, e);
}
- if (!(schemaField.getType() instanceof IntRangeField)) {
+ if (schemaField.getType() instanceof AbstractNumericRangeField
rangeField) {
+ NumericRangeValue range;
+ try {
+ range = rangeField.parseRangeValue(rangeValue);
+ } catch (SolrException e) {
+ 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);
+ };
+ } else {
throw new SolrException(
ErrorCode.BAD_REQUEST,
"Field '"
+ fieldName
- + "' must be of type IntRangeField, but is: "
+ + "' must be a numeric range field type, but is: "
+ schemaField.getType().getTypeName());
}
-
- IntRangeField fieldType = (IntRangeField) schemaField.getType();
-
- // Parse the range value
- RangeValue range;
- try {
- range = fieldType.parseRangeValue(rangeValue);
- } catch (SolrException e) {
- throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid range value:
" + rangeValue, e);
- }
-
- // Create appropriate query based on criteria
- return createRangeQuery(fieldName, range.mins, range.maxs, criteria);
- }
-
- /**
- * Create the appropriate Lucene query based on the query criteria.
- *
- * @param fieldName the field to query
- * @param mins minimum values for each dimension
- * @param maxs maximum values for each dimension
- * @param criteria the query relationship criteria
- * @return the created Lucene Query
- */
- private Query createRangeQuery(
- String fieldName, int[] mins, int[] maxs, QueryCriteria criteria) {
- switch (criteria) {
- case INTERSECTS:
- return IntRange.newIntersectsQuery(fieldName, mins, maxs);
-
- case WITHIN:
- return IntRange.newWithinQuery(fieldName, mins, maxs);
-
- case CONTAINS:
- return IntRange.newContainsQuery(fieldName, mins, maxs);
-
- case CROSSES:
- return IntRange.newCrossesQuery(fieldName, mins, maxs);
-
- default:
- throw new AssertionError("Unhandled QueryCriteria: " + criteria);
- }
}
};
}
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-intrange.xml
b/solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml
similarity index 67%
rename from solr/core/src/test-files/solr/collection1/conf/schema-intrange.xml
rename to solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml
index bcfd1b5dc1a..4d70167a59e 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-intrange.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-numericrange.xml
@@ -16,8 +16,8 @@
limitations under the License.
-->
-<!-- Test schema for IntRangeField -->
-<schema name="intrange-test" version="1.7">
+<!-- Test schema for IntRangeField and LongRangeField -->
+<schema name="numericrange-test" version="1.7">
<!-- Basic field types -->
<fieldType name="string" class="solr.StrField" sortMissingLast="true" />
@@ -30,6 +30,12 @@
<fieldType name="intrange3d" class="solr.numericrange.IntRangeField"
numDimensions="3"/>
<fieldType name="intrange4d" class="solr.numericrange.IntRangeField"
numDimensions="4"/>
+ <!-- LongRangeField types with different dimensions -->
+ <fieldType name="longrange" class="solr.numericrange.LongRangeField"
numDimensions="1"/>
+ <fieldType name="longrange2d" class="solr.numericrange.LongRangeField"
numDimensions="2"/>
+ <fieldType name="longrange3d" class="solr.numericrange.LongRangeField"
numDimensions="3"/>
+ <fieldType name="longrange4d" class="solr.numericrange.LongRangeField"
numDimensions="4"/>
+
<!-- Field definitions -->
<field name="id" type="string" indexed="true" stored="true" required="true"
multiValued="false"/>
<field name="_version_" type="long" indexed="false" stored="false"
docValues="true"/>
@@ -48,6 +54,19 @@
<!-- 4D IntRangeField (tesseract) -->
<field name="tesseract" type="intrange4d" indexed="true" stored="true"/>
+ <!-- 1D LongRangeField -->
+ <field name="long_range" type="longrange" indexed="true" stored="true"/>
+ <field name="long_range_multi" type="longrange" indexed="true" stored="true"
multiValued="true"/>
+
+ <!-- 2D LongRangeField (bounding box) -->
+ <field name="long_range_2d" type="longrange2d" indexed="true" stored="true"/>
+
+ <!-- 3D LongRangeField (bounding cube) -->
+ <field name="long_range_3d" type="longrange3d" indexed="true" stored="true"/>
+
+ <!-- 4D LongRangeField (tesseract) -->
+ <field name="long_range_4d" type="longrange4d" indexed="true" stored="true"/>
+
<!-- Required by Solr -->
<uniqueKey>id</uniqueKey>
</schema>
diff --git
a/solr/core/src/test/org/apache/solr/schema/numericrange/LongRangeFieldTest.java
b/solr/core/src/test/org/apache/solr/schema/numericrange/LongRangeFieldTest.java
new file mode 100644
index 00000000000..0597939c12c
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/schema/numericrange/LongRangeFieldTest.java
@@ -0,0 +1,357 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.schema.numericrange;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.lucene.document.LongRange;
+import org.apache.lucene.index.IndexableField;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.junit.BeforeClass;
+
+/** Tests for {@link LongRangeField} */
+public class LongRangeFieldTest extends SolrTestCase {
+
+ @BeforeClass
+ public static void ensureAssumptions() {
+ assumeWorkingMockito();
+ }
+
+ public void test1DRangeParsing() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // Valid 1D range
+ LongRangeField.RangeValue range = fieldType.parseRangeValue("[10 TO 20]");
+ assertEquals(1, range.getDimensions());
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.maxs[0]);
+
+ // With extra whitespace
+ range = fieldType.parseRangeValue("[ 10 TO 20 ]");
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.maxs[0]);
+
+ // Negative numbers
+ range = fieldType.parseRangeValue("[-100 TO -50]");
+ assertEquals(-100L, range.mins[0]);
+ assertEquals(-50L, range.maxs[0]);
+
+ // Point range (min == max)
+ range = fieldType.parseRangeValue("[5 TO 5]");
+ assertEquals(5L, range.mins[0]);
+ assertEquals(5L, range.maxs[0]);
+
+ // Values outside int range
+ range = fieldType.parseRangeValue("[3000000000 TO 4000000000]");
+ assertEquals(3_000_000_000L, range.mins[0]);
+ assertEquals(4_000_000_000L, range.maxs[0]);
+ }
+
+ public void test2DRangeParsing() {
+ LongRangeField fieldType = createFieldType(2);
+
+ // Valid 2D range (bounding box)
+ LongRangeField.RangeValue range = fieldType.parseRangeValue("[10,20 TO
30,40]");
+ assertEquals(2, range.getDimensions());
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.mins[1]);
+ assertEquals(30L, range.maxs[0]);
+ assertEquals(40L, range.maxs[1]);
+
+ // With extra whitespace
+ range = fieldType.parseRangeValue("[ 10 , 20 TO 30 , 40 ]");
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.mins[1]);
+ assertEquals(30L, range.maxs[0]);
+ assertEquals(40L, range.maxs[1]);
+ }
+
+ public void test3DRangeParsing() {
+ LongRangeField fieldType = createFieldType(3);
+
+ // Valid 3D range (bounding cube)
+ LongRangeField.RangeValue range = fieldType.parseRangeValue("[10,20,30 TO
40,50,60]");
+ assertEquals(3, range.getDimensions());
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.mins[1]);
+ assertEquals(30L, range.mins[2]);
+ assertEquals(40L, range.maxs[0]);
+ assertEquals(50L, range.maxs[1]);
+ assertEquals(60L, range.maxs[2]);
+ }
+
+ public void test4DRangeParsing() {
+ LongRangeField fieldType = createFieldType(4);
+
+ // Valid 4D range (tesseract)
+ LongRangeField.RangeValue range = fieldType.parseRangeValue("[10,20,30,40
TO 50,60,70,80]");
+ assertEquals(4, range.getDimensions());
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.mins[1]);
+ assertEquals(30L, range.mins[2]);
+ assertEquals(40L, range.mins[3]);
+ assertEquals(50L, range.maxs[0]);
+ assertEquals(60L, range.maxs[1]);
+ assertEquals(70L, range.maxs[2]);
+ assertEquals(80L, range.maxs[3]);
+ }
+
+ public void testInvalidRangeFormat() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // Missing brackets
+ SolrException e1 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("10
TO 20"));
+ assertThat(e1.getMessage(), containsString("Invalid range format"));
+ assertThat(e1.getMessage(), containsString("Expected: [min1,min2,... TO
max1,max2,...]"));
+
+ // Missing TO keyword
+ SolrException e2 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[10
20]"));
+ assertThat(e2.getMessage(), containsString("Invalid range format"));
+
+ // Empty value
+ SolrException e3 = expectThrows(SolrException.class, () ->
fieldType.parseRangeValue(""));
+ assertThat(e3.getMessage(), containsString("Range value cannot be null or
empty"));
+
+ // Null value
+ SolrException e4 = expectThrows(SolrException.class, () ->
fieldType.parseRangeValue(null));
+ assertThat(e4.getMessage(), containsString("Range value cannot be null or
empty"));
+ }
+
+ public void testInvalidNumbers() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // Non-numeric values
+ SolrException e1 =
+ expectThrows(SolrException.class, () ->
fieldType.parseRangeValue("[abc TO def]"));
+ assertThat(e1.getMessage(), containsString("Invalid range"));
+ assertThat(e1.getMessage(), containsString("where min and max values are
longs"));
+
+ // Partially numeric
+ SolrException e2 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[10
TO xyz]"));
+ assertThat(e2.getMessage(), containsString("Invalid range"));
+ assertThat(e2.getMessage(), containsString("where min and max values are
longs"));
+
+ // Floating point (should fail for LongRange)
+ SolrException e3 =
+ expectThrows(SolrException.class, () ->
fieldType.parseRangeValue("[10.5 TO 20.5]"));
+ assertThat(e3.getMessage(), containsString("Invalid range"));
+ assertThat(e3.getMessage(), containsString("where min and max values are
longs"));
+ }
+
+ public void testDimensionMismatch() {
+ LongRangeField fieldType1D = createFieldType(1);
+ LongRangeField fieldType2D = createFieldType(2);
+
+ // 2D value on 1D field
+ SolrException e1 =
+ expectThrows(SolrException.class, () ->
fieldType1D.parseRangeValue("[10,20 TO 30,40]"));
+ assertThat(e1.getMessage(), containsString("Range dimensions"));
+ assertThat(e1.getMessage(), containsString("do not match field type
numDimensions"));
+
+ // 1D value on 2D field
+ SolrException e2 =
+ expectThrows(SolrException.class, () ->
fieldType2D.parseRangeValue("[10 TO 20]"));
+ assertThat(e2.getMessage(), containsString("Range dimensions"));
+ assertThat(e2.getMessage(), containsString("do not match field type
numDimensions"));
+
+ // Min/max dimension mismatch
+ SolrException e3 =
+ expectThrows(
+ SolrException.class,
+ () -> fieldType2D.parseRangeValue("[10,20 TO 30]")); // 2D mins,
1D maxs
+ assertThat(e3.getMessage(), containsString("Min and max dimensions must
match"));
+ }
+
+ public void testMinGreaterThanMax() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // Min > max should fail
+ SolrException e1 =
+ expectThrows(SolrException.class, () -> fieldType.parseRangeValue("[20
TO 10]"));
+ assertThat(e1.getMessage(), containsString("Min value must be <= max
value"));
+ assertThat(e1.getMessage(), containsString("dimension 0"));
+
+ // For 2D
+ LongRangeField fieldType2D = createFieldType(2);
+ SolrException e2 =
+ expectThrows(
+ SolrException.class,
+ () -> fieldType2D.parseRangeValue("[30,20 TO 10,40]")); // First
dimension invalid
+ assertThat(e2.getMessage(), containsString("Min value must be <= max
value"));
+ assertThat(e2.getMessage(), containsString("dimension 0"));
+ }
+
+ public void testFieldCreation1D() {
+ LongRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "long_range");
+
+ IndexableField field = fieldType.createField(schemaField, "[100 TO 200]");
+ assertNotNull(field);
+ assertTrue(field instanceof LongRange);
+ assertEquals("long_range", field.name());
+ }
+
+ public void testFieldCreation2D() {
+ LongRangeField fieldType = createFieldType(2);
+ SchemaField schemaField = createSchemaField(fieldType, "long_range_2d");
+
+ IndexableField field = fieldType.createField(schemaField, "[0,0 TO
10,10]");
+ assertNotNull(field);
+ assertTrue(field instanceof LongRange);
+ assertEquals("long_range_2d", field.name());
+ }
+
+ public void testStoredField() {
+ LongRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "long_range");
+
+ String value = "[100 TO 200]";
+ IndexableField storedField = fieldType.getStoredField(schemaField, value);
+ assertNotNull(storedField);
+ assertEquals("long_range", storedField.name());
+ assertEquals(value, storedField.stringValue());
+ }
+
+ public void testToInternal() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // Valid value should pass through after validation
+ String value = "[10 TO 20]";
+ String internal = fieldType.toInternal(value);
+ assertEquals(value, internal);
+
+ // Invalid value should throw exception
+ SolrException e = expectThrows(SolrException.class, () ->
fieldType.toInternal("invalid"));
+ assertThat(e.getMessage(), containsString("Invalid range format"));
+ }
+
+ public void testToNativeType() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // String input
+ Object nativeType = fieldType.toNativeType("[10 TO 20]");
+ assertTrue(nativeType instanceof LongRangeField.RangeValue);
+ LongRangeField.RangeValue range = (LongRangeField.RangeValue) nativeType;
+ assertEquals(10L, range.mins[0]);
+ assertEquals(20L, range.maxs[0]);
+
+ // RangeValue input (should pass through)
+ LongRangeField.RangeValue inputRange =
+ new LongRangeField.RangeValue(new long[] {5L}, new long[] {15L});
+ Object result = fieldType.toNativeType(inputRange);
+ assertSame(inputRange, result);
+
+ // Null input
+ assertNull(fieldType.toNativeType(null));
+ }
+
+ public void testSortFieldThrowsException() {
+ LongRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "long_range");
+
+ // Sorting should not be supported
+ SolrException e =
+ expectThrows(SolrException.class, () ->
fieldType.getSortField(schemaField, true));
+ assertThat(e.getMessage(), containsString("Cannot sort on
LongRangeField"));
+ assertThat(e.getMessage(), containsString("long_range"));
+ }
+
+ public void testUninversionType() {
+ LongRangeField fieldType = createFieldType(1);
+ SchemaField schemaField = createSchemaField(fieldType, "long_range");
+
+ // Should return null (no field cache support)
+ assertNull(fieldType.getUninversionType(schemaField));
+ }
+
+ public void testInvalidNumDimensions() {
+ LongRangeField field = new LongRangeField();
+ Map<String, String> args = new HashMap<>();
+ IndexSchema schema = createMockSchema();
+
+ // Test numDimensions = 0
+ args.put("numDimensions", "0");
+ SolrException e1 = expectThrows(SolrException.class, () ->
field.init(schema, args));
+ assertThat(e1.getMessage(), containsString("numDimensions must be between
1 and 4"));
+ assertThat(e1.getMessage(), containsString("but was [0]"));
+
+ // Test numDimensions = 5 (too high)
+ args.put("numDimensions", "5");
+ LongRangeField field2 = new LongRangeField();
+ SolrException e2 = expectThrows(SolrException.class, () ->
field2.init(schema, args));
+ assertThat(e2.getMessage(), containsString("numDimensions must be between
1 and 4"));
+ assertThat(e2.getMessage(), containsString("but was [5]"));
+
+ // Test negative numDimensions
+ args.put("numDimensions", "-1");
+ LongRangeField field3 = new LongRangeField();
+ SolrException e3 = expectThrows(SolrException.class, () ->
field3.init(schema, args));
+ assertThat(e3.getMessage(), containsString("numDimensions must be between
1 and 4"));
+ assertThat(e3.getMessage(), containsString("but was [-1]"));
+ }
+
+ public void testRangeValueToString() {
+ LongRangeField fieldType = createFieldType(2);
+ LongRangeField.RangeValue range = fieldType.parseRangeValue("[10,20 TO
30,40]");
+
+ String str = range.toString();
+ assertEquals("[10,20 TO 30,40]", str);
+ }
+
+ public void testExtremeValues() {
+ LongRangeField fieldType = createFieldType(1);
+
+ // Test with Long.MIN_VALUE and Long.MAX_VALUE
+ LongRangeField.RangeValue range =
+ fieldType.parseRangeValue("[" + Long.MIN_VALUE + " TO " +
Long.MAX_VALUE + "]");
+ assertEquals(Long.MIN_VALUE, range.mins[0]);
+ assertEquals(Long.MAX_VALUE, range.maxs[0]);
+ }
+
+ private IndexSchema createMockSchema() {
+ final var schema = mock(IndexSchema.class);
+ when(schema.getVersion()).thenReturn(1.7f);
+ return schema;
+ }
+
+ private LongRangeField createFieldType(int numDimensions) {
+ LongRangeField field = new LongRangeField();
+ Map<String, String> args = new HashMap<>();
+ args.put("numDimensions", String.valueOf(numDimensions));
+
+ field.init(createMockSchema(), args);
+
+ return field;
+ }
+
+ private SchemaField createSchemaField(LongRangeField fieldType, String name)
{
+ final var fieldProperties =
+ 0b1 | 0b100; // INDEXED | STORED - constants cannot be accessed
directly due to visibility.
+ return new SchemaField(name, fieldType, fieldProperties, null);
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
index 4264d85e360..817458569bf 100644
--- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
+++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java
@@ -28,7 +28,7 @@ import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.search.numericrange.IntRangeQParserPlugin;
+import org.apache.solr.search.numericrange.NumericRangeQParserPlugin;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -58,7 +58,9 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
if (!doAssertParserCoverage) return;
final var qParsersToTest = new
HashSet<>(QParserPlugin.standardPlugins.keySet());
- qParsersToTest.remove(IntRangeQParserPlugin.NAME); // Tested in
IntRangeQParserPluginTest
+ qParsersToTest.remove(
+ NumericRangeQParserPlugin.NAME); // Tested in
NumericRangeQParserPluginIntTest and
+ // NumericRangeQParserPluginLongTest
for (String name : qParsersToTest) {
assertTrue(
"testParserCoverage was run w/o any other method explicitly testing
qparser: " + name,
diff --git
a/solr/core/src/test/org/apache/solr/search/numericrange/IntRangeQParserPluginTest.java
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
similarity index 98%
rename from
solr/core/src/test/org/apache/solr/search/numericrange/IntRangeQParserPluginTest.java
rename to
solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
index 40fb2e6396d..b76db8acf82 100644
---
a/solr/core/src/test/org/apache/solr/search/numericrange/IntRangeQParserPluginTest.java
+++
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginIntTest.java
@@ -21,12 +21,15 @@ import org.apache.solr.common.SolrException;
import org.junit.BeforeClass;
import org.junit.Test;
-/** Tests for {@link IntRangeQParserPlugin} */
-public class IntRangeQParserPluginTest extends SolrTestCaseJ4 {
+/**
+ * Tests for {@link NumericRangeQParserPlugin} using {@link
+ * org.apache.solr.schema.numericrange.IntRangeField} fields.
+ */
+public class NumericRangeQParserPluginIntTest extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
- initCore("solrconfig.xml", "schema-intrange.xml");
+ initCore("solrconfig.xml", "schema-numericrange.xml");
}
@Override
@@ -281,7 +284,7 @@ public class IntRangeQParserPluginTest extends
SolrTestCaseJ4 {
// Query on non-IntRangeField should fail
assertQEx(
"Query on wrong field type should fail",
- "must be of type IntRangeField",
+ "must be a numeric range field type",
req("q", "{!numericRange criteria=intersects field=title}[100 TO
200]"),
SolrException.ErrorCode.BAD_REQUEST);
}
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
new file mode 100644
index 00000000000..1b774e88e90
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/search/numericrange/NumericRangeQParserPluginLongTest.java
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search.numericrange;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests for {@link NumericRangeQParserPlugin} using {@link
+ * org.apache.solr.schema.numericrange.LongRangeField} fields.
+ */
+public class NumericRangeQParserPluginLongTest extends SolrTestCaseJ4 {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig.xml", "schema-numericrange.xml");
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ clearIndex();
+ assertU(commit());
+ }
+
+ @Test
+ public void test1DIntersectsQuery() {
+ assertU(adoc("id", "1", "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]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range}[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]']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range}[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]"),
+ "//result[@numFound='3']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']",
+ "//result/doc/str[@name='id'][.='4']");
+ }
+
+ @Test
+ public void test1DWithinQuery() {
+ assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
+ assertU(adoc("id", "2", "long_range", "[150 TO 250]"));
+ assertU(adoc("id", "3", "long_range", "[50 TO 80]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"within\" field=long_range}[0 TO
300]"),
+ "//result[@numFound='3']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"within\" field=long_range}[100 TO
200]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"within\" field=long_range}[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]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"contains\" field=long_range}[160
TO 170]"),
+ "//result[@numFound='3']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']",
+ "//result/doc/str[@name='id'][.='3']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"contains\" field=long_range}[0 TO
400]"),
+ "//result[@numFound='0']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"contains\" field=long_range}[100
TO 200]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='3']");
+ }
+
+ @Test
+ public void test1DCrossesQuery() {
+ assertU(adoc("id", "1", "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]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=\"crosses\" field=long_range}[150 TO
250]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='4']");
+ }
+
+ @Test
+ public void test2DIntersectsQuery() {
+ assertU(adoc("id", "1", "long_range_2d", "[0,0 TO 10,10]"));
+ assertU(adoc("id", "2", "long_range_2d", "[5,5 TO 15,15]"));
+ assertU(adoc("id", "3", "long_range_2d", "[20,20 TO 30,30]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range_2d}[8,8
TO 12,12]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_2d}[25,25 TO 35,35]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='3']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_2d}[100,100 TO 200,200]"),
+ "//result[@numFound='0']");
+ }
+
+ @Test
+ public void test3DQuery() {
+ assertU(adoc("id", "1", "long_range_3d", "[0,0,0 TO 10,10,10]"));
+ assertU(adoc("id", "2", "long_range_3d", "[5,5,5 TO 15,15,15]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_3d}[8,8,8 TO 12,12,12]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void test4DQuery() {
+ assertU(adoc("id", "1", "long_range_4d", "[0,0,0,0 TO 10,10,10,10]"));
+ assertU(adoc("id", "2", "long_range_4d", "[5,5,5,5 TO 15,15,15,15]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_4d}[8,8,8,8 TO 12,12,12,12]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testMultiValuedField() {
+ assertU(
+ adoc("id", "1", "long_range_multi", "[100 TO 200]",
"long_range_multi", "[300 TO 400]"));
+ assertU(adoc("id", "2", "long_range_multi", "[150 TO 250]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_multi}[110 TO 120]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/arr[@name='long_range_multi']/str[1][.='[100 TO 200]']",
+ "//result/doc/arr[@name='long_range_multi']/str[2][.='[300 TO 400]']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_multi}[310 TO 320]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range_multi}[150 TO 250]"),
+ "//result[@numFound='2']");
+ }
+
+ @Test
+ public void testMissingFieldParameter() {
+ assertQEx(
+ "Missing field parameter should fail",
+ "Missing required parameter: field",
+ req("q", "{!numericRange criteria=intersects}[100 TO 200]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testMissingCriteriaParameter() {
+ assertQEx(
+ "Missing criteria parameter should fail",
+ "Missing required parameter: criteria",
+ req("q", "{!numericRange field=long_range}[100 TO 200]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testInvalidFieldType() {
+ // Query on a plain string field should fail
+ assertQEx(
+ "Query on wrong field type should fail",
+ "must be a numeric range field type",
+ req("q", "{!numericRange criteria=intersects field=title}[100 TO
200]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testInvalidQueryType() {
+ assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
+ assertU(commit());
+
+ assertQEx(
+ "Invalid query criteria should fail",
+ "Unknown query criteria",
+ req("q", "{!numericRange criteria=\"invalid\" field=long_range}[100 TO
200]"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testInvalidRangeValue() {
+ assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
+ assertU(commit());
+
+ assertQEx(
+ "Invalid range format should fail",
+ "Invalid range",
+ req("q", "{!numericRange criteria=intersects
field=long_range}invalid"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testEmptyRangeValue() {
+ assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
+ assertU(commit());
+
+ assertQEx(
+ "Empty range value should fail",
+ req("q", "{!numericRange criteria=intersects field=long_range}"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+
+ @Test
+ public void testNegativeValues() {
+ assertU(adoc("id", "1", "long_range", "[-100 TO -50]"));
+ assertU(adoc("id", "2", "long_range", "[-75 TO -25]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range}[-80 TO
-60]"),
+ "//result[@numFound='2']");
+ }
+
+ @Test
+ public void testValuesOutsideIntRange() {
+ // 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(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects
field=long_range}[3500000000 TO 3600000000]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+ }
+
+ @Test
+ public void testExtremeValues() {
+ long min = Long.MIN_VALUE;
+ long max = Long.MAX_VALUE;
+
+ assertU(adoc("id", "1", "long_range", "[" + min + " TO " + max + "]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range}[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]"));
+ assertU(commit());
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range}[100 TO
100]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+
+ assertQ(
+ req("q", "{!numericRange criteria=intersects field=long_range}[50 TO
150]"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='id'][.='1']");
+ }
+
+ // ------------------------------------------
+ // Tests for getFieldQuery and getSpecializedRangeQuery via the standard
query parser.
+ // These default to "contains" semantics.
+
+ @Test
+ public void testGetFieldQueryFullRange() {
+ // doc 1: narrow range, fully inside the query range → should NOT match
(doc contains query)
+ // doc 2: wide range that fully contains the query range → should match
+ // doc 3: range that only partially overlaps → should NOT match
+ assertU(adoc("id", "1", "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(commit());
+
+ // Contains semantics: find indexed ranges that fully contain [120 TO 180]
+ assertQ(
+ req("q", "long_range:[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
+ assertU(commit());
+
+ assertQ(
+ req("q", "long_range:[100 TO 200]"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQuerySingleBound() {
+ // Single-bound syntax: 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
+ assertU(commit());
+
+ assertQ(
+ req("q", "long_range:150"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQuerySingleBound2D() {
+ // 2D single-bound: bbox:5,5 is sugar for contains([5,5 TO 5,5])
+ assertU(adoc("id", "1", "bbox", "[0,0 TO 10,10]")); // Match!
+ assertU(adoc("id", "2", "bbox", "[5,5 TO 5,5]")); // Match!
+ assertU(adoc("id", "3", "bbox", "[0,0 TO 4,10]")); // No match - X
dimension ends too low
+ assertU(adoc("id", "4", "bbox", "[6,0 TO 10,10]")); // No match - X
dimension starts too high
+ assertU(commit());
+
+ assertQ(
+ req("q", "bbox:5,5"),
+ "//result[@numFound='2']",
+ "//result/doc/str[@name='id'][.='1']",
+ "//result/doc/str[@name='id'][.='2']");
+ }
+
+ @Test
+ public void testGetFieldQueryFieldFormatting() {
+ // Test 1D field formatting
+ assertU(adoc("id", "1", "long_range", "[100 TO 200]"));
+ // Test 2D field formatting
+ assertU(adoc("id", "2", "bbox", "[10,20 TO 30,40]"));
+ // Test 3D field formatting
+ assertU(adoc("id", "3", "cube", "[5,10,15 TO 25,30,35]"));
+ // Test 4D field formatting
+ assertU(adoc("id", "4", "tesseract", "[1,2,3,4 TO 11,12,13,14]"));
+ // Test multi-valued field formatting
+ assertU(
+ adoc(
+ "id",
+ "5",
+ "long_range_multi",
+ "[50 TO 100]",
+ "long_range_multi",
+ "[200 TO 300]",
+ "long_range_multi",
+ "[400 TO 500]"));
+ assertU(commit());
+
+ // Verify 1D field returns correctly formatted value
+ assertQ(
+ req("q", "id:1"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='long_range'][.='[100 TO 200]']");
+
+ // Verify 2D field returns correctly formatted value
+ assertQ(
+ req("q", "id:2"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='bbox'][.='[10,20 TO 30,40]']");
+
+ // Verify 3D field returns correctly formatted value
+ assertQ(
+ req("q", "id:3"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='cube'][.='[5,10,15 TO 25,30,35]']");
+
+ // Verify 4D field returns correctly formatted value
+ assertQ(
+ req("q", "id:4"),
+ "//result[@numFound='1']",
+ "//result/doc/str[@name='tesseract'][.='[1,2,3,4 TO 11,12,13,14]']");
+
+ // Verify multi-valued field returns all values correctly formatted
+ assertQ(
+ req("q", "id:5"),
+ "//result[@numFound='1']",
+ "//result/doc/arr[@name='long_range_multi']/str[1][.='[50 TO 100]']",
+ "//result/doc/arr[@name='long_range_multi']/str[2][.='[200 TO 300]']",
+ "//result/doc/arr[@name='long_range_multi']/str[3][.='[400 TO 500]']");
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/indexing-guide/pages/field-types-included-with-solr.adoc
b/solr/solr-ref-guide/modules/indexing-guide/pages/field-types-included-with-solr.adoc
index d530a4993f4..35d634b48ad 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
@@ -57,6 +57,8 @@ The
{solr-javadocs}/core/org/apache/solr/schema/package-summary.html[`org.apache
|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 is defined in the
`org.apache.solr.schema.numericrange` package; fieldType definitions typically
reference this as: `<fieldType name="long [...]
+
|NestPathField | Specialized field type storing enhanced information, when
xref:indexing-nested-documents.adoc#schema-configuration[working with nested
documents].
|PointType |A single-valued n-dimensional point. It's both for sorting spatial
data that is _not_ lat-lon, and for some more rare use-cases. (NOTE: this is
_not_ related to the "Point" based numeric fields). See
xref:query-guide:spatial-search.adoc[] for more information.
diff --git a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
index 2bd6dac3e7a..dabb283a0b5 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/other-parsers.adoc
@@ -1006,7 +1006,7 @@ For more information about the possibilities of nested
queries, see Yonik Seeley
NOTE: Syntax specifics of the `{!numericRange}` query parser are considered
experimental and may change in the future.
-Allows users to search range fields (e.g. `IntRangeField`) using a specified
query-range.
+Allows users to search range fields (e.g. `IntRangeField`, `LongRangeField`)
using a specified query-range.
Multiple match semantics supported, see the `criteria` parameter below for
more details.
=== Numeric Range Parameters
@@ -1019,7 +1019,7 @@ Multiple match semantics supported, see the `criteria`
parameter below for more
|===
+
The field name to operate on.
-Must be a "range" field (e.g. `IntRangeField`)
+Must be a numeric range field type (e.g. `IntRangeField`, `LongRangeField`)
`criteria`::
+
[%autowidth,frame=none]
@@ -1068,6 +1068,13 @@ Find products whose `price_range` overlaps at all with a
user's desired price ra
{!numericRange field="price_range" criteria="intersects" v="[100 TO 120]"}
----
+Find events whose `event_window` (a `LongRangeField` storing Unix epoch
milliseconds) overlaps a time range.
+
+[source,text]
+----
+{!numericRange field="event_window" criteria="intersects"}[1700000000000 TO
1800000000000]
+----
+
== Vector Query Parsers