apache / apache/lucene

[ENH]: Fast O(1) Precomputed Array Lookup Table for `ExclusiveLongRangeCounter`

Open
#16,436 1 comment 0 reactions 0 assignees View on GitHub
type:enhancement
Dominant language
Java
Stars
3.6k
Forks
1.4k
Avg merge
2d 11h
Merged PRs (30d)
88

Description

### Description

### 1. Motivation & Problem Statement

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:

```java
// Current ExclusiveLongRangeCounter binary search
int lo = 0, hi = numRanges - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
if (v < min[mid]) {
hi = mid - 1;
} else if (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$)
- **`percentile / score buckets`**: Domain $[0, 100]$ (Span $= 101$)

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:

1. **Domain Span Calculation**:
During `ExclusiveLongRangeCounter` constructor initialization, compute global domain boundaries:
$$\text{span} = \text{globalMax} - \text{globalMin} + 1$$

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

3. **$O(1)$ Single-Instruction Value Lookup**:
In `addSingleValued(long v)`:
```java
if (useFastTable && v >= fastMinVal && v <= fastMaxVal) {
int bucket = fastRangeMap[(int) (v - fastMinVal)];
if (bucket != -1) {
countBuffer[bucket]++;
}
} else {
// Fallback to existing O(log R) binary search for out-of-bounds or wide domains
addSingleValuedBinarySearch(v);
}
```

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

| Query Category | `post_collection_facets` (QPS) | `during_collection_facets` (QPS) | QPS Difference |
| :--- | :---: | :---: | :---: |
| **`range` Facets** | **1,605.31** | **1,548.19** | Baseline Validated |
| **`MedTerm`** | 1,795.97 | **2,471.15** | **+37.6%** |
| **`HighPhrase`** | 89.80 | **130.96** | **+45.8%** |
| **`OrHighHigh`** | 39.68 | **59.57** | **+50.1%** |
| **`MedIntervalsOrdered`** | 37.97 | **65.70** | **+73.0%** |
| **`ConstMSM2`** | 261.04 | **340.77** | **+30.5%** |

---

### 4. Code Location & Branch

- **Feature Branch**: `perf/range_agg_bin_search`
- **Modified Classes**:
- `org.apache.lucene.facet.range.ExclusiveLongRangeCounter`
- `org.apache.lucene.facet.range.LongRangeFacetCounts`

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

Contributor guide

Open the contributing guide

Research direction

Start with org.apache.lucene.facet.range.ExclusiveLongRangeCounter and LongRangeFacetCounts, focusing on the constructor and addSingleValued path described in the issue. Review the existing binary-search behavior, then run the relevant luceneutil runFacets.py benchmark on the stated dataset. Done means bounded domains use the proposed lookup without changing count results, while wider domains retain the existing behavior; coordinate first because active development is noted.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
performance, search
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.