Round-3 perf push sub-issue (tracked under umbrella #534).
[XL] No query result / filter cache layer (LRU + Roaring)
- Where: cross-cutting; entry
InvertedIndexSearcher::search (searcher.rs:632). No QueryCache / FilterCache type exists.
- Current behavior: every query re-decodes every posting list. Even repeated
SearchRequest::filter_query (engine/search.rs:190) is re-evaluated from scratch. No doc-set memoisation, no Query → bitset rewrite, no small-vs-large clause hit-set strategy.
- Why it might be a bottleneck / risk: filter clauses dominate application workloads (tenancy, category, status flag). Multi-tenant servers cannot amortise. The single biggest scaling gap vs Lucene/Tantivy.
- Reference precedent: Lucene
LRUQueryCache materialises a BitDocIdSet (FixedBitSet or Roaring) per (segment, query); Tantivy ships QueryCache in tantivy-common. Both key on Query::hashCode/Eq.
- Data structure (current → proposed): nothing → segment-keyed
LruCache<Arc<dyn QueryKey>, Arc<RoaringBitmap>>. Roaring is the right choice: dense ranges become RunContainers, sparse facet filters stay ArrayContainers, and word-level AND/OR is 64-bit-aligned.
- Suggested direction: add
QueryCacheKey (hash + eq) on Query; gate with Query::cacheable() -> bool (skip score-dependent); add Arc<QueryCache> on InvertedIndexReader. Miss → materialise RoaringBitmap from the existing matcher; hit → single Roaring::contains/intersection.
- Risk / scope: needs
Eq + Hash on every concrete Query impl (15+ types under laurus/src/lexical/query/); large but mechanical.
ID: LS-01 — see ~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.md for the full Round-3 issue list.
Round-3 perf push sub-issue (tracked under umbrella #534).
[XL] No query result / filter cache layer (LRU + Roaring)
InvertedIndexSearcher::search(searcher.rs:632). NoQueryCache/FilterCachetype exists.SearchRequest::filter_query(engine/search.rs:190) is re-evaluated from scratch. No doc-set memoisation, noQuery → bitsetrewrite, no small-vs-large clause hit-set strategy.LRUQueryCachematerialises aBitDocIdSet(FixedBitSet or Roaring) per(segment, query); Tantivy shipsQueryCacheintantivy-common. Both key onQuery::hashCode/Eq.LruCache<Arc<dyn QueryKey>, Arc<RoaringBitmap>>. Roaring is the right choice: dense ranges becomeRunContainers, sparse facet filters stayArrayContainers, and word-level AND/OR is 64-bit-aligned.QueryCacheKey(hash + eq) onQuery; gate withQuery::cacheable() -> bool(skip score-dependent); addArc<QueryCache>onInvertedIndexReader. Miss → materialiseRoaringBitmapfrom the existing matcher; hit → singleRoaring::contains/intersection.Eq + Hashon every concreteQueryimpl (15+ types underlaurus/src/lexical/query/); large but mechanical.ID:
LS-01— see~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.mdfor the full Round-3 issue list.