# Materializer **Last Updated:** 2026-01-31 **Confidence:** High **Status:** Implemented ## Summary The Materializer is a background worker that pre-computes winning assertions for each subject+predicate pair, storing them at `MV:{subject}:{predicate}` for O(1) reads. It bridges the gap between O(N) lens resolution and sub-millisecond query latency. **Key Facts:** - Scans all `SP:` compound indexes to discover pairs - Resolves each pair through an `AsyncLens` (default: VoteAwareConsensus) - Stores `MaterializedView` with winner + provenance metadata - `step()` for one-shot, `run()` for polling loop, `run_notified()` for event-driven mode - Event-driven: IngestWorker signals `tokio::sync::Notify` on new data; Materializer reacts immediately - QueryEngine uses fast-path: checks `MV:` key before falling back to `SP:` index - Error-resilient: individual pair failures are logged and skipped **File Pointers:** - `crates/stemedb-query/src/materializer.rs` - Materializer worker - `crates/stemedb-core/src/types.rs` - MaterializedView type ## Storage Layout | Key Pattern | Value | Purpose | |-------------|-------|---------| | `MV:{subject}:{predicate}` | Serialized `MaterializedView` | Pre-computed winner + metadata | ## MaterializedView Type ```rust pub struct MaterializedView { pub winner: Assertion, // The resolved winner pub lens_name: String, // Which lens produced this (e.g., "VoteAwareConsensus") pub resolution_confidence: f32, // Confidence in the resolution [0.0, 1.0] pub candidates_count: usize, // How many candidates were considered pub materialized_at: u64, // When this view was last computed } ``` ## Materializer Interface ```rust pub struct Materializer { store: Arc, index_store: GenericIndexStore>, lens: Box, } impl Materializer { /// Create with any AsyncLens implementation pub fn new(store: Arc, lens: Box) -> Self; /// One full materialization pass over all SP: pairs pub async fn step(&self) -> Result; /// Materialize a single subject+predicate pair pub async fn materialize_pair(&self, subject: &str, predicate: &str) -> Result>; /// Read a pre-computed view (O(1)) pub async fn get_materialized_view(&self, subject: &str, predicate: &str) -> Result>; /// Run continuously with configurable interval (polling mode) pub async fn run(&self, interval: Duration); /// Run in event-driven mode, triggered by IngestWorker notifications pub async fn run_notified(&self, notify: Arc, max_interval: Duration); } ``` ## Read Path Integration The `QueryEngine` automatically uses the fast path when both subject and predicate are specified. **Fast Path (O(1)):** ``` QueryEngine::execute() -> MV:{subject}:{predicate} -> MaterializedView.winner -> QueryResult ``` **Slow Path (O(N)):** ``` QueryEngine::execute() -> SP:{subject}:{predicate} -> [H:{hash}...] -> candidates -> filter -> QueryResult ``` The fast path is used when a materialized view exists and the winner matches query filters (lifecycle, epoch). The slow path is the fallback when no MV exists, the winner doesn't match filters, or only a subject is specified. **File:** `crates/stemedb-query/src/engine.rs` — `try_fast_path()` method ## Event-Driven Mode The Materializer supports two operating modes: **Polling mode** (`run(interval)`): Fixed-interval passes. Simple but wastes cycles when idle and adds latency after writes. **Event-driven mode** (`run_notified(notify, max_interval)`): The IngestWorker signals a `tokio::sync::Notify` after each successful record ingestion. The Materializer awaits this signal, running a pass immediately when new data arrives. A `max_interval` timeout acts as a safety net for missed notifications. ``` IngestWorker::step() -> notify.notify_one() -> Materializer::run_notified() wakes -> step() ``` **Wiring:** ```rust let notify = Arc::new(tokio::sync::Notify::new()); let worker = IngestWorker::new(journal, store.clone()).await?.with_notify(Arc::clone(¬ify)); let materializer = Materializer::new(store, Box::new(lens)); // In separate tasks: tokio::spawn(async move { worker.run().await }); tokio::spawn(async move { materializer.run_notified(notify, Duration::from_secs(30)).await }); ``` **File:** `crates/stemedb-ingest/src/worker.rs` — `with_notify()` method ## Design Rationale ### Why a Background Worker? Inline materialization (on every write) would: 1. Add latency to the write path 2. Create contention when many agents write simultaneously 3. Couple write and read concerns The background worker approach: 1. Keeps the write path fast (append-only) 2. Batches resolution work efficiently 3. Tolerates temporary staleness (eventual consistency) ### Why Store Metadata? The `MaterializedView` includes `lens_name`, `confidence`, `candidates_count`, and `materialized_at` because: 1. **Provenance:** Agents can verify how truth was determined 2. **Debugging:** "Why does the system think Tesla's revenue is X?" 3. **Staleness detection:** Readers can check `materialized_at` to decide if a slow-path re-resolution is needed ## Related Topics - [Ballot Box](./ballot-box.md) - Vote data consumed by the Materializer - [Storage](./storage.md) - KV layout and key patterns - [Architecture](../../architecture.md) - Section 3 (Write Path) and Section 4 (Read Path)