Phases 16.2 + L2 + 17 VRAM gate + MySQL + 18 Lance hybrid milestone

Five threads of work landing as one milestone — all individually
verified end-to-end against real data, full release build clean,
46 unit tests pass.

## Phase 16.2 / 16.5 — autotune agent + ingest triggers

`vectord::agent` is a long-running tokio task that watches the trial
journal and autonomously proposes + runs new HNSW configs. Distinct
from `autotune::run_autotune` (synchronous one-shot grid). Triggered
on POST /vectors/agent/enqueue/{idx} or by the periodic wake; ingest
paths now push DatasetAppended events when an index's source dataset
gets re-ingested. Rate-limited (max_trials_per_hour) and cooldown-
gated so it can't saturate Ollama under live load.

The proposer is ε-greedy around the current champion: with prob 0.25
sample random from full bounds, otherwise perturb champion ± small
delta on both axes. Dedup against history. Deterministic — RNG seeded
from history.len() so the same journal state proposes the same next
config (helps offline replay debugging).

`[agent]` config section in lakehouse.toml; opt-in via enabled=true.

## Federation Layer 2 — runtime bucket lifecycle + per-index scoping

`BucketRegistry.buckets` moved to `std::sync::RwLock<HashMap>` so
buckets can be added/removed after startup. POST /storage/buckets
provisions at runtime; DELETE /storage/buckets/{name} unregisters
(refuses primary/rescue with 403). Local-backend buckets get their
root directory auto-created.

`IndexMeta.bucket` (default "primary" via serde) records each index's
home bucket. `TrialJournal` and `PromotionRegistry` now hold
Arc<BucketRegistry> + IndexRegistry; they resolve target store per-
index via IndexMeta.bucket. PromotionRegistry::list_all scans every
bucket and dedups by index_name. Pre-federation indexes keep working
unchanged — they just default to primary.

`ModelProfile.bucket: Option<String>` declares per-profile artifact
home. POST /vectors/profile/{id}/activate auto-provisions the
profile's bucket under storage.profile_root if not yet registered.

EvalSets stay primary-only for now — noted gap, low-risk to extend
later with the same resolver pattern.

## Phase 17 — VRAM-aware two-profile gate

Sidecar gains POST /admin/unload (Ollama keep_alive=0 trick — forces
immediate VRAM release), POST /admin/preload (keep_alive=5m with
empty prompt, takes the slot warm), and GET /admin/vram (combines
nvidia-smi snapshot with Ollama /api/ps). Exposed via aibridge as
unload_model / preload_model / vram_snapshot.

`VectorState.active_profile` is the GPU-slot singleton —
Arc<RwLock<Option<ActiveProfileSlot>>>. activate_profile checks for
a previous profile with a different ollama_name and unloads it
before preloading the new one; same-model reactivations skip the
unload (Ollama no-ops). New routes: POST /vectors/profile/{id}/
deactivate (unload + clear slot), GET /vectors/profile/active.

Verified live: staffing-recruiter (qwen2.5) → docs-assistant
(mistral) swap freed qwen2.5 from VRAM and loaded mistral. nomic-
embed-text persists across swaps because both profiles use it —
free optimization that fell out of the design. Scoped search
correctly 403s cross-profile in both directions.

## MySQL streaming connector

`crates/ingestd/src/my_stream.rs` mirrors pg_stream.rs for MySQL.
Pure-rust `mysql_async` driver (default-features=false to avoid C
deps). Same OFFSET pagination, same Parquet-streaming write shape.
Type mapping per ADR-010: int/bigint → Int32/Int64, decimal/float
→ Float64, tinyint(1)/bool → Boolean, everything else → Utf8 with
fallback parsers for date/time/json/uuid via Display.

POST /ingest/mysql parallel to /ingest/db. Same PII auto-detection,
same lineage capture (source_system="mysql"), same agent-trigger
hook. `redact_dsn` generalized — was hardcoded to "postgresql://"
length, now works for any scheme://user:pass@host/path URL (latent
PII leak fix for MySQL DSNs).

Verified live against MariaDB on localhost: 10 rows × 9 columns of
test data round-tripped through datatypes int/varchar/decimal/
tinyint/datetime/text. PII detection auto-flagged name + email.
Aggregation queries through DataFusion match the source values
exactly.

## Phase 18 — Hybrid Parquet+HNSW ⊕ Lance backend (ADR-019)

`vectord-lance` is a new firewall crate. Lance pulls Arrow 57 and
DataFusion 52 — incompatible with the rest of the workspace's
Arrow 55 / DataFusion 47. The firewall isolates that dep tree:
public API uses only std types (Vec<f32>, Vec<String>, Hit, Row,
*Stats), so no Arrow types cross the crate boundary and nothing
propagates to vectord. The ADR-019 path that didn't ship until now.

`vectord::lance_backend::LanceRegistry` lazy-creates a
LanceVectorStore per index, resolving bucket → URI via the
conventional local-bucket layout. `IndexMeta.vector_backend` and
`ModelProfile.vector_backend` carry the choice (default Parquet so
existing indexes unchanged).

Six routes under /vectors/lance/*:
- migrate/{idx}: convert binary-blob Parquet → Lance FixedSizeList
- index/{idx}: build IVF_PQ
- search/{idx}: vector search (embed via sidecar)
- doc/{idx}/{doc_id}: random row fetch
- append/{idx}: native fragment append
- stats/{idx}: row count + index presence

Verified live on the real resumes_100k_v2 corpus (100K × 768d):
- Migrate: 0.57s
- Build IVF_PQ index: 16.2s (matches ADR-019 bench; 14× faster than
  HNSW's 230s for the same data)
- Search end-to-end (Ollama embed + Lance scan): 23-53ms
- Random doc_id fetch: 5-7ms (filter scan; faster than Parquet's
  ~35ms full-file scan, slower than the bench's 311us positional
  take — would close that gap with a scalar btree on doc_id)
- Append 100 rows: 3.3ms / +320KB on disk vs Parquet's required
  full ~330MB rewrite — the structural win
- Index survives append; both backends coexist cleanly

## Known follow-ups not in this milestone

- ModelProfile.vector_backend doesn't yet auto-route /vectors/profile/
  {id}/search to Lance; callers go through /vectors/lance/* directly
- Scalar btree on doc_id (closes the 5-7ms → ~300us gap)
- vectord-lance built default-features=false → no S3 yet
- IVF_PQ recall not measured (ADR-019 caveat) — needs a Lance-aware
  variant of the eval harness
- Watcher-path ingest doesn't push agent triggers (HTTP paths do)
- EvalSets still primary-only (federation gap)
- No PATCH endpoint to move an existing index between buckets
- The pre-existing storaged::append_log doctest fails to compile
  (malformed `{prefix}/` parses as code fence) — pre-existing bug,
  left for a focused fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-16 20:24:46 -05:00
co-authored by Claude Opus 4.6
parent 4e1c400f5d
commit 0d037cfac1
28 changed files with 3240 additions and 111 deletions
Generated
+251 -22
View File
@@ -262,7 +262,7 @@ dependencies = [
"arrow-schema 55.2.0",
"arrow-select 55.2.0",
"atoi",
"base64",
"base64 0.22.1",
"chrono",
"comfy-table",
"half",
@@ -284,7 +284,7 @@ dependencies = [
"arrow-schema 57.3.0",
"arrow-select 57.3.0",
"atoi",
"base64",
"base64 0.22.1",
"chrono",
"comfy-table",
"half",
@@ -738,6 +738,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@@ -757,6 +763,24 @@ dependencies = [
"num-traits",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.117",
]
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -871,6 +895,15 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "btoi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad"
dependencies = [
"num-traits",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
@@ -976,6 +1009,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "cfb"
version = "0.7.3"
@@ -1005,7 +1047,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
dependencies = [
"base64",
"base64 0.22.1",
"encoding_rs",
]
@@ -1070,6 +1112,26 @@ dependencies = [
"inout",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "combine"
version = "4.6.7"
@@ -1283,6 +1345,19 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -1638,7 +1713,7 @@ dependencies = [
"ahash",
"arrow 55.2.0",
"arrow-ipc 55.2.0",
"base64",
"base64 0.22.1",
"half",
"hashbrown 0.14.5",
"indexmap",
@@ -2041,7 +2116,7 @@ checksum = "2ddf0a0a2db5d2918349c978d42d80926c6aa2459cd8a3c533a84ec4bb63479e"
dependencies = [
"arrow 55.2.0",
"arrow-buffer 55.2.0",
"base64",
"base64 0.22.1",
"blake2",
"blake3",
"chrono",
@@ -2070,7 +2145,7 @@ checksum = "07356c94118d881130dd0ffbff127540407d969c8978736e324edcd6c41cd48f"
dependencies = [
"arrow 57.3.0",
"arrow-buffer 57.3.0",
"base64",
"base64 0.22.1",
"blake2",
"blake3",
"chrono",
@@ -2867,7 +2942,7 @@ dependencies = [
"async-tungstenite",
"axum",
"axum-core",
"base64",
"base64 0.22.1",
"bytes",
"ciborium",
"const-str",
@@ -2921,7 +2996,7 @@ checksum = "cda8b152e85121243741b9d5f2a3d8cb3c47a7b2299e902f98b6a7719915b0a2"
dependencies = [
"anyhow",
"axum-core",
"base64",
"base64 0.22.1",
"ciborium",
"dioxus-core",
"dioxus-document",
@@ -3325,6 +3400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"libz-sys",
"miniz_oxide",
"zlib-rs",
]
@@ -3705,7 +3781,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"headers-core",
"http",
@@ -3867,7 +3943,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -4063,6 +4139,7 @@ dependencies = [
"chrono",
"csv",
"lopdf",
"mysql_async",
"object_store",
"parquet 55.2.0",
"serde",
@@ -4074,6 +4151,7 @@ dependencies = [
"tokio-postgres",
"tracing",
"uuid",
"vectord",
]
[[package]]
@@ -4306,6 +4384,15 @@ dependencies = [
"bitflags",
]
[[package]]
name = "keyed_priority_queue"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d"
dependencies = [
"indexmap",
]
[[package]]
name = "lance"
version = "4.0.0"
@@ -4650,7 +4737,7 @@ dependencies = [
"tempfile",
"tokio",
"tracing",
"twox-hash",
"twox-hash 2.1.2",
"uuid",
]
@@ -4890,6 +4977,17 @@ dependencies = [
"libc",
]
[[package]]
name = "libz-sys"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -5012,7 +5110,7 @@ version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
dependencies = [
"twox-hash",
"twox-hash 2.1.2",
]
[[package]]
@@ -5021,7 +5119,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98c23545df7ecf1b16c303910a69b079e8e251d60f7dd2cc9b4177f2afaf1746"
dependencies = [
"twox-hash",
"twox-hash 2.1.2",
]
[[package]]
@@ -5255,6 +5353,68 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b"
[[package]]
name = "mysql_async"
version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0b66e411c31265e879d9814d03721f2daa7ad07337b6308cb4bb0cde7e6fd47"
dependencies = [
"bytes",
"crossbeam",
"flate2",
"futures-core",
"futures-sink",
"futures-util",
"keyed_priority_queue",
"lru",
"mysql_common",
"pem",
"percent-encoding",
"pin-project",
"rand 0.8.5",
"serde",
"serde_json",
"socket2 0.5.10",
"thiserror 1.0.69",
"tokio",
"tokio-util",
"twox-hash 1.6.3",
"url",
]
[[package]]
name = "mysql_common"
version = "0.32.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478b0ff3f7d67b79da2b96f56f334431aef65e15ba4b29dd74a4236e29582bdc"
dependencies = [
"base64 0.21.7",
"bindgen",
"bitflags",
"btoi",
"byteorder",
"bytes",
"cc",
"cmake",
"crc32fast",
"flate2",
"lazy_static",
"num-bigint",
"num-traits",
"rand 0.8.5",
"regex",
"saturating",
"serde",
"serde_json",
"sha1",
"sha2",
"smallvec",
"subprocess",
"thiserror 1.0.69",
"uuid",
"zstd",
]
[[package]]
name = "ndarray"
version = "0.16.1"
@@ -5485,7 +5645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbfbfff40aeccab00ec8a910b57ca8ecf4319b335c542f2edcd19dd25a1e2a00"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"bytes",
"chrono",
"form_urlencoded",
@@ -5667,7 +5827,7 @@ dependencies = [
"arrow-ipc 55.2.0",
"arrow-schema 55.2.0",
"arrow-select 55.2.0",
"base64",
"base64 0.22.1",
"brotli",
"bytes",
"chrono",
@@ -5685,7 +5845,7 @@ dependencies = [
"snap",
"thrift",
"tokio",
"twox-hash",
"twox-hash 2.1.2",
"zstd",
]
@@ -5703,7 +5863,7 @@ dependencies = [
"arrow-ipc 57.3.0",
"arrow-schema 57.3.0",
"arrow-select 57.3.0",
"base64",
"base64 0.22.1",
"brotli",
"bytes",
"chrono",
@@ -5719,7 +5879,7 @@ dependencies = [
"simdutf8",
"snap",
"thrift",
"twox-hash",
"twox-hash 2.1.2",
"zstd",
]
@@ -5741,6 +5901,16 @@ dependencies = [
"stfu8",
]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64 0.22.1",
"serde_core",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -5871,7 +6041,7 @@ version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491"
dependencies = [
"base64",
"base64 0.22.1",
"byteorder",
"bytes",
"fallible-iterator",
@@ -6112,6 +6282,7 @@ dependencies = [
"datafusion 47.0.0",
"futures",
"object_store",
"parquet 55.2.0",
"serde",
"serde_json",
"shared",
@@ -6427,7 +6598,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
@@ -6618,6 +6789,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "saturating"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
[[package]]
name = "schannel"
version = "0.1.29"
@@ -7039,6 +7216,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "std_prelude"
version = "0.2.12"
@@ -7106,6 +7289,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "subprocess"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c56e8662b206b9892d7a5a3f2ecdbcb455d3d6b259111373b7e08b8055158a8"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "subsecond"
version = "0.7.3"
@@ -7196,7 +7389,7 @@ checksum = "64a966cb0e76e311f09cf18507c9af192f15d34886ee43d7ba7c7e3803660c43"
dependencies = [
"aho-corasick",
"arc-swap",
"base64",
"base64 0.22.1",
"bitpacking",
"bon",
"byteorder",
@@ -7655,7 +7848,7 @@ checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9"
dependencies = [
"async-trait",
"axum",
"base64",
"base64 0.22.1",
"bytes",
"h2",
"http",
@@ -7872,6 +8065,17 @@ dependencies = [
"utf-8",
]
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"rand 0.8.5",
"static_assertions",
]
[[package]]
name = "twox-hash"
version = "2.1.2"
@@ -8004,6 +8208,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vectord"
version = "0.1.0"
@@ -8012,6 +8222,7 @@ dependencies = [
"arrow 55.2.0",
"axum",
"bytes",
"catalogd",
"chrono",
"instant-distance",
"object_store",
@@ -8023,6 +8234,24 @@ dependencies = [
"tokio",
"tracing",
"uuid",
"vectord-lance",
]
[[package]]
name = "vectord-lance"
version = "0.1.0"
dependencies = [
"arrow 57.3.0",
"arrow-array 57.3.0",
"arrow-schema 57.3.0",
"bytes",
"futures",
"lance",
"lance-index",
"lance-linalg",
"parquet 57.3.0",
"serde",
"tokio",
]
[[package]]