Skip to content

Warm serverless: stateless writers, manifest refresh, lazy attach (roadmap Phases 0-3) - #10

Closed
EdgarBabajanyan wants to merge 27 commits into
mainfrom
feat/warm-serverless
Closed

Warm serverless: stateless writers, manifest refresh, lazy attach (roadmap Phases 0-3)#10
EdgarBabajanyan wants to merge 27 commits into
mainfrom
feat/warm-serverless

Conversation

@EdgarBabajanyan

Copy link
Copy Markdown
Contributor

Problem

Compass v0.3.0 made object storage durable, but serving stayed fully stateful: every write required the node that had the collection attached, boot rebuilt every collection before serving (O(total data)), two nodes on one bucket diverged forever, and attached state was unbounded. None of that scales toward serverless.

Fix — the "warm serverless" milestone (roadmap Phases 0–3, all opt-in, local mode untouched)

  • Stateless writer roleCOMPASS_ROLE=writer nodes boot instantly, validate against the bucket's collection config, mint ids from CAS-leased blocks ({ns}/id-alloc — writers and attached nodes can never collide), append one durable WAL fragment, and return its seq.
  • Bucket collection config{ns}/collection.json makes specs/created_at/CollectionConfig durable (cold rebuild previously fabricated them and silently lost embed_model); vector-space CRUD is bucket-first CAS.
  • Manifest refresh + read-your-writes — a per-collection seq tracker (contiguous frontier + out-of-band set) lets the background refresher converge nodes without ever double-applying a node's own writes; config changes, deletes, and recreates are detected on refresh. min_seq on search gives cross-node read-your-writes with a bounded wait.
  • Lazy attach + LRU detachCOMPASS_LAZY_ATTACH boots in O(namespaces) and attaches on first request (stampede-safe); COMPASS_MAX_ATTACHED detaches least-recently-used collections, which re-attach on demand.
  • CI — the object-storage build + real-S3 integration tests now run against MinIO on every PR (silent-skip guarded); DCO enforced.

Adversarial review (fixed pre-PR, each with a regression test)

An independent review round found 3 critical + 6 high issues, all fixed in the final commit: config changes never converged to attached nodes (and quarantined their chunks — silent data loss); seqs were marked applied before the apply (a failed apply became permanently invisible); concurrent rebuilds could run into the same live index directory; "LRU" was actually FIFO-by-attach; writer deletes could create phantom namespaces and poison the id high-water mark; delete/recreate left stale nodes serving dead data. Also fixed a latent v0.2 bug the block allocator exposed: sub-1000-vector collections never persisted the vector keymap and silently served wrong chunk ids once ids were non-dense.

Testing

  • 103 default / 149 object-storage tests — including two-node convergence (chunks/deletes/relations/config), ingest racing the refresher, persistent-disk restart catching up a writer's delta, both compaction branches, LRU semantics, wrong-dims quarantine, id-allocator concurrency, writer end-to-end; fmt clean, CI-exact clippy zero warnings
  • Live MinIO, two real HTTP nodes on one bucket: writer-role ingest → min_seq read-your-writes on the serving node → background-refresh convergence → writer delete replication → lazy node boots registering (not rebuilding) and attaches on demand in 0.11s

Notes / follow-ups

  • Warm, not cold: attach cost is still proportional to collection size — bounded cold start arrives with the sectioned segment format + serve-from-storage indexes (roadmap Phases 5–6, v0.5/v0.6)
  • Convergence is periodic (refresh interval, default 5s), not synchronous; min_seq covers read-your-writes
  • Rolling-upgrade caveat: don't run v0.3 and v0.4 writers against one bucket (id-allocator migration seeds from the bucket high-water mark)
  • No namespace generations yet: delete/recreate detection uses the config created_at (documented in the roadmap as a v0.5 hardening item)

