MiniLucene Immutable Storage and Commit Design and Implementation History¶
Historical objective: Persist Phase 1 segments with deterministic codecs, publish commits atomically through one manifest, and reopen committed indexes with search results equivalent to the in-memory oracle.
Architecture: A frozen SegmentImage crosses the RAM/disk boundary. SegmentCodec writes immutable, checksummed segment directories. ManifestStore is the only recoverable root. IndexWriter owns the RAM buffer and generation counters; Index.open_reader() consumes only the committed manifest.
Tech Stack: Python 3.12 standard library (pathlib, json, hashlib, os, struct), pytest, Ruff, uv.
Milestone 1: Freeze SegmentImage as the storage boundary¶
Recorded file scope:
- Added: src/minilucene/storage/__init__.py
- Added: src/minilucene/storage/image.py
- Added: tests/unit/storage/test_segment_image.py
Recorded activity 1 — Test intent: the failing invariant tests
import pytest
from minilucene.storage.image import SegmentImage
def test_segment_image_rejects_non_dense_documents():
with pytest.raises(ValueError, match="dense"):
SegmentImage(
generation=1,
schema_fingerprint="abc",
stored_documents={1: {"id": "late"}},
postings={},
field_lengths={},
)
def test_segment_image_is_immutable(ram_segment):
image = SegmentImage.from_ram_segment(7, "schema", ram_segment)
with pytest.raises(TypeError):
image.stored_documents[0] = {"id": "changed"}
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/unit/storage/test_segment_image.py.
Historical expected evidence: import failure because storage.image does not exist.
Recorded activity 3 — Design outcome: the frozen image
The design used immutable tuples and MappingProxyType. Validate generation is positive,
document IDs are exactly range(max_doc), posting doc IDs are increasing and
in range, positions are increasing, and every field-length vector has
max_doc entries.
Public constructor:
@dataclass(frozen=True)
class SegmentImage:
generation: int
schema_fingerprint: str
stored_documents: Mapping[int, Mapping[str, str]]
postings: Mapping[str, Mapping[str, tuple[Posting, ...]]]
field_lengths: Mapping[str, tuple[int, ...]]
@property
def max_doc(self) -> int: ...
@classmethod
def from_ram_segment(
cls, generation: int, schema_fingerprint: str, segment: MemorySegment
) -> "SegmentImage": ...
Recorded activity 4 — Verification intent: GREEN and regression
Historical verification covered targeted or full test coverage, including tests/unit/storage/test_segment_image.py, tests/unit/index.
Milestone 2: Implement unsigned varints and delta sequences¶
Recorded file scope:
- Added: src/minilucene/storage/varint.py
- Added: tests/unit/storage/test_varint.py
Recorded activity 1 — Test intent: table and malformed-input tests
import pytest
from minilucene.storage.varint import (
decode_delta_sequence,
decode_uvarint,
encode_delta_sequence,
encode_uvarint,
)
@pytest.mark.parametrize("value", [0, 1, 127, 128, 16_384, 2**63 - 1])
def test_uvarint_round_trip(value):
encoded = encode_uvarint(value)
assert decode_uvarint(encoded, 0) == (value, len(encoded))
def test_delta_sequence_requires_strict_increase():
with pytest.raises(ValueError, match="increasing"):
encode_delta_sequence((2, 2))
def test_unterminated_varint_is_rejected():
with pytest.raises(ValueError, match="unterminated"):
decode_uvarint(b"\x80", 0)
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/unit/storage/test_varint.py.
Recorded activity 3 — Design outcome: bounded decoding
decode_uvarint accepts at most ten bytes and rejects overflow, negative
offsets, out-of-bounds offsets, and unterminated input. Delta decoding accepts
an explicit element count and rejects zero deltas after the first element.
Recorded activity 4 — Verification intent: GREEN
Historical verification covered targeted or full test coverage, including tests/unit/storage/test_varint.py.
Milestone 3: Encode terms, postings, stored fields, and norms¶
Recorded file scope:
- Added: src/minilucene/storage/codec.py
- Added: tests/unit/storage/test_segment_codec.py
- Added: docs/segment-format.md
Recorded activity 1 — Test intent: byte-stability and round-trip tests
from minilucene.storage.codec import SegmentDataCodec
def test_segment_data_codec_is_deterministic(segment_image):
first = SegmentDataCodec.encode(segment_image)
second = SegmentDataCodec.encode(segment_image)
assert first == second
assert set(first) == {
"terms.bin",
"postings.bin",
"stored.bin",
"norms.bin",
}
def test_segment_data_codec_round_trips(segment_image):
files = SegmentDataCodec.encode(segment_image)
decoded = SegmentDataCodec.decode(
generation=segment_image.generation,
schema_fingerprint=segment_image.schema_fingerprint,
files=files,
)
assert decoded == segment_image
The recorded scope added corruption cases for invalid UTF-8, offsets outside postings.bin,
non-monotonic doc IDs, non-monotonic positions, malformed JSON frames, trailing
bytes, and field-length count mismatches.
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/unit/storage/test_segment_codec.py.
Recorded activity 3 — Design outcome: the documented codec
Encoding contracts:
terms.bin:
term_count
repeated(field_utf8, term_utf8, postings_offset, postings_length)
postings.bin:
posting_count
repeated(doc_delta, tf, position_count, position_deltas)
stored.bin:
doc_count
repeated(json_byte_length, canonical_json_bytes)
norms.bin:
field_count
repeated(field_utf8, length_count, document_lengths)
Every integer and byte-string length uses unsigned varints. Terms sort by
(field UTF-8 bytes, term UTF-8 bytes). JSON uses
sort_keys=True, ensure_ascii=False, separators=(",", ":").
Recorded activity 4 — Document the exact format
docs/segment-format.md records magic/version ownership, every frame, sorting
rules, validation rules, and that compatibility with Apache Lucene is absent.
Recorded activity 5 — Verification intent: GREEN and deterministic check
Historical verification covered targeted or full test coverage, static analysis, including tests/unit/storage/test_segment_codec.py, src/minilucene/storage, tests/unit/storage.
Milestone 4: Publish checksummed immutable segment directories¶
Recorded file scope:
- Added: src/minilucene/storage/filesystem.py
- Added: src/minilucene/storage/segment_store.py
- Added: tests/storage/test_segment_store.py
Recorded activity 1 — Test intent: publication and cleanup tests
from pathlib import Path
import pytest
from minilucene.storage.segment_store import SegmentStore
def test_segment_store_writes_metadata_last(tmp_path, segment_image, recording_fs):
store = SegmentStore(tmp_path, fs=recording_fs)
descriptor = store.publish(segment_image)
assert recording_fs.file_syncs[-1].name == "segment.json"
assert descriptor.path == Path("segments/seg_000001")
assert not list((tmp_path / "segments").glob(".tmp-*"))
def test_failed_publish_never_creates_final_directory(
tmp_path, segment_image, failing_fs
):
store = SegmentStore(tmp_path, fs=failing_fs.fail_on_write("postings.bin"))
with pytest.raises(OSError):
store.publish(segment_image)
assert not (tmp_path / "segments" / "seg_000001").exists()
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/storage/test_segment_store.py.
Recorded activity 3 — Design outcome: injected filesystem operations
FileSystemOps wraps mkdir, write, fsync-file, fsync-directory, replace, read,
remove-tree, and list-directory. SegmentStore.publish uses a UUID temp
directory, writes four data files, fsyncs them, writes segment.json last with
magic/version/generation/schema fingerprint/length/SHA-256 entries, fsyncs,
renames atomically, and fsyncs segments/.
Recorded activity 4 — Design outcome: strict open
SegmentStore.open(generation, expected_schema_fingerprint) validates metadata
before decoding. Unknown version, wrong schema, missing file, wrong length,
wrong checksum, or invalid codec data raises CorruptIndexError without
returning a partial image.
Recorded activity 5 — Verification intent: GREEN
Historical verification covered targeted or full test coverage, including tests/storage/test_segment_store.py.
Milestone 5: Make the manifest the only committed root¶
Recorded file scope:
- Added: src/minilucene/storage/manifest.py
- Added: tests/storage/test_manifest_store.py
Recorded activity 1 — Test intent: atomic-root tests
import pytest
from minilucene.storage.manifest import Manifest, ManifestStore
def test_open_ignores_complete_orphan_segment(tmp_path, published_segment):
store = ManifestStore(tmp_path)
store.create(schema_fingerprint="schema")
manifest = store.read()
assert manifest.segment_generations == ()
def test_replace_failure_preserves_old_manifest(tmp_path, failing_fs):
store = ManifestStore(tmp_path)
store.create(schema_fingerprint="schema")
old = store.read()
broken = Manifest.next_from(old, segment_generations=(1,))
with pytest.raises(OSError):
store.write_atomic(broken, fs=failing_fs.fail_on_replace())
assert store.read() == old
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/storage/test_manifest_store.py.
Recorded activity 3 — Design outcome: immutable manifest values
Manifest fields:
@dataclass(frozen=True)
class SegmentCommit:
segment_generation: int
live_docs_generation: int | None
live_docs_checksum: str | None
@dataclass(frozen=True)
class Manifest:
format_version: int
schema_fingerprint: str
commit_generation: int
segments: tuple[SegmentCommit, ...]
next_segment_generation: int
next_commit_generation: int
The recorded scope added writes persisted schema plus generation-zero manifest. Update writes
canonical JSON to manifest.tmp, fsyncs, replaces manifest.json, and fsyncs
the index directory. Startup removes neither orphans nor temp files; it ignores
anything not referenced by the manifest.
Recorded activity 4 — Verification intent: GREEN
Historical verification covered targeted or full test coverage, including tests/storage/test_manifest_store.py.
Milestone 6: Add index creation, schema persistence, and writer exclusion¶
Recorded file scope:
- Added: src/minilucene/index.py
- Added: src/minilucene/writer.py
- Added: src/minilucene/errors.py
- Changed: src/minilucene/__init__.py
- Added: tests/contract/test_index_lifecycle.py
Recorded activity 1 — Test intent: public lifecycle tests
import pytest
from minilucene import Index, KeywordField, Schema, TextField
from minilucene.errors import SchemaMismatchError, WriterAlreadyOpenError
def test_create_open_and_schema_fingerprint(tmp_path):
schema = Schema(id=KeywordField(stored=True), body=TextField(stored=True))
index = Index.create(tmp_path, schema)
assert Index.open(tmp_path).schema == schema
def test_open_rejects_supplied_different_schema(tmp_path, schema):
Index.create(tmp_path, schema)
with pytest.raises(SchemaMismatchError):
Index.open(tmp_path, Schema(body=TextField(stored=True)))
def test_only_one_writer_can_be_open(tmp_path, schema):
index = Index.create(tmp_path, schema)
first = index.writer()
with pytest.raises(WriterAlreadyOpenError):
index.writer()
first.close()
index.writer().close()
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/contract/test_index_lifecycle.py.
Recorded activity 3 — Design outcome: lifecycle ownership
Index.create refuses non-empty initialized paths, persists schema, and writes
manifest zero. Index.open validates schema fingerprint. Index.writer
creates an exclusive .writer.lock with O_CREAT | O_EXCL; close() releases
it idempotently. Stale-lock recovery is explicit and outside V1.
Recorded activity 4 — Verification intent: GREEN
Historical verification covered targeted or full test coverage, including tests/contract/test_index_lifecycle.py.
Milestone 7: Implement deterministic flush without publication¶
Recorded file scope:
- Changed: src/minilucene/writer.py
- Added: tests/storage/test_writer_flush.py
Recorded activity 1 — Test intent: visibility and threshold tests
def test_flush_creates_segment_but_does_not_change_manifest(index):
with index.writer() as writer:
writer.add_document(id="1", body="alpha")
segment = writer.flush()
assert segment.generation == 1
assert index.manifest().segments == ()
def test_document_threshold_flushes_before_next_add(index_with_threshold_one):
with index_with_threshold_one.writer() as writer:
writer.add_document(id="1", body="alpha")
writer.add_document(id="2", body="beta")
assert writer.segment_generations == (1,)
assert writer.buffered_document_count == 1
Recorded activity 2 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/storage/test_writer_flush.py.
Recorded activity 3 — Design outcome: flush
Before each add, validate and analyze the complete document into a temporary
prepared document. Mutate the RAM buffer only after preparation succeeds.
Flush freezes SegmentImage, publishes it, appends its descriptor only to the
writer's current segment set, increments generation, and replaces the RAM
buffer only after publication succeeds. Empty flush returns None.
Automatic flush uses FlushPolicy(max_documents, max_postings) and checks the
logical counts before admitting the next document.
Recorded activity 4 — Verification intent: GREEN
Historical verification covered targeted or full test coverage, including tests/storage/test_writer_flush.py, tests/contract/test_memory_search.py.
Milestone 8: Commit and reopen equivalent disk search¶
Recorded file scope:
- Changed: src/minilucene/writer.py
- Changed: src/minilucene/index.py
- Added: src/minilucene/reader.py
- Added: tests/storage/test_commit_recovery.py
- Added: tests/contract/test_disk_search.py
Recorded activity 1 — Test intent: old-or-new crash tests
Inject failures at: segment data write, segment metadata write, segment rename, manifest temp write, and manifest replace. For each failure reopen the index and assert it yields the last successfully published manifest only.
def test_complete_segment_without_manifest_is_ignored_after_reopen(index):
with index.writer() as writer:
writer.add_document(id="1", body="orphan")
writer.flush()
reopened = type(index).open(index.path)
assert reopened.open_reader().max_doc == 0
Recorded activity 2 — Test intent: equivalence contract
Build the same corpus in MemoryIndex and disk Index, commit, reopen, search
term/phrase/prefix/boolean queries, and compare total hits, stored fields,
scores with pytest.approx, and final order.
Recorded activity 3 — Verification intent: RED
Historical verification covered targeted or full test coverage, including tests/storage/test_commit_recovery.py, tests/contract/test_disk_search.py.
Recorded activity 4 — Design outcome: commit
Commit flushes, verifies all referenced segments, creates the next immutable manifest, publishes it atomically, then updates writer committed state. A failed manifest publication leaves the prior committed manifest authoritative and keeps the writer open for explicit retry or close.
Recorded activity 5 — Design outcome: committed reader
Index.open_reader() loads exactly the manifest's segments, constructs one
multi-segment reader view, computes global corpus statistics, and searches
through the Phase 1 scorer/collector path. It never scans unreferenced
directories.
Recorded activity 6 — Verification intent: GREEN and phase verification
Historical verification covered targeted or full test coverage, static analysis, bytecode compilation, diff hygiene, including tests/storage/test_commit_recovery.py, tests/contract/test_disk_search.py.
Milestone 9: Accept immutable storage and commit¶
Recorded file scope:
- Added: tests/acceptance/test_phase2_storage_commit.py
- Added: docs/phase2-storage-commit.md
Recorded activity 1 — Design outcome: one restart acceptance
The recorded scope added two segments, commit, reopen through a fresh Index, and prove stored
fields, phrase hits, BM25 scores, schema fingerprint, generation ordering, and
absence of orphan visibility match the in-memory oracle.
Recorded activity 2 — Verification intent: phase acceptance
Historical verification covered targeted or full test coverage, static analysis, bytecode compilation, diff hygiene, including tests/acceptance/test_phase2_storage_commit.py.
Recorded activity 3 — Test intent: the phase report
docs/phase2-storage-commit.md records the segment format, manifest authority,
flush/commit distinction, crash matrix, verification commands, and the absence
of refresh, deletion, merge, query parsing, network, and vector behavior.
Recorded activity 4 — Recorded acceptance snapshot
Historical expected evidence: clean worktree.