rajat315315 opened a new issue, #16448:
URL: https://github.com/apache/lucene/issues/16448
### Description
## 1. Motivation
Currently, Lucene's collection phase is easily parallelized using the
concurrent search capabilities of `IndexSearcher` combined with
`FacetsCollectorManager`. Slices of segments are searched in parallel, and
their matching doc IDs are collected into thread-local `FacetsCollector`
instances, which are later merged into a single `FacetsCollector` (list of
`MatchingDocs`).
However, the **aggregation phase**—where term ordinals are read from
`SortedSetDocValues` and their counts are accumulated—remains strictly
single-threaded in the standard `SortedSetDocValuesFacetCounts` constructor:
```java
// Sequential segment aggregation in constructor
for (MatchingDocs hits : matchingDocs) {
countOneSegment(ordinalMap, hits.context().reader(), hits.context().ord,
hits, null);
}
```
If an index has a large number of segments and query match density is high,
executing this loop sequentially on a single thread becomes a significant
bottleneck, completely underutilizing multi-core CPU architectures.
While Lucene provides a separate `ConcurrentSortedSetDocValuesFacetCounts`
class, it:
1. Mandates a different class instantiating schema and requires a full
`ExecutorService`.
2. Forces the use of an `AtomicIntegerArray` spanning the entire global
ordinal space, which is memory-heavy and can suffer from false sharing / CPU
cache-line bouncing under high contention.
3. Cannot be configured directly on the standard
`SortedSetDocValuesFacetCounts` class.
---
## 2. Proposed Solution
We propose introducing a new constructor overload in
`SortedSetDocValuesFacetCounts` that accepts a standard
`java.util.concurrent.Executor`:
```java
public SortedSetDocValuesFacetCounts(
SortedSetDocValuesReaderState state, FacetsCollector hits, Executor
executor)
```
### Implementation Details:
* **TaskExecutor**: Reuses Lucene's internal `TaskExecutor` to execute
segment counting tasks (`countOneSegmentConcurrent`) concurrently.
* **Thread-Local / Task-Local Collection**: Each segment task accumulates
counts into its own isolated segment-local structure:
* **Sparse Mode** (if `hits.totalHits() < numSegOrds / 10`): Collects
global ordinals directly into a thread-local HPPC `IntIntHashMap`. This avoids
allocating massive count arrays on worker threads.
* **Dense Mode**: Collects into a segment-local `int[] segCounts` of size
`numSegOrds`.
* **Lock-Free Reduction**: Once all segment tasks complete, the main thread
merges the segment-local results into the shared `counts` array. This
completely avoids write locks, synchronization, and atomic write contention on
worker threads.
---
## 3. Benchmark Results
### 3.1. Synthetic Benchmark
Tested on a multi-segment index (200,000 documents partitioned across 8
segments, using 100 unique facet categories per document):
* **CPU**: 8-core Processor
* **Worker Threads**: 8 threads
* **Query**: `MatchAllDocsQuery`
| Implementation | Latency (ms/op) | Throughput (ops/sec) | QPS Change |
| :--- | :---: | :---: | :---: |
| **1. Sequential Baseline** | 39.572 ms/op | 25.27 ops/sec | Baseline |
| **2. Concurrent (AtomicIntegerArray class)** | 18.583 ms/op | 53.81
ops/sec | **🚀 +112.9%** |
| **3. Concurrent ThreadLocal (This Proposal)** | 20.761 ms/op | 48.17
ops/sec | **🚀 +90.6%** |
### 3.2. Wikimedia Dataset Benchmark (`wikimedium1m`)
We ran standard search queries on a **1,000,000 document** Wikimedia dataset
using `luceneutil` on all 4 CPU cores:
* **BrowseDateSSDVFacets** (High Cardinality): 21.27 QPS (Sequential) vs
36.12 QPS (Concurrent) [**+69.8% 🚀**]
* **BrowseRandomLabelSSDVFacets** (High Cardinality): 40.06 QPS (Sequential)
vs 63.66 QPS (Concurrent) [**+58.9% 🚀**]
* **BrowseDayOfYearSSDVFacets** (Medium Cardinality): 115.00 QPS
(Sequential) vs 131.73 QPS (Concurrent) [**+14.5% 🚀**]
* **BrowseMonthSSDVFacets** (Low Cardinality - 12): 228.80 QPS (Sequential)
vs 120.53 QPS (Concurrent) [**-47.3%**]
### Key Observations:
1. **Parallel Speedup**: On healthy segment sizes (like 12.5k
documents/segment for 1M doc index), concurrent aggregation delivers **+59% to
+70% QPS improvement** on high-cardinality fields.
2. **Cardinality vs. Thread Overhead**: For tiny collections (like
`wikimedium10k` with 1,250 docs/segment) or extremely low-cardinality facets
(like `Month` with 12 unique buckets), the JIT-compiled sequential aggregation
is incredibly fast, and scheduling worker threads introduces reduction overhead
that can slow down performance. Concurrency is best suited for realistic index
scales and high-cardinality fields.
3. **Memory Efficiency**: Unlike `ConcurrentSortedSetDocValuesFacetCounts`
which allocates a static `AtomicIntegerArray` of global cardinality size (which
can be millions of entries), our thread-local implementation scales dynamically
and keeps memory usage minimal under low hit counts via sparse maps.
---
## 4. Testing & Verification
All existing 305 tests in `:lucene:facet` compiled and passed cleanly. A new
benchmark test class `TestSortedSetDocValuesConcurrentBenchmark` was added to
verify correctness and assert that counts from all three aggregation paths are
100% identical.
---
## 5. Work in Progress
> [!IMPORTANT]
> **A Pull Request implementing this concurrent aggregation is currently in
progress.** We have successfully verified correct behavior and measured
significant performance improvements. Please do not duplicate efforts on this
feature. We will link the PR here shortly!
CC: @gsmiller, @mikemccand, @rmuir
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]