Public plan for evolving Compass from a cloud-durable single-node engine
(v0.3.0) to a fully serverless database, in eight phases across four releases
— every item additive and Apache 2.0. v0.4.0 targets Phases 0-3 ("warm
serverless"): stateless writes, lazy attach, manifest refresh.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
The object-storage feature and its real-S3 integration tests never ran in CI
— only the default build was tested, so a cloud-path regression would merge
green. Add a test-cloud job running `cargo test --features object-storage`
against a MinIO service container with a pre-created bucket, which also
exercises the env-gated s3_integration tests (server-side ETag CAS, LSM
lifecycle with GC, concurrent appends) on every PR.

Also add a dependency-free DCO check requiring Signed-off-by on every PR
commit, ahead of external contributions.

Neither job touches the required-checks list.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Cold rebuild fabricated collection metadata: vector-space specs were inferred
from recovered embeddings (model:"recovered"), created_at was reset to now(),
and CollectionConfig — including embed_model — was silently replaced with
defaults. A detached/recovered collection therefore lost user configuration.

- create_collection (cloud mode) now writes a create-only
  {ns}/collection.json (name, created_at, vector spaces, default space, dims,
  config) plus an EMPTY manifest, so zero-ingest collections are discoverable
  from a fresh disk and stateless writers can validate against real config.
  Bucket failures roll the local creation back; colliding with a pre-v0.4
  namespace that has data is refused.
- rebuild_collection_from_storage reads the bucket config; the embedding
  inference remains only as a pre-v0.4 fallback and back-fills the bucket
  config create-only (organic migration, runs at most once).
- Vector-space CRUD (add/delete/set-default/mark-active) is bucket-first:
  short-lock validate -> CAS the bucket config with revalidation against the
  latest doc -> write-lock local apply. No S3 round-trip ever holds the
  collections lock (existing codebase rule).
- All gated on cloud_mode: in local mode a Storage-issued collection.json
  would collide with the real local metadata file.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Chunk ids were minted from a per-node next_id counter, so a write required
the stateful node that had the collection attached — and two nodes writing
one namespace would collide. Recovery makes the collision concrete: a
rebuilt node computes next_id = max_id+1, which can land inside another
writer's active block.

- storage/id_alloc.rs: {ns}/id-alloc holds the next block start; blocks of
  max(10_000, batch) are claimed via the proven get_versioned/put_if_match
  CAS loop. Concurrent claims receive disjoint ranges (tested with 16
  racing claimants). Crashed writers leak at most their pooled ranges —
  gaps are fine; the defended invariant is no-reuse, and replay ordering
  comes from manifest seq, not ids.
- In cloud mode EVERY ingest path allocates from blocks (attached nodes
  pool on the collection; refills never hold the collections lock across
  the S3 round-trip). next_id becomes a diagnostic high-water mark. Local
  mode is untouched.
- Seeding: create_collection seeds the allocator at 0; pre-v0.4 namespaces
  migrate on first claim by seeding from the bucket-derived high-water
  mark (create-only, race-safe). Rolling caveat documented in the roadmap:
  do not run v0.3 and v0.4 writers against one bucket.
- COMPASS_ROLE=writer: durable-append-only node. Boots instantly (no local
  collections, no recovery), validates dims against the cached bucket
  config (re-fetching once on validation failure so a stale cache never
  poisons a durable fragment), claims ids, appends ONE WAL fragment, and
  returns. Deletes append tombstones (idempotent on replay); relation
  creates store target_status "missing" (re-resolved at read time on
  serving nodes); queries and delete-by-filter are refused with clear
  errors. Consistency contract: durable immediately, searchable after a
  serving node's refresh/attach.

Tests: writer end-to-end with zero local state; writer/attached id
disjointness; pre-v0.4 allocator migration; bucket-config recovery
(real model specs + created_at survive cold rebuild, replacing the
model:"recovered" inference); zero-ingest collections survive node loss.
Suites: 103 default / 133 object-storage, all green.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
…n_seq

Serving nodes never re-read the manifest after boot, so two nodes on one
bucket diverged forever, and a stateless writer's fragments were invisible
until a restart.

- SeqTracker per collection: `contiguous` frontier + out-of-band set. A
  node's own appends apply locally ahead of remote fragments with earlier
  seqs, so a single watermark would skip those remote fragments forever;
  the out-of-band set closes that hole. All five local append sites thread
  their seq into the tracker, and a check-and-mark under the write lock
  resolves the race where the refresher applies a node's own fragment
  between its S3 append and lock reacquisition (replay is idempotent).
- apply_fragment_locally: one shared replay path mirroring materialize's
  kind dispatch (data/tombstone/relation upsert+delete), reused by ingest,
  delete, and the refresher. Data replay skips already-present ids (so
  chunk_count can't double-count) and QUARANTINES wrong-dims embeddings
  with a loud error instead of corrupting the mmap vector file.
- refresh_collection: poll the manifest, apply fragments past the frontier
  in seq order. Two-branch compaction rule: watermark within our frontier
  -> skip segments and replay the tail; watermark PAST our frontier means
  fragments we never saw were folded -> full re-attach. Background
  refresher task (COMPASS_REFRESH_INTERVAL, default 5s, 0 disables) holds
  a Weak so it dies with the manager.
- applied_seq persisted in collection metadata: a persistent-disk restart
  knows how fresh its local state is and catches up the delta instead of
  rebuilding (may lag on relation-only applies; replay is idempotent).
- Read-your-writes: ingest/delete responses carry the fragment seq;
  SearchRequest.min_seq refreshes-then-serves with a bounded (2s) wait,
  rejecting seqs beyond the write history.

Tests: two-node convergence (chunks, deletes, relations); no double-apply
of own writes; both compaction branches (stale node re-attaches, current
node skips); cross-node min_seq read-your-writes. Suites: 103 default /
137 object-storage.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
…budget

Cloud boot rebuilt EVERY collection from the bucket before serving — cold
start was O(total data) and attached state was unbounded, which caps how
many namespaces one worker can host.

- COMPASS_LAZY_ATTACH: boot registers bucket namespaces (one delimiter
  listing) and attaches on first request. A request stampede on a cold
  namespace rebuilds ONCE via a per-namespace mutex; the global collections
  lock is never held across the rebuild. Collections created by OTHER nodes
  after boot attach on demand too (registry miss falls through to a bucket
  existence check). Default off — eager boot remains today's behavior.
- COMPASS_MAX_ATTACHED: LRU budget. Past it, the least-recently-used
  collection detaches: removed from the map under the write lock, local
  files deleted only after the lock is released. Detach is safe because the
  bucket now carries everything (config included — the prior commit); the
  namespace stays registered and re-attaches on demand with all data.
- Every entry point (search, ingest, deletes, relations, facets, vector-
  space CRUD, compact, get/list) ensures attachment first; list/get answer
  from bucket configs for registered-but-unattached namespaces (live counts
  are known only once attached — documented).
- new_with_storage_opts: fully-explicit constructor (role, lazy, budget) so
  tests avoid process-global env races; env parsing stays in
  new_with_storage. Writer-role nodes skip local loading and recovery
  entirely — instant boot.

Tests: lazy boot does not rebuild; 8-way attach stampede attaches once;
LRU eviction at budget 1 with full-data re-attach; foreign creates attach
on demand. Suites: 103 default / 140 object-storage.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
…ion bugs

An independent review round on the warm-serverless work found three critical
and six high-severity issues before merge. All fixed here, each with a
regression test:

- Config convergence (critical): vector-space changes are CAS'd into the
  bucket config, not written as fragments, so already-attached nodes never
  learned about them — and then QUARANTINED chunks carrying the new space
  (silent per-node data loss). refresh_collection now syncs the bucket
  config (spaces, default, CollectionConfig) before replaying fragments.
- Mark-after-apply (critical): delete/relations/ingest recorded a fragment
  seq as applied BEFORE applying it, so a failed local apply was invisible
  to the refresher forever (a node would serve deleted data indefinitely).
  All sites now mark only after a successful apply; replay is idempotent.
- Serialized destructive transitions (critical): refresh-triggered full
  re-attach and LRU eviction now take the same per-namespace attach mutex
  as ensure_attached — two rebuilds (or a rebuild racing a file deletion)
  can no longer run into the same live index directory.
- Delete/recreate detection: a vanished manifest detaches the collection
  (instead of warning forever while serving dead data); a recreated
  collection (bucket created_at differs) forces a full re-attach. Local
  caches (writer pools, configs, registry, attach locks) purge on delete.
- Writer hardening: deletes validate the namespace (a tombstone for a bogus
  name used to CREATE a phantom bucket namespace) and reject ids at/past
  the allocator frontier (a u64::MAX id would poison max_id -> next_id
  overflow / id reuse on every future rebuild). Writers refuse
  create_collection and vector-space CRUD with clear errors.
- True LRU: last_used is now stamped on search/ingest/facets/relations —
  eviction previously keyed on attach order and evicted the HOTTEST
  collection under budget pressure (rebuild thrash).
- Refresher efficiency: fragment refs are filtered by seq BEFORE payload
  fetches (a caught-up node downloads nothing per tick) and fragments apply
  under per-fragment lock holds, so a large backlog can't cause a node-wide
  read outage. Eviction during an ingest's S3-append gap no longer erases
  the (healthy, durable) batch — evicted is distinguished from deleted.
- delete_collection works on lazy/evicted collections, never holds the
  global lock across S3, and purges all namespace caches.
- Keymap persistence (pre-existing, exposed by block ids): sub-1000-vector
  collections never persisted the HNSW keymap, silently relying on identity
  key->id mapping that broke as soon as ids were non-dense. The keymap is
  now saved on every build and synthesized as identity for pre-fix dirs.
- CI: DCO ignores merge commits; the MinIO job fails loudly if the
  integration tests were silently skipped (env-drift guard).

New tests: SeqTracker unit semantics; true-LRU eviction; config propagation
across nodes; ingest racing a refresher loop; persistent-disk restart
catching up a writer's delta; wrong-dims quarantine without corruption;
min_seq local-mode + boundary; writer phantom-namespace/absurd-id rejection;
delete+recreate detection. Suites: 103 default / 149 object-storage.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
The bitnami/minio service-container tag didn't exist, and GitHub service
containers can't override the image command that minio/minio requires
(`server /data`). Start MinIO with docker run (same images as
docker-compose.minio.yml), wait on its health endpoint, and create the
bucket with mc — the silent-skip guard already fails the job if the
integration tests don't actually run.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Three scale walls fell together because they shared one root: segments were
a single JSON object produced by an O(collection) fold.

- JSON segments encoded f32 embeddings as decimal text (~10x bloat) and were
  written with a single PUT (S3 caps those at 5GB) — compaction simply
  failed once the live set crossed a few GB, roughly 1-3M chunks.
- Compaction materialized the ENTIRE collection into RAM every 32 fragments
  (O(collection) memory + IO per cycle; quadratic write amplification).

Now:
- Segment v2: magic-tagged sectioned binary — chunk metadata (JSON, sans
  embeddings), embeddings per space as raw f32 LE rows keyed by id,
  relations, cross-segment tombstone sections, max_id. v1 JSON and the
  oldest bare-array form still decode (versioned fallback); unknown
  sections are skipped for forward compat.
- Storage::put_large: multipart upload on the object-store backend (16MB
  parts) past a single-PUT threshold; segments are immutable and UUID-keyed
  so no CAS token is needed on them.
- Partitioned compaction: the routine cycle folds ONLY the WAL tail into an
  APPENDED segment — O(batch), not O(collection). Deletes that don't match
  anything within the tail are carried as segment tombstone sections and
  applied against older segments at materialize time (relation deletes
  too). A full merge into one segment runs only past 8 accumulated
  segments — the sole O(live-set) operation, 1/8th as often. GC discipline
  is unchanged: folded objects stage one cycle, CAS losers delete their
  orphan segment.

Tests: v2 round-trip (multi-space embeddings, tombstones), cross-segment
delete via a real two-fold sequence, GC bounds re-verified across 14 cycles
crossing the merge threshold. Suites: 105 default / 151 object-storage.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
…d HNSW saves

Two more scale walls: every delete (and every replayed fragment) rebuilt the
ENTIRE filter index, and every ingest batch rewrote the ENTIRE HNSW index
file — both O(collection) per write, disqualifying past ~10M chunks.

- FilterIndex: the numeric range index moves from a sorted Vec (O(N) per
  update) to a BTreeMap keyed by total-order-encoded f64 bits (O(log N)
  inserts/removes, range scans union pre-grouped treemaps). New remove()
  reverses insert() exactly, pruning empty entries. Ingest/replay now
  insert incrementally; deletes and ingest-failure compensation remove
  incrementally; full rebuilds remain only on attach/recovery. finalize()
  is a no-op kept for API compat; the (unwired) persistence codec is
  updated for the new layout.
- HNSW: the in-RAM index stays mutable across batches (first mutation loads
  from disk once); the index FILE is rewritten every 16 batches instead of
  every batch. Vectors are already durable per batch in the mmap file, so a
  crash between saves leaves only a stale index — detected at load
  (index.size() < keymap length) and rebuilt from the mmap, then saved.
  Save-batching state lives on the collection, not the vector state.

Suites: 105 default / 151 object-storage — including the delete/facet tests
that verify incremental filter maintenance agrees with the old full rebuild.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
LoadedCollection held EVERY chunk — embeddings included — in a RAM HashMap,
capping a serving node somewhere in the 5-20M chunk range regardless of
compute. The bounded LRU read-through cache existed (search/chunk_cache.rs)
but was never wired.

- chunk_store is now the ChunkCache (bounded LRU over redb, default 100k
  resident): search assembly reads hits via get/get_batch; scoring metadata
  batches through it; parent enrichment reads through it; deletes fetch
  metadata for incremental filter-index removal through it.
- Existence checks (relation target_status, delete eligibility, replay
  dedup) go through the filter index universe — a treemap of LIVE ids
  maintained incrementally on every path, so no chunk load is needed to
  answer "does id X exist". Replay dedup additionally consults tombstones
  (a deleted id must not be re-applied just because it left the universe).
- Full scans (delete-by-filter, rebuild-job export, TAMS segment lookup)
  stream from redb via for_each instead of iterating a resident map. Boot
  rehydration builds the filter index streaming and keeps only the max-id
  scan; the chunks themselves stay on disk.

The entire existing suite — search, relations, deletes, facets, TAMS,
cloud convergence, lazy attach — passes unchanged on the out-of-core path:
105 default / 151 object-storage, clippy clean.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Operating multi-node deployments blind was a production blocker. /metrics
(Prometheus text, unauthenticated like /health) exposes ingest/search/delete
counters, refresh convergence (fragments applied, forced re-attaches),
attach counts + cumulative time, compactions, and quarantined chunks, plus
per-collection chunk-count and applied-seq gauges. Hand-rolled atomics — no
new metrics dependency; call sites go through free functions so a real
facade can replace the implementation later.

COMPASS_MAX_CONCURRENCY bounds in-flight requests (tower global concurrency
limit) instead of queueing without limit; unset = unlimited.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
COMPASS_SCALE_N gates a harness that ingests N synthetic chunks through the
full cloud path (local-disk Storage backend — same code, disk-bound),
measures ingest throughput, cold-attach time, and search latency, and skips
cleanly when unset. First measured point (250k chunks, 128 dims, under ARM
emulation): 1,603 chunks/s ingest, 134.5s cold attach, 5.5ms search.

docs/scale-envelope.md records measured numbers only, names cold-attach as
the binding constraint now that the RAM/segment/compaction/per-write walls
are gone, and states plainly that billion-vector serving waits on Phase 6
serve-from-storage indexes.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
…action safety

An adversarial review of the five scale-wall commits found two criticals and
a set of high/medium issues; all fixed:

- Boot panic on DEFAULT env (critical): the concurrency layer passed
  usize::MAX/2 permits to tokio's Semaphore, whose hard cap is
  usize::MAX>>3 — the server asserted at startup whenever
  COMPASS_MAX_CONCURRENCY was unset. Capped under MAX_PERMITS.
- HNSW hole (critical): a batch erroring after in-RAM adds but before a
  save left the next batch reloading a STALE index file, adding only new
  vectors, and saving — permanently missing up to 15 committed batches
  (recall silently degraded until a cold attach). The fresh-load path now
  detects index.size() < base_key and heals from the mmap before adding.
- Compaction folds now use the STRICT fragment reader (the tolerant one
  skips NotFound — advancing the watermark past an unread fragment would be
  silent data loss), and the segment merge NEVER runs in the same
  invocation as a fold, restoring the one-cycle GC grace for readers
  holding the pre-fold manifest.
- Stale-index rebuild truncates at the keymap length (orphan mmap tail rows
  can't be indexed under fabricated ids); ambiguous manifest-commit
  failures no longer delete the possibly-referenced new segment (only a
  definite CAS conflict does); skipped filter-index removals are logged
  loudly; /metrics reads attached collections only (it was an
  unauthenticated one-S3-GET-per-namespace-per-scrape amplifier in lazy
  mode); the scale harness exercises put_large.
- fold_tail keeps carried tombstones on re-create (safe: materialize
  applies segment tombstones before the segment's own chunks) so folded
  and unfolded relation state can never diverge.

Second measured point (500k chunks, 128 dims, emulated): 1,392 chunks/s
ingest, 369.3s cold attach, 11.2ms search — attach scales linearly, as the
envelope doc now states along with the merge/attach RAM caveat.
Suites: 105 default / 152 object-storage, clippy clean.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
…clude deleted

The live E2E harness (scripts/e2e.sh, added here) caught facets returning
{} on a healthy collection. Three distinct bugs, all in the same design:

- build_index returned facet bitsets for the new batch only, and
  apply_ingest_commit replaced the collection's facet state with them —
  every ingest after the first wiped all prior facets (latent since v0.2).
- open_index returned empty facet state and nothing ever rebuilt it, so
  facets were permanently empty after any restart or re-attach.
- The bitsets were dense arrays keyed by insertion position while the
  query side intersected them by chunk id; id-block allocation (cloud
  mode) breaks the dense-id assumption entirely.

Fix: facets are now HashMap<field, HashMap<value, RoaringTreemap>> keyed
by chunk id. Ingest absorbs prior state instead of replacing it; the
load/rebuild chunk scans rebuild facets in the same pass that builds the
filter index; and get_facets intersects each value's treemap with the
FilterIndex live-id universe, so tombstoned chunks stop inflating counts
(previously deleted chunks were counted until a full FTS rebuild).

scripts/e2e.sh is a 44-check live-stack harness covering every endpoint,
every filter operator, writer-role behavior, refresh visibility, and
compaction survival; it is what caught this.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
The v0.3.0 comparison bench caught a warm-restart regression: 20.2s vs
1.1s at 100k chunks. Boot logs show why:

    HNSW index at /app/data/benchcoll/vectors/default.index is stale
    (97000 < 100000); rebuilding from mmap

Batched HNSW persistence (HNSW_SAVE_EVERY=16) means a clean shutdown can
leave the index file up to 15 batches behind the per-batch-durable mmap
and keymap — so nearly every warm restart of an actively-written
collection took the stale path, and that path re-inserted ALL vectors
(O(collection), ~20s per 100k under emulation).

The runtime heal in apply_ingest_commit already does this right: load
the existing graph and append only rows index.size()..keymap.len() from
the mmap. Mirror it at load time, then re-view the saved file so the
healed index stays mmap-backed instead of RAM-resident. The keymap is
persisted per batch, so the index file is only ever behind it, never
ahead — appending the tail is always sufficient.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Three independent audit passes (dead code, additive-ness, over-engineering)
swept the codebase. This commit acts on the mechanical findings:

Deleted (grep-proven zero call sites in any target):
- BitSet dense-bitset facet structure — fully replaced by chunk-id-keyed
  RoaringTreemaps; the query side had already stopped using it
- search/backend.rs (VectorIndex wiring, UsearchHnswIndex, build_backend,
  COMPASS_BACKEND env) — zero constructors anywhere; the live vector path
  calls usearch directly. With it go the compass-index-api dependency and
  the gpu feature, which gated code no runtime path could ever select.
  The compass-index-api / compass-vector-gpu workspace crates remain for a
  future real wiring but no longer ship weight into the binary
- lsm::compact generic primitive — the live compaction path is
  read_uncompacted_fragments_strict + append_segment /
  replace_with_single_segment; its three tests are PORTED to those
  primitives (fold, watermark advance, strict-read abort on missing
  fragment), not dropped
- filter.rs (matches_filters + 7 tests): delete_by_filter now resolves ids
  through the same roaring FilterIndex::eligible pushdown search uses, so
  search and delete-by-filter can never disagree on filter semantics
- FilterExpr::eval + eval_predicate + stringify_metadata: the third,
  never-called filter evaluator
- FilterIndex serialize/deserialize + MetadataKey codec (~200 lines of
  never-wired persistence scaffolding), FilterIndex::finalize() no-op and
  its call sites, FilterIndex::is_empty
- save_vectors legacy writer (reader stays for migration), VectorState.dims
  and five FtsState field handles nobody read, three unread
  FilteredSearchExplain fields, SharedStorage alias,
  build_filter_index_from_chunks, RelationshipGraph::len
- rayon dependency: its only use was current_num_threads() inside
  128.max(...) — now std::thread::available_parallelism via one helper

Fixed (dead-code finding that was actually a live bug):
- mark_vector_space_active was never called: a completed vector-space
  rebuild updated only the in-RAM progress tracker — the space stayed
  status=building in collection metadata and the rebuilt index was never
  hot-loaded until restart. start_rebuild now takes the manager and calls
  it on completion; activation failure is reported as a failed rebuild
- The unreachable filters branch in tantivy_fts::search (with its stale
  TODO) and the vestigial existing_count param of build_index are gone

Lint hygiene: dead_code removed from the crate-level allow list — it had
been suppressing ALL dead-code detection, which is how this pile
accumulated. Test-only helpers are now cfg(test); the two deliberate
API-surface items (Storage::get_range, ObjectMeta size/version — the
sectioned-segment range-read contract) carry targeted allows with reasons.

Net -1,178 lines. Suites: 94 local / 141 object-storage, clippy clean
under -D warnings with the lint live.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
- local_mode_writes_no_wal now also asserts the id-block allocator object
  is never seeded locally and that ids stay dense next_id values across
  batches (the audit flagged both as untested local-mode invariants)
- New writer_role_is_neutralized_in_local_mode: a stray COMPASS_ROLE=writer
  on a local-disk deployment must not disable serving — cloud_mode forces
  the role to Full; previously only code-verified
- ARCHITECTURE.md data layout matched a format that no longer exists
  (meta.json/chunks.bin/index.usearch); now documents the real one
  (collection.json/chunks.redb/tantivy//vectors/<space>.*) plus the cloud
  bucket layout (manifest, wal/, segments/, id-alloc)
- CLAUDE.md/README API tables gain GET /metrics and /segments/at; CLAUDE.md
  stops claiming a wired GPU feature (the crate is standalone until wired)
- serverless-roadmap status: Phases 0-3 shipped on this branch
- .env.example documents COMPASS_MAX_CONCURRENCY
- Deleted an orphaned doc block and a stale 'Planned follow-up' note that
  described already-shipped filter pushdown

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Pure file move: 49% of the 7,118-line module was #[cfg(test)] code. Each
module is now a child file (same module tree, so super::* keeps private
access); mod.rs is 3,720 lines of engine code. No test changed: 142 pass.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Searching a missing collection returned HTTP 500 (api/search.rs mapped
every engine error to INTERNAL_SERVER_ERROR) and ingesting into one
returned 400 — stringly Box<dyn Error> carried no classification, and
three handlers (delete, relations, segments) each had their own copy of
a substring-sniffing mapper ('msg.contains("not found")').

All 32 not-found construction sites in the collection manager now build a
typed NotFound error; one shared api::error_response maps it to 404 by
downcast (no string sniffing), logs 500-class details server-side, and
keeps each handler's default for everything else. The three duplicate
mappers are gone.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
The dead-code pass gated it #[cfg(all(test, feature))] but the
object-store backend calls it at RUNTIME (object_store_backend.rs:219).
No CI job compiles the non-test object-storage combination — only the
release Docker build does — so the gate slipped through green checks:

    error[E0599]: no method named `is_empty` found for `&Version`

Gate is now #[cfg(any(test, feature = "object-storage"))]: present for
the backend and the integration tests, still dead-code-checked in the
default build.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Nothing in CI compiled the cloud feature without cfg(test) — the exact
combination the release Docker image builds — so a mis-scoped cfg gate
broke the image while every check stayed green. One cargo check line
closes the gap.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
The any(test, feature) gate made it dead code in test-without-feature
builds, failing clippy --all-targets and the default-feature test jobs.
Every user (backend runtime + s3_integration tests) is behind the
feature, so gate on the feature alone. All four build combinations
verified: default test, feature test, feature non-test, clippy
--all-targets.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
@EdgarBabajanyan

Copy link
Copy Markdown
Contributor Author

Shipped in v0.4.0 via the consolidated release PR #13 (all commits, authorship, and sign-offs from this branch landed there verbatim).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant