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