ADR-0038: SIMD-Accelerated BM25 Lexical Scoring Optimization¶
| Field | Value |
|---|---|
| Status | Accepted (Implemented) |
| Date | 2026-08-10 |
| Authors | Spector Maintainers & Architecture Working Group |
| Deciders | Spector Technical Steering Committee (TSC) |
| Supersedes | None |
| Superseded By | None |
| Last Verified | 2026-09-16 (Verified against main) |
1. Context¶
Spector utilizes hybrid lexical-vector retrieval to combine semantic dense embeddings with precise lexical keyword matching. In empirical baseline profiling (Issue #600), BM25 keyword scoring accounted for ~70% of query latency (~3.0 ms out of 4.8 ms) in cognitive memory recall. Meanwhile, the Panama AVX-512 direct off-heap vector scan completed in ~0.4 ms, making lexical search the primary system bottleneck.
2. Problem Statement¶
Detailed code analysis of BM25Index and StandardAnalyzer surfaced five primary performance detractors:
- Garbage Generation & Array Zeroing: Allocating
new float[N]on every single search call (plus per-virtual-thread allocations during parallel scoring) triggered memory zero-fill cycles, TLB misses, and L1/L2 cache eviction. - Inner Loop Division & Recalculation: Calculating
b * docLen / avgDLfand floating-point division on every posting iteration cost 11–15 CPU cycles per posting. - Regex Pattern Matching: POSIX/Unicode regex compilation and matcher traversal in
StandardAnalyzerallocated multiple intermediateStringobjects and incurred regex engine overhead. - Full-Corpus \(O(N)\) Top-K Heap Insertion: Evaluating all \(N\) corpus entries rather than only touched non-zero document indices.
- Micro-Task Virtual Thread Latency: Spawning and joining virtual threads for sub-millisecond posting sets introduced ~0.5–1.0 ms of fixed scheduling latency.
3. Decision Drivers¶
- Sub-0.5ms Lexical Query Latency: BM25 query time must drop by ~10× to match vector scan speeds.
- Zero-Allocation Hot Path: Lexical scoring and tokenization must produce 0 GC heap bytes per query.
- Exact Algorithmic Equivalence: Precomputed math must yield identical ranking scores to canonical Robertson Okapi BM25.
- Adaptive Execution: Small to medium candidate sets (< 50,000 documents) must execute sequentially without thread-scheduling overhead.
4. Considered Options¶
Option 1: External Lucene Integration¶
- Description: Delegate lexical indexing and search to Apache Lucene.
- Advantages: Battle-tested open-source search engine.
- Disadvantages: Heavy dependency weight; complex off-heap memory coordination with Panama FFM; significant heap churn during score fusion.
Option 2: Dense-Only Retrieval (Deprecate BM25)¶
- Description: Rely purely on vector embeddings and ColBERT MaxSim reranking.
- Advantages: Eliminates lexical index maintenance.
- Disadvantages: Severe regression on exact identifiers, function names, and rare lexical tokens (e.g. error codes, hash prefixes).
Option 3: Zero-Allocation Fast-Path BM25 Architecture (Selected)¶
- Description: Precompute length normalization at index time, use thread-local sparse accumulators, implement a single-pass zero-regex tokenizer, and adaptively bypass virtual-thread scheduling on small-to-medium corpora.
- Advantages: Drops latency from 3.0ms to < 0.35ms; zero heap allocation; 100% score identicality.
- Disadvantages: Custom tokenizer maintenance for non-Latin character sets.
5. Decision Outcome¶
Chosen Option: Option 3 (Zero-Allocation Fast-Path BM25 Architecture).
classDiagram
class FastTokenizer {
+tokenize(CharSequence text, Consumer~String~ sink)
-isAlphanumeric(char c)
-isStopWord(CharSequence token)
}
class SparseScoreAccumulator {
-float[] scores
-int[] touchedIndices
-int touchedCount
+accumulate(int docIdx, float score)
+collectTopK(int k, NeighborQueue heap)
+reset()
}
class BM25Index {
-float[] docLenNorm
-PostingList[] invertedIndex
+search(String query, int k)
-accumulatePostingsFast(...)
}
BM25Index --> FastTokenizer : uses
BM25Index --> SparseScoreAccumulator : ThreadLocal Architectural Decisions:¶
Decision 1: Index-Time Precomputed Document Length Normalization¶
Instead of computing: $\(\text{tfNorm} = \frac{\text{tf} \cdot (k_1 + 1)}{\text{tf} + k_1 \cdot \left(1 - b + b \cdot \frac{\text{docLen}}{\text{avgDL}}\right)}\)$ during search, we precalculate: $\(\text{docLenNorm}[d] = k_1 \cdot \left(1 - b + b \cdot \frac{\text{docLen}[d]}{\text{avgDL}}\right)\)$ at ingestion time and maintain a primitive float[] docLenNorm array. The inner scoring denominator reduces to: $\(\text{tfNorm} = \frac{\text{tf} \cdot (k_1 + 1)}{\text{tf} + \text{docLenNorm}[d]}\)$ This eliminates document length array lookups, inner multiplications, and divisions.
Decision 2: Zero-Allocation ThreadLocal<SparseScoreAccumulator>¶
We implement a thread-local reusable accumulator structure:
- Pre-allocated
float[] scores(grown dynamically as corpus expands). int[] touchedIndicestracking actively scored document indices.int touchedCountfor \(O(\text{hits})\) top-K extraction and \(O(\text{hits})\) buffer resetting.- Result: Zero heap array allocations per query, 0 GC bytes generated.
Decision 3: Single-Pass Fast Tokenizer & Zero-Regex Parsing¶
Replace StandardAnalyzer's Pattern.compile("[\\p{L}\\p{N}]+"); with an optimized single-pass ASCII/UTF-8 char walker:
- In-place case normalization (
c |= 0x20for ASCII A-Z). - Compact static set / switch filter for common English stop words.
- Emits tokens with minimal heap allocations.
Decision 4: Adaptive Single-Thread Fast Path¶
Eliminate virtual thread forkJoinAll overhead when the corpus size is under 50,000 documents or posting count is small. Sequential traversal with precomputed normalization and sparse accumulation runs in < 0.2 ms on a single core.
Decision 5: Streamlined RRF Fusion in RecallCandidateGatherer¶
Streamline rank assignment during lexical-vector fusion in RecallCandidateGatherer to eliminate unnecessary intermediate map instantiations.
Positive Consequences¶
- BM25 search latency drops from ~3.0 ms to < 0.35 ms (~8.5× speedup).
- Completely eliminates GC allocations during lexical search.
- Exact parity with canonical BM25 scoring rankings.
Negative Consequences & Trade-offs¶
- In-memory
docLenNormarray consumes 4 bytes per indexed document (~4 MB per 1M docs).
6. Pros and Cons of the Options¶
| Option | Pros | Cons |
|---|---|---|
| Option 1: Lucene | Complete feature set | Heavyweight dependency, memory segment impedance mismatch |
| Option 2: Dense-Only | No keyword index | Poor exact-match keyword recall |
| Option 3: Fast-Path BM25 | 8.5× faster, 0 GC bytes, exact scores | Requires maintaining fast tokenizer |
7. Implementation Plan¶
- Phase 1: Author
SparseScoreAccumulatorandFastTokenizerinnucleus/spector-index. - Phase 2: Add index-time
docLenNormprecomputation toBM25Index. - Phase 3: Implement single-thread fast-path thresholding for small corpora (<50k docs).
- Phase 4: Verify ranking relevance parity in
BM25IndexTestand JMH benchmarks inspector-bench.
8. Code Reference & Verification¶
- Primary Module(s):
nucleus/spector-index,memory/spector-memory - Key Packages:
com.spectrayan.spector.index.bm25,com.spectrayan.spector.memory.index - Classes:
BM25Index.java,FastTokenizer.java,SparseScoreAccumulator.java,MemoryBM25Index.java - Verification Tests:
BM25IndexTest.java,StandardAnalyzerTest.java,MemoryBM25IndexTest.java