Architecture internals
LeanCorpus is organised around immutable segments. Indexing creates new segments, deletion records hide documents in existing segments, merges consolidate compatible segments, and commits publish a coherent manifest.
The public architecture overview explains the model from an application perspective. This page concentrates on ownership, concurrency, and the boundaries contributors must preserve.
Component boundaries
architecture-beta
group product(cloud)[LeanCorpus]
service api(server)[Public API] in product
service indexing(server)[Indexing] in product
service search(server)[Search] in product
service codecs(database)[Codecs and Store] in product
service compression(disk)[Compression plugins] in product
service cli(internet)[CLI]
service sourcegen(server)[Source generator]
cli:B --> T:api
sourcegen:B --> T:api
api:B --> T:indexing
api:B --> T:search
indexing:B --> T:codecs
search:B --> T:codecs
codecs:B --> T:compression
Rowles.LeanCorpus owns the core file formats and abstractions. Optional compression projects implement provider contracts without becoming dependencies of the core package. The source generator emits mappings against public contracts. The CLI consumes the same operational APIs available to applications.
Indexing path
Each indexing thread writes through a documents-writer-per-thread buffer. A buffer becomes eligible for flushing when document or RAM limits are reached. Flushed segments are immutable, then merge policy decisions may schedule background consolidation.
flowchart LR
A[Application threads] --> P[DWPT pool]
P --> D1[Thread buffer]
P --> D2[Thread buffer]
D1 -->|RAM or document limit| F1[Flush]
D2 -->|RAM or document limit| F2[Flush]
F1 --> S1[Immutable segment]
F2 --> S2[Immutable segment]
S1 --> M[Merge scheduler]
S2 --> M
M --> S3[Consolidated segment]
S3 --> C[Next commit]
The backpressure settings on IndexWriterConfig constrain queued documents, queued bytes, concurrent flushes, concurrent merges, and pending merge bytes. Changes in this area must preserve bounded memory, document ordering guarantees, and commit visibility.
The writer lock may acquire a DWPT monitor while committing. A path holding a DWPT monitor must not acquire the writer lock in return. Backpressure slot accounting is atomic so producer auto-flush can return semaphore capacity without creating that lock inversion.
Functional test assemblies configure a non-durable LeanCorpusDefaults override at
startup. Tests covering durable publication, recovery, or filesystem synchronisation
must set IndexWriterConfig.DurableCommits = true explicitly; the production default
remains durable.
LeanCorpusDefaults publishes immutable override snapshots. Configure serialises
the complete read-modify-write operation, validates the effective candidate through
the normal configuration rules, and publishes it atomically. Factory delegates are
stored but not invoked while the update lock is held. New top-level configuration
graphs capture one snapshot, while existing configurations and active components
retain their captured values. Factories create fresh analysis, policy, scoring, or
diagnostic instances for their receiving configuration. A SearcherManager owns
factory-created slow-query logging for the lifetime of its configuration graph and
keeps it across refreshes.
The reference-valued defaults follow the implementation lifecycles:
| Value | Current behaviour | Global default ownership |
|---|---|---|
IAnalyser |
Built-in analysers are stateful and a standard analyser is not thread-safe. | Factories create the configured analyser for each writer; DWPT setup continues to create thread-local built-in analyser instances. |
ICharFilter |
Filters are invoked in configured order and have no common disposal contract. | Factories create a fresh ordered filter collection for each writer. |
IIndexDeletionPolicy and IMergePolicy |
Policies are writer-owned strategy objects; tiered merge policy state is configured at construction. | Factories create fresh policy objects per writer. |
ISimilarity |
Built-in similarities are safe to reuse, while custom implementations may carry state. | Root scoring factories materialise separate writer and searcher values; explicit local values remain caller-owned. |
IMetricsCollector |
The interface has no disposal contract and collectors receive calls from the owning component. | Factories create a collector per writer or searcher configuration; directly supplied collectors are not disposed by LeanCorpus. |
SlowQueryLog |
It owns a background writer loop and is disposable. | A standalone searcher owns a factory-created log; a manager keeps one across refresh and disposes it with the manager. |
SearchAnalytics |
It is a thread-safe bounded event buffer without a disposal contract. | Factories create one per searcher graph, and a manager retains it across refresh. |
Commit publication
A commit must not expose a manifest that names incomplete files. The writer flushes pending work, makes file contents durable when configured to do so, and publishes the new segments_N file last.
sequenceDiagram
participant App
participant Writer as IndexWriter
participant Store as Directory
participant Reader as IndexSearcher
App->>Writer: Commit()
Writer->>Writer: Acquire write lock
Writer->>Store: Flush segment files
Writer->>Store: Flush deletion and stats files
opt DurableCommits
Writer->>Store: fsync files and directory
end
Writer->>Store: Atomically publish segments_N
Writer-->>App: Commit generation
Reader->>Store: Open latest complete segments_N
Recovery can discard an incomplete newest generation because the previous committed manifest and its referenced immutable files remain valid.
Searcher ownership
An IndexSearcher owns a coherent commit view. Segment readers may be opened lazily and retained in a bounded cache. Cross-directory file-lifetime leases prevent deletion of files still reachable by an active reader.
SearcherManager periodically checks for a newer commit. Refresh creates a replacement searcher before swapping the current instance. Existing leases continue to use the old searcher until released.
The important invariants are:
- a search sees one commit generation, never a mixture;
- a refreshed searcher is fully usable before publication;
- old segment files remain available while any reader can still open or use them;
- refresh failure retains the last healthy searcher;
- disposal releases leases and memory-mapped views in a defined order.
Merge ownership
Merge policy chooses candidates. The merge scheduler controls when work runs and how much may be pending. The writer publishes merged output only after it is complete. Source segments cannot be removed while snapshots, searchers, or another commit still reference them.
Do not assume that Compact() or ForceMerge(1) is a routine maintenance operation. Both can rewrite large portions of an index and require substantial temporary disk space. They are explicit operational choices, not a substitute for correct lifetime management.
Native AOT boundary
Core paths avoid runtime code generation and reflection-dependent discovery. New extensibility points should use explicit registration, source generation, or statically reachable implementations. Validate changes that affect serialisation, generated mappings, or plugin discovery with the repository AOT command.