You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, Lucene's range faceting implementation (LongRangeFacetCounts / ExclusiveLongRangeCounter) evaluates document numeric values against $R$ requested range intervals by executing a binary search over a range segment tree.
For every document evaluated, this costs $O(\log R)$ comparisons per value:
// Current ExclusiveLongRangeCounter binary searchintlo = 0, hi = numRanges - 1;
while (lo <= hi) {
intmid = (lo + hi) >>> 1;
if (v < min[mid]) {
hi = mid - 1;
} elseif (v > max[mid]) {
lo = mid + 1;
} else {
countBuffer[mid]++;
break;
}
}
However, many real-world numeric range faceting use cases operate over bounded numeric domains where the domain span (globalMax - globalMin + 1) is relatively small. Common examples include:
dayOfYear: Domain $[1, 366]$ (Span $\approx 366$)
month: Domain $[1, 12]$ (Span $= 12$)
age: Domain $[0, 120]$ (Span $= 121$)
HTTP status code: Domain $[100, 599]$ (Span $= 500$)
In these scenarios, executing an $O(\log R)$ binary search per document introduces unnecessary CPU branch mispredictions and memory comparison overhead when a simple precomputed array lookup can resolve the value to its range index in $O(1)$ time (1 CPU instruction).
2. Proposed Solution: Precomputed Array Lookup Table (Option B)
We propose adding a fast $O(1)$ precomputed array lookup path to ExclusiveLongRangeCounter for bounded range domains:
Domain Span Calculation:
During ExclusiveLongRangeCounter constructor initialization, compute global domain boundaries: $$\text{span} = \text{globalMax} - \text{globalMin} + 1$$
Precomputed Array Initialization:
If $\text{span} \le 65,536$ (64 KB array size limit for optimal L1/L2 cache locality):
Allocate int[] fastRangeMap = new int[(int) span].
Populate fastRangeMap with range bucket indices, or -1 for unmapped values.
$O(1)$ Single-Instruction Value Lookup:
In addSingleValued(long v):
if (useFastTable && v >= fastMinVal && v <= fastMaxVal) {
intbucket = fastRangeMap[(int) (v - fastMinVal)];
if (bucket != -1) {
countBuffer[bucket]++;
}
} else {
// Fallback to existing O(log R) binary search for out-of-bounds or wide domainsaddSingleValuedBinarySearch(v);
}
Zero Overhead Fallback:
If the domain span exceeds $65,536$, useFastTable is set to false, incurring zero extra memory or runtime overhead and falling back to standard binary search.
3. Benchmark Results (luceneutil)
We benchmarked the implementation using luceneutil (runFacets.py) on the wikimedium10k dataset, evaluating 39 fine-grained range buckets over the dayOfYear field ($[0, 390]$):
Correctness: Count outputs were verified to be 100% identical across all test runs.
Pure Scalar: Implemented in 100% pure Java without any external framework or Vector API / SIMD dependencies.
Active Development Notice: I am actively working on the implementation and benchmarking for this optimization and will be opening a Pull Request shortly. Please ping me before starting redundant work on this issue.
Description
1. Motivation & Problem Statement
Currently, Lucene's range faceting implementation ($R$ requested range intervals by executing a binary search over a range segment tree.
LongRangeFacetCounts/ExclusiveLongRangeCounter) evaluates document numeric values againstFor every document evaluated, this costs$O(\log R)$ comparisons per value:
However, many real-world numeric range faceting use cases operate over bounded numeric domains where the domain span (
globalMax - globalMin + 1) is relatively small. Common examples include:dayOfYear: Domainmonth: Domainage: DomainHTTP status code: Domainpercentile / score buckets: DomainIn these scenarios, executing an$O(\log R)$ binary search per document introduces unnecessary CPU branch mispredictions and memory comparison overhead when a simple precomputed array lookup can resolve the value to its range index in $O(1)$ time (1 CPU instruction).
2. Proposed Solution: Precomputed Array Lookup Table (Option B)
We propose adding a fast$O(1)$ precomputed array lookup path to
ExclusiveLongRangeCounterfor bounded range domains:Domain Span Calculation:
$$\text{span} = \text{globalMax} - \text{globalMin} + 1$$
During
ExclusiveLongRangeCounterconstructor initialization, compute global domain boundaries:Precomputed Array Initialization:$\text{span} \le 65,536$ (64 KB array size limit for optimal L1/L2 cache locality):
If
int[] fastRangeMap = new int[(int) span].fastRangeMapwith range bucket indices, or-1for unmapped values.In
addSingleValued(long v):Zero Overhead Fallback:$65,536$ ,
If the domain span exceeds
useFastTableis set tofalse, incurring zero extra memory or runtime overhead and falling back to standard binary search.3. Benchmark Results (
luceneutil)We benchmarked the implementation using$[0, 390]$ ):
luceneutil(runFacets.py) on thewikimedium10kdataset, evaluating 39 fine-grained range buckets over thedayOfYearfield (post_collection_facets(QPS)during_collection_facets(QPS)rangeFacetsMedTermHighPhraseOrHighHighMedIntervalsOrderedConstMSM24. Code Location & Branch
perf/range_agg_bin_searchorg.apache.lucene.facet.range.ExclusiveLongRangeCounterorg.apache.lucene.facet.range.LongRangeFacetCountsFeedback and suggestions from maintainers (@gsmiller, @mikemccand, @rmuir) are welcome!
Important
Active Development Notice: I am actively working on the implementation and benchmarking for this optimization and will be opening a Pull Request shortly. Please ping me before starting redundant work on this issue.