Skip to content

SOLR-18201: Range Fields support docValues based optimization#4645

Open
ercsonusharma wants to merge 2 commits into
apache:mainfrom
ercsonusharma:feat_pr_18201
Open

SOLR-18201: Range Fields support docValues based optimization#4645
ercsonusharma wants to merge 2 commits into
apache:mainfrom
ercsonusharma:feat_pr_18201

Conversation

@ercsonusharma

@ercsonusharma ercsonusharma commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18201

Description

The numeric range field types i.e. IntRangeField, LongRangeField, FloatRangeField, and DoubleRangeField, did not support docValues. This PR adds opt-in, single/multiValued docValues to all four.

The motivation is a query-time optimization. Today a range clause (e.g. my_range:[100 TO 200]) always walks the field's Points (BKD) tree and materializes a bitset of all matching docs, regardless of how selective the rest of the query is. When such a clause is AND-ed with a much more selective clause (myStrField:someSelectiveValue AND my_range:[100 TO 200]), this is wasteful, we materialize a large bitset only to intersect it with a handful of candidates.

Solution

With docValues present, the range clause is wrapped in Lucene's IndexOrDocValuesQuery, which lets the selective clause lead iteration and verifies the range predicate per-candidate from docValues, skipping the BKD materialization entirely.
Also, introducec MultiBinaryRangeDocValuesQuery, backed by BinaryDocValues as SortedSet backing builds a huge term dictionary and every per-doc verify pays a lookupOrd making it more costly than the sequential BKD materialization it was meant to replace. It comes with minor modification in lucene document creation by packing multiValued field's ranges into a single blob.
These field types are new/unreleased, so this is added without back-compat concerns.

Tests

  • Added new integration tests - NumericRangeDocValuesTest, NumericRangeMultiValuedDocValuesTest and MultiBinaryRangeDocValuesQueryTest.
  • Existing numericrange tests are unaffected
  • Added a benchmark RangeSearch whose run results are as below (showing ~2x-3x improvement in selective queries only):
Benchmark               (criteria)     (field)    (scenario)   Mode  Cnt     Score      Error  Units
RangeSearch.rangeQuery    contains        r_ir  selective_dv  thrpt    5  1637.126 ±   33.185  ops/s
RangeSearch.rangeQuery    contains        r_ir  broad_points  thrpt    5   318.608 ±    5.378  ops/s
RangeSearch.rangeQuery    contains        r_ir   none_points  thrpt    5   433.680 ±    5.315  ops/s
RangeSearch.rangeQuery    contains     r_ir_dv  selective_dv  thrpt    5  4953.934 ±  283.397  ops/s
RangeSearch.rangeQuery    contains     r_ir_dv  broad_points  thrpt    5   314.803 ±   16.153  ops/s
RangeSearch.rangeQuery    contains     r_ir_dv   none_points  thrpt    5   425.166 ±   16.021  ops/s
RangeSearch.rangeQuery    contains     r_mv_ir  selective_dv  thrpt    5   934.336 ±   11.399  ops/s
RangeSearch.rangeQuery    contains     r_mv_ir  broad_points  thrpt    5   315.280 ±   19.342  ops/s
RangeSearch.rangeQuery    contains     r_mv_ir   none_points  thrpt    5   267.174 ±    8.491  ops/s
RangeSearch.rangeQuery    contains  r_mv_ir_dv  selective_dv  thrpt    5  2714.818 ±   40.461  ops/s
RangeSearch.rangeQuery    contains  r_mv_ir_dv  broad_points  thrpt    5   322.387 ±    2.730  ops/s
RangeSearch.rangeQuery    contains  r_mv_ir_dv   none_points  thrpt    5   268.392 ±    4.039  ops/s
RangeSearch.rangeQuery  intersects        r_ir  selective_dv  thrpt    5  1899.224 ±   60.904  ops/s
RangeSearch.rangeQuery  intersects        r_ir  broad_points  thrpt    5   324.266 ±    9.377  ops/s
RangeSearch.rangeQuery  intersects        r_ir   none_points  thrpt    5   438.226 ±   59.882  ops/s
RangeSearch.rangeQuery  intersects     r_ir_dv  selective_dv  thrpt    5  4073.935 ±  903.913  ops/s
RangeSearch.rangeQuery  intersects     r_ir_dv  broad_points  thrpt    5   313.721 ±   17.786  ops/s
RangeSearch.rangeQuery  intersects     r_ir_dv   none_points  thrpt    5   441.639 ±   20.801  ops/s
RangeSearch.rangeQuery  intersects     r_mv_ir  selective_dv  thrpt    5  1038.356 ±   25.230  ops/s
RangeSearch.rangeQuery  intersects     r_mv_ir  broad_points  thrpt    5   326.816 ±   21.920  ops/s
RangeSearch.rangeQuery  intersects     r_mv_ir   none_points  thrpt    5   267.894 ±   15.156  ops/s
RangeSearch.rangeQuery  intersects  r_mv_ir_dv  selective_dv  thrpt    5  2676.503 ±  203.337  ops/s
RangeSearch.rangeQuery  intersects  r_mv_ir_dv  broad_points  thrpt    5   329.054 ±    5.202  ops/s
RangeSearch.rangeQuery  intersects  r_mv_ir_dv   none_points  thrpt    5   269.826 ±   17.403  ops/s
RangeSearch.rangeQuery      within        r_ir  selective_dv  thrpt    5  1937.916 ±  195.623  ops/s
RangeSearch.rangeQuery      within        r_ir  broad_points  thrpt    5   306.693 ±   12.386  ops/s
RangeSearch.rangeQuery      within        r_ir   none_points  thrpt    5   449.107 ±   24.540  ops/s
RangeSearch.rangeQuery      within     r_ir_dv  selective_dv  thrpt    5  4862.166 ±  678.356  ops/s
RangeSearch.rangeQuery      within     r_ir_dv  broad_points  thrpt    5   311.665 ±   30.428  ops/s
RangeSearch.rangeQuery      within     r_ir_dv   none_points  thrpt    5   449.009 ±   10.885  ops/s
RangeSearch.rangeQuery      within     r_mv_ir  selective_dv  thrpt    5  1089.617 ±   45.707  ops/s
RangeSearch.rangeQuery      within     r_mv_ir  broad_points  thrpt    5   332.549 ±   20.213  ops/s
RangeSearch.rangeQuery      within     r_mv_ir   none_points  thrpt    5   279.219 ±    5.414  ops/s
RangeSearch.rangeQuery      within  r_mv_ir_dv  selective_dv  thrpt    5  2518.521 ±  344.224  ops/s
RangeSearch.rangeQuery      within  r_mv_ir_dv  broad_points  thrpt    5   336.337 ±   15.615  ops/s
RangeSearch.rangeQuery      within  r_mv_ir_dv   none_points  thrpt    5   270.378 ±   10.551  ops/s
RangeSearch.rangeQuery     crosses        r_ir  selective_dv  thrpt    5  1683.941 ±  398.317  ops/s
RangeSearch.rangeQuery     crosses        r_ir  broad_points  thrpt    5   289.636 ±   18.353  ops/s
RangeSearch.rangeQuery     crosses        r_ir   none_points  thrpt    5   440.876 ±   11.024  ops/s
RangeSearch.rangeQuery     crosses     r_ir_dv  selective_dv  thrpt    5  4366.407 ± 1623.692  ops/s
RangeSearch.rangeQuery     crosses     r_ir_dv  broad_points  thrpt    5   302.358 ±   38.972  ops/s
RangeSearch.rangeQuery     crosses     r_ir_dv   none_points  thrpt    5   452.579 ±    9.217  ops/s
RangeSearch.rangeQuery     crosses     r_mv_ir  selective_dv  thrpt    5  1031.718 ±   36.046  ops/s
RangeSearch.rangeQuery     crosses     r_mv_ir  broad_points  thrpt    5   319.433 ±   15.376  ops/s
RangeSearch.rangeQuery     crosses     r_mv_ir   none_points  thrpt    5   276.894 ±   16.116  ops/s
RangeSearch.rangeQuery     crosses  r_mv_ir_dv  selective_dv  thrpt    5  2452.196 ±  319.052  ops/s
RangeSearch.rangeQuery     crosses  r_mv_ir_dv  broad_points  thrpt    5   330.320 ±   18.011  ops/s
RangeSearch.rangeQuery     crosses  r_mv_ir_dv   none_points  thrpt    5   276.039 ±   17.509  ops/s

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests cat:search cat:index cat:schema labels Jul 18, 2026

@gerlowskija gerlowskija left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ercsonusharma - this looks great overall! There are two big conceptual questions worth discussing before getting this in:

  1. Binary vs. SortedSet DocValues - your PR opts to use Lucene's "Binary" docValue type. This isn't without precedent in Solr, but SortedSet is definitely more common. Did you consider (but discard) the idea of using SortedSet? If so, why did you end up deciding on Binary? Did you do any benchmarking of the different approaches?
  2. Sorting and Faceting - I notice this PR omits support for docValues. Is that due to a particular technical limitation or hurdle, or more a question of scope/value?

Documenting answers to those questions will be a big help to other reviewers or folks trying to understand this code down the road 👍

Other than that, this code looks pretty far along and hopefully we can get it merged in pretty short order. I've left a number of other smaller comments "in line". Many of these are in the test code. It feels like there's some consolidation possible there, but I may be missing certain nuances between the test files...

Anyway, let me know what you think!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[-0] I think most folks' association with "range search" is using a range query to search non-range data (i.e. price_i:[0 TO 100].

Wdyt of renaming to be more specific: eg. RangeFieldSearch, DocValuesRangeFieldSearch...something along those lines?

* calls {@link #createFieldsFromAllValues(SchemaField, Collection)} once per (multiValued) field
* instead of {@link #createFields(SchemaField, Object)} per value.
*/
public boolean createsFieldsFromAllValues() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[-0] IMO the code that uses these new methods would be easier to grok if it started with "should" or "is" or some other question-word that suggests that the method returns a boolean. Maybe, shouldBatchProcessFieldValues() or something similar? Wdyt?

@Override
protected boolean enableDocValuesByDefault() {
return false; // Range fields do not support docValues
// DocValues are supported for both single and multiValued range fields, but are opt-in: they

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] IMO these are worth enabling by default, for two reasons:

  1. Solr has been moving towards "docValues by default" for awhile (example).
  2. Many users would probably have a hard time determining at schema-design time whether a given field will be used with a filter or not. It's probably better to give these folks the optimization by default. They can always reverse-course and reindex if they decide that they don't want to pay the disk-space penalty.


// Force useDocValuesAsStored off; it otherwise defaults on for schema, which
// would make fl=* responses fail on a docValues-enabled range field.
properties &= ~USE_DOCVALUES_AS_STORED;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] Why are these fields incompatible with useDocValuesAsStored?

If it's a hard restriction we should probably throw an exception or log a warning or something, rather than just quietly overriding what the user explicitly requested.

}

return newContainsQuery(field.getName(), singleBoundRange);
// A single bound is the degenerate range [p,p]; "contains p" is equivalent to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] Can you clarify this a bit? I've poked around the newIntersectsQuery and newContainsQuery implementations for (e.g.) IntRangeField, and afaict they both have similar optimizations in terms of how they use IndexOrDocValuesQuery. Is newIntersectsQuery really more optimized than newContainsQuery in some way?

import org.junit.Test;

/**
* Integration tests over the {@code schema-numericrange.xml} verifying that the docValues-enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Integration tests over the {@code schema-numericrange.xml} verifying that the docValues-enabled
* Integration tests over {@code schema-numericrange.xml} verifying that the docValues-enabled

@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] If value faceting isn't supported, then it might be nice to have a test case similar to what you've written for "sort" above, but for the facet.field case. Wdyt?

import org.junit.Test;

/**
* Integration tests for multiValued range fields with docValues (backed by SortedSet docValues).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] I'm not totally clear on the difference between this test-class and NumericRangeDocValuesTest. Are the test cases different between the two? Or are they near duplicates with the multivalued flag in the schema types being the primary difference?

[-1] We're not actually using SortedSet docValues as this comment claims, are we?

assertU(commit());

// Neither individual range of doc 1 contains [1,15]
assertSameCount("contains", "[1 TO 15]", "price_range_multi", "price_range_mv_dv", 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] 'assertSameCount' might blithely pass even if a totally different set of documents matched than what you intended. If we're going to keep this test class around, it might be worth asserting on the specific doc IDs that are returned.

|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. 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] Most readers will assume that sorting+faceting are possible whenever they see that a field supports docValues. It might be worth calling that out here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cat:index cat:schema cat:search documentation Improvements or additions to documentation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants