Two-tier hybrid search for Rust: sub-millisecond initial results via potion-128M, quality-refined rankings in 150ms via MiniLM-L6-v2. Combines lexical (Tantivy BM25) and semantic (vector cosine) search with Reciprocal Rank Fusion. Progressive iterator API, f16 SIMD vector index, feature-gated compilation.
84
stars
4,051
commits
Rust
primary language
Sep 11, 2026
updated
Two-tier hybrid local search for Rust and the fsfs standalone CLI: fast first-pass results, then quality refinement.
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/frankensearch/main/install.sh | bash -s -- --easy-mode
The installer verifies every downloaded archive before replacing a binary. A
missing or malformed checksum, unavailable SHA-256 tool, or mismatch is a hard
failure. If the platform has no full semantic release artifact, the ordinary
path builds the loader-capable default from source; it never silently installs
the model-free lite profile. Use --lite only when that reduced capability is
intentional. Intel macOS is the explicit exception: the pinned ONNX Runtime has
no supported x86_64 Darwin distribution, so ordinary semantic installation
fails with unsupported_platform and points to --lite instead of attempting
a source build that cannot succeed.
The Linux x86-64 full archive requires glibc 2.43 or newer; build from source on older glibc systems for full semantic search. The standard installer provisions and verifies the six registered production models separately; their download is roughly 1.64 GB. The two search tiers alone require roughly 621 MB.
When upgrading a Linux full installation from 1.9.0 or earlier, use the installer
above: those executables' fsfs update selects the MUSL lite archive and loses
semantic support. Older Apple Silicon lite installations should use
install.sh --lite to retain their profile. The 1.9.1 updater preserves both
the compiled ABI and the full/lite profile for subsequent updates.
Semantic indexes written by 1.7/1.8 require an explicit rebuild with the original
configuration: fsfs index /original/source --index-dir /existing/index.
Indexes written by 1.9.0 can be opened directly by 1.9.1.
The 1.10.0 release changes the embedder adapter version and therefore
requires that explicit rebuild for 1.9.x semantic indexes too. Keep the original
documents and configuration available; opening an old generation with the new
producer is refused. Model artifact checksums and native producer fingerprints
are unchanged, but the fast Potion tier has a new adapter fingerprint.
Installer goals:
fsfs currently builds from this workspace and uses the pinned nightly toolchain
(rust-toolchain.toml). Its default feature set compiles the Model2Vec and
FastEmbed loaders, but keeps the large model bytes outside the crate. Normal
workspace and crates.io builds therefore need no model artifacts at compile
time. The documented default path is loader-capable without any feature flags:
cargo +nightly install --path crates/frankensearch-fsfs
fsfs download-models potion-multilingual-128m
fsfs download-models all-minilm-l6-v2
fsfs download-models potion-multilingual-128m --verify
fsfs download-models all-minilm-l6-v2 --verify
fsfs version
fsfs status --format json
fsfs doctor --format json
For an uninstalled development binary, use
cargo build --locked -p frankensearch-fsfs --bin fsfs with the repository's
pinned toolchain, then run target/debug/fsfs with the same model setup.
The Linux DSR quality lane builds in a clean checkout, installs with
cargo install --locked --debug --path crates/frankensearch-fsfs --root <private-root>,
and verifies that the installed executable matches the Cargo build byte for byte.
It uses fresh HOME/XDG directories and checks repeat indexing, both vector
generations, ranked hybrid and vector-only search, and actual Initial/Refined
result records. Its executable quick-start
driver retains each run's artifacts and exercises model, result, authority,
timeout, child/listener and provenance failures. It needs Python 3.11+ and
strace for the warm-query network check.
Replay it with scripts/check_fsfs_executable_quickstart.sh --negative-probes --require-source.
--binary /absolute/path/to/fsfs checks a supplied executable as a smoke test;
its SHA-256 is recorded, while its source revision remains explicitly unknown.
The checker checkout revision cannot establish where that binary was built.
The two downloads use revision-pinned manifests and verify every file before
atomic promotion. Explicit --verify is fail-closed: a missing or corrupt
registered cache returns a typed nonzero error rather than a successful payload.
fsfs status reports manifest states (missing, incomplete, mismatch, or
verified); only fsfs doctor also opens each verified cache through the
compiled Model2Vec/FastEmbed loader. A hard doctor verdict exits nonzero with
one stable subsystem_error report with the failing checks in its context. If the cache is absent or offline mode
forbids acquisition, indexing fails with an actionable typed error; it never
substitutes hash control embeddings for semantic results. Use
--no-default-features alone produces the model-free lite binary. Add
--features semantic-native for the native semantic profile described below.
The default fsfs build can use pure-Rust F32 MiniLM for its quality tier:
fsfs download-models all-MiniLM-L6-v2-native
Set quality_model = "all-MiniLM-L6-v2-native" in the [indexing] section of
your fsfs configuration, then rebuild the index with that configuration.
Indexing, search, append and watch use the selected native producer;
fsfs status verifies its artifacts and fsfs doctor loads the native model.
The CLI supplies and drains its existing blocking pool. Library users of
FsfsRuntime attach their pool with with_native_blocking_pool.
Use an optimized (--release) executable for native inference. Cold model
loading can exceed the default 500 ms quality deadline even in a release build,
depending on the host, and return Initial results with a refinement-timeout
explanation.
In a daemon or TUI, successful initialization is retained even if that first
query times out, so later queries can reuse the loaded model. Each query still
has its own deadline and must match the index's producer identity.
Native and ONNX models have separate installation directories and producer identities. Changing this setting requires re-embedding the corpus. A missing or invalid native quality model leaves a new index fast-only; an existing native quality generation rejects incompatible models and preserves Initial results when refinement fails. The standard build's default quality model remains ONNX.
For an ONNX-free source build, use the explicit native profile:
cargo build --release -p frankensearch-fsfs --locked --no-default-features --features semantic-native
target/release/fsfs download-models
target/release/fsfs doctor
target/release/fsfs index ./documents
target/release/fsfs search "your query"
This profile includes Model2Vec, native quality inference and native reranking,
without the fastembed or ort dependency. It defaults to English native F32
MiniLM; bare download-models provisions the configured fast and quality models.
The optional reranker still needs fsfs download-models ms-marco-minilm-l-6-v2.
Existing ONNX quality generations need re-embedding with the native producer.
Enabling both semantic-native and semantic-loaders keeps the standard ONNX
default. Native-only builds update from source; fsfs update refuses to replace
them with a standard/lite release archive or an unclassified rollback backup.
fsfs update --check and rollback listing remain available. The profile cannot
initialize ONNX models; existing valid cached answers or an explicitly selected
daemon can still serve results from their attested producer.
The pure-Rust native feature also supports the 384-dimensional
paraphrase-multilingual-MiniLM-L12-v2 model for CJK and mixed-language
corpora. Acquisition and activation are both explicit; it is not part of the
default download set and is never selected merely because it is installed:
fsfs download-models paraphrase-multilingual-minilm-l12-v2
fsfs download-models paraphrase-multilingual-minilm-l12-v2 --verify
For fsfs, select it in your configuration and build a fresh index:
[indexing]
quality_model = "paraphrase-multilingual-minilm-l12-v2"
The default fsfs build uses the native multilingual loader for indexing, search, append, watch and doctor with this selection. Use an optimized executable and daemon-backed search: cold initialization can exceed the 500 ms quality budget, while the daemon retains the completed model for subsequent queries.
Library callers compiled with --features native load the verified model
directory with NativeEmbedder::load_multilingual(...) (or the corresponding
NativeEmbeddingModel variant). This producer has a distinct frozen identity
from all-MiniLM-L6-v2, despite sharing its 384-value output dimension. An
existing semantic index therefore cannot be reused or mixed: switch the model,
re-embed the complete corpus, and atomically publish that backfilled generation
before serving queries from it. Identity checks fail closed if vectors from the
two spaces are combined.
To build the full binary profile with Potion and MiniLM embedded, provision the revision-pinned inputs and select the feature explicitly:
scripts/rch-ensure-deps.sh --models-only
scripts/rch-ensure-deps.sh --models-only --check
cargo +nightly install --path crates/frankensearch-fsfs \
--no-default-features --features embedded-models
Provisioning validates every artifact's byte length and SHA-256. Cargo's build script performs no network access. The embedded profile changes distribution, not retrieval semantics: it uses the same loaders and registered model identities as the default source build.
The two semantic search tiers use roughly 621 MB of pinned model artifacts; the standard installer also provisions the other registered production models. First setup time depends on network speed. Later starts reuse the verification receipt while the exact manifest and file states remain unchanged.
# 1) Install
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/frankensearch/main/install.sh | bash -s -- --easy-mode
# 2) Acquire and independently verify both registered semantic tiers
fsfs download-models potion-multilingual-128m
fsfs download-models all-minilm-l6-v2
fsfs download-models potion-multilingual-128m --verify
fsfs download-models all-minilm-l6-v2 --verify
# 3) Index a directory
fsfs index ./my-project
# 4) Search
fsfs search "how does retry backoff work" --limit 5
Plain fsfs index <path> is a one-shot operation: it seals the generation and
exits. Use fsfs watch <path> or fsfs index <path> --watch only when you
explicitly want a long-running incremental watcher. Known limit (bd-z2nfa):
the watcher holds the vector generations' exclusive writer lock for its whole
life, so fsfs search from another process (and the query daemon) is refused
with fsvi.map_lock until the watcher stops; new and changed files are
ingested and become searchable the moment it exits, and the in-process TUI
cockpit can search a watched index.
Example output:
PHASE REFINED: 5 hit(s) for "how does retry backoff work"
vector generation: potion-multilingual-128M class=semantic
1. src/retry.rs score=0.033 [lexical+semantic]
Recover transient network failures with exponential <b>backoff</b>, bounded <b>retries</b>…
2. docs/failures.md score=0.016 [semantic]
…
5 results in 21ms
fsfs index writes two vector generations from the same documents:
vector/index.fsvi (the fast tier, potion) and vector/quality.fsvi (the
quality tier, all-MiniLM-L6-v2, its own embedding space). A search first
fuses the fast tier with Quill BM25 and emits INITIAL, then re-ranks the
head against the quality tier and emits REFINED; with --stream both
phases arrive as separate frames (query.stream.initial_ready, then
query.stream.refined_ready). If the quality model is not installed when
you index, the generation is built fast-only, fsfs doctor reports
semantic.quality_generation as a warning with the recorded reason, and
searches stop at INITIAL until you re-index with the model present.
fsfs status shows both generations (vector_generation_id,
quality_generation_id). Deletes, append-batch, compact, and watch mode
keep the two tiers in step. Both generations carry RaptorQ repair sidecars
(index.fsvi.fec, quality.fsvi.fec) like Quill's segments: fsfs doctor
verifies them as durability.vector_sidecars, fsfs compact restores a
generation whose bytes drifted before merging, and an in-place delete drops
the sidecar until the next compaction re-protects the file. The first search in a shell pays the model load
(about 3 s for potion, plus the MiniLM session for the quality stage); the
query daemon that fsfs search starts by default keeps later searches to tens
of milliseconds and exits on its own after ten idle minutes.
--stream also uses that warm daemon: Initial can arrive while quality
refinement is still running, followed by Refined or RefinementFailed and one terminal
event. JSONL and TOON announce complete cached replays with daemon_cache_hit.
Use --no-daemon for direct execution. A daemon policy or producer mismatch is
an error requiring a daemon restart or explicit direct execution; a request
that has delivered Initial is never silently retried.
The Unix socket transport admits at most 16 clients, bounds each phase frame to 4 MiB and each response to 8 MiB plus 64 KiB, and reports capacity failures explicitly. Slow readers do not hold the shared search state while socket writes drain. Queued and currently written response bytes are bounded by 16 × (8 MiB + 64 KiB); the shared query cache retains at most eight entries of at most 4 MiB of encoded phases each. Search admission to the shared index is serialized. Model/index memory and the active query's candidate set depend on the corpus and requested limit; these transport budgets are not a process memory cap.
frankensearch combines lexical and semantic retrieval with progressive delivery:
quill feature; Tantivy retained behind lexical-tantivy as the conformance oracle/migration lane)Result: responsive first answers plus better final ranking without blocking the UI.
potion-multilingual-128M + all-MiniLM-L6-v2) on first run. The embedded-models build profile additionally embeds those bytes for a zero-download first run (macOS/Windows full assets); the Linux full asset uses the loader + verified-download pathInitial, Refined, RefinementFailed)--stream) with machine-readable outputfsfs explain <rank|R-id|path> against the last search)table, json, jsonl, toon, csvfsfs search --rerank, search.rerank); ANN path via feature flagsfsfs index <dir> --format json emits one completion envelope after durable
publication; --format jsonl emits it as one line. The payload includes actual
file counts, vector model identities, and generation_complete. If embedding
retries are exhausted, semantic_deferred_files and a warning explain why the
published artifacts still need indexing resumed before semantic search.
# Basic search
fsfs search "structured concurrency" --limit 10
# Stream for agents/pipelines
fsfs search "query" --stream --format jsonl
# TOON mode
fsfs search "query" --stream --format toon
# Explain one result of the last search: by rank, R0-style id, or path
fsfs explain 1
# Re-score the refined head with the cross-encoder (one-time model install)
fsfs download-models ms-marco-minilm-l-6-v2
fsfs search "query" --rerank --format json
# Keep index fresh
fsfs index ~/projects --watch
# Health checks
fsfs doctor
Use this sequence to reproduce the core demo + benchmark evidence bundle:
# Progressive CLI behavior and machine-output surfaces
fsfs search "structured concurrency" --limit 10 --format table
fsfs search "structured concurrency" --limit 10 --stream --format jsonl
# Statistical benchmark regression lane (Tier-3 reproducibility anchor)
cargo test -p frankensearch-fsfs --test benchmark_baseline_matrix -- --nocapture
# Graceful-degradation/fault demonstration lane
cargo test -p frankensearch-fsfs --test pressure_simulation_harness \
scenario_long_run_soak_fault_injection_stays_within_drift_thresholds -- --nocapture
The benchmark lane validates deterministic artifact identity fields (dataset_sha256, matrix_sha256, samples_sha256) plus a fixed replay command contract.
Configuration precedence (highest first; fsfs config prints the resolved
source_precedence_applied for the current process):
fsfs.toml over user ~/.config/fsfs/config.toml)--fast-only, FRANKENSEARCH_FAST_ONLY=true, and [search] fast_only = true
disable quality work under the default performance pressure profile. The
highest-precedence value wins, including an explicit false. The strict
and degraded profiles keep quality disabled: a winning fast_only=false
returns a configuration error naming the profile. A hard pause disables quality
after source precedence and records the safety clamp in profile diagnostics.
Common environment variables:
| Variable | Purpose | Example |
|---|---|---|
FRANKENSEARCH_INDEX_DIR | Override index/data directory | ~/.local/share/frankensearch |
FRANKENSEARCH_MODEL_DIR | Override model location | ~/.cache/frankensearch/models |
FRANKENSEARCH_RERANK | Re-score the refined head with the cross-encoder (same as --rerank); needs fsfs download-models ms-marco-minilm-l-6-v2 once | 1 |
FRANKENSEARCH_FAST_ONLY | Disable quality work; follows CLI > environment > config precedence. false requires a profile that permits quality | true |
FRANKENSEARCH_QUALITY_WEIGHT | Blend quality vs fast tier | 0.7 |
FRANKENSEARCH_RRF_K | RRF constant | 60 |
FRANKENSEARCH_LOG | Tracing filter | info |
For full contracts and knobs:
docs/fsfs-config-contract.mddocs/fsfs-dual-mode-contract.mddocs/fsfs-packaging-release-install-contract.mddocs/fsfs-packaging-release-install-contract.md#host-migration-playbooks-priority-projectsdocs/fsfs-packaging-release-install-contract.md#staged-rollout-and-deterministic-fallback-protocoldocs/fsfs-packaging-release-install-contract.md#upgrade-and-migration-compatibility-verification-strategydocs/architecture/Pipeline summary:
Query
-> canonicalize
-> classify
-> fast embed + Quill BM25 (default lexical)
-> RRF fusion (initial)
-> quality embed (top candidates)
-> blend (and optional rerank)
-> refined results
Model path used in the default quality lane:
fsfs search --rerank / search.rerank; library rerank feature): pure-Rust frankentorch cross-encoder (native, ms-marco-MiniLM-L-6-v2 or jina-reranker) re-scores the refined head once fsfs download-models ms-marco-minilm-l-6-v2 has installed the weights; without a verified model the stage is skipped with a typed reason (query.stage.rerank.disabled.unavailable). A FastEmbed/ONNX alternative sits behind fastembed-rerankerfrankensearch is split into focused crates so each concern can evolve independently:
| Crate | Responsibility |
|---|---|
frankensearch-core | Shared types/traits/errors/config, query canonicalization/classification, metrics/eval helpers |
frankensearch-embed | Embedding backends and fallback stack (hash, model2vec, fastembed) |
frankensearch-index | FSVI vector storage, SIMD dot products, top-k search, optional native HNSW ANN |
frankensearch-lexical | Tantivy schema/index/search for BM25 lexical retrieval (conformance oracle + cass-compat interop lane) |
frankensearch-quill | Native pure-Rust BM25 lexical engine (FSLX segments, delta-visible indexing) |
frankensearch-quill-gauntlet | Differential conformance/perf gauntlet certifying Quill against the pinned Tantivy oracle |
frankensearch-fusion | RRF fusion, two-tier orchestration, blending, optional rerank integration |
frankensearch-rerank | Cross-encoder reranking (pure-Rust frankentorch native backend + optional FastEmbed) |
frankensearch-storage | FrankenSQLite metadata persistence, dedup/content-hash tracking, embedding queue |
frankensearch-durability | Repair/protection primitives for index artifacts and segment health |
crates/frankensearch-fsfs | Standalone CLI product around the library stack |
crates/frankensearch-tui | Shared TUI shell/input/theme/replay framework used by fsfs/ops |
crates/frankensearch-ops | Fleet observability/control-plane TUI and telemetry materialization. Experimental (decision 2026-09-02, bd-p6k61): no shipped telemetry source yet; its only producer is its own simulator, nothing depends on it, and no release lane builds it |
This separation gives you two options:
fsfs binary with progressive CLI/TUI workflowsAt execution time, the system follows this shape:
identifier, short keyword, natural language) for adaptive budgets.Initial results quickly.fast_only)Refined or RefinementFailed (graceful degradation path).RRF is rank-based and model-agnostic. It does not require score calibration across systems:
RRF(doc) = Σ_sources 1 / (K + rank(doc, source) + 1)
Default K is 60 (configurable with FRANKENSEARCH_RRF_K / rrf_k).
Why RRF:
During refinement, fast and quality semantic scores are normalized then blended:
blended_score = alpha * quality_score + (1 - alpha) * fast_score
alpha is controlled by quality_weight (default target 0.7).
When ties happen, ranking remains deterministic through stable tie-break logic
(including lexical comparison and doc_id ordering), which helps replayability
and makes diff-based evaluation much cleaner.
Vector data is stored in FSVI files with memory-mapped access:
f16 (good memory/quality tradeoff)f32 paths where neededWhy this matters:
f32 storage in common workloadsThe brute-force search path is optimized around:
This gives strong baseline behavior while ANN remains optional for larger corpora.
The async model uses asupersync and capability context (Cx), not Tokio.
Important implications:
This is useful if you need to embed search inside existing non-Tokio runtimes or strictly controlled execution environments.
Core engineering principles in this project:
Progressive delivery first
Fast initial answer, then quality refinement, instead of blocking on best possible ranking.
Graceful degradation
If quality tier/reranker/model loading fails, search still returns useful initial results.
Determinism and reproducibility
Stable ordering and artifact-driven evaluation support regression tracking and CI gates.
Explicit tradeoffs over hidden magic
Key knobs (rrf_k, blend weight, fast-only mode, candidate multipliers) are visible and tunable.
Practical hybrid retrieval
BM25 and embeddings are treated as complementary signals, not mutually exclusive choices.
frankensearch is especially strong when you need:
jsonl, toon) and explainability hooksIn short: it closes the gap between exact text lookup and semantic retrieval without forcing you into remote services or heavyweight distributed systems.
Common tuning patterns:
Need lower tail latency:
FRANKENSEARCH_FAST_ONLY=true or --fast-only; both work with the default performance profileNeed higher relevance quality:
Need memory efficiency:
Need operational clarity:
The repository includes explicit quality harnesses and statistical checks:
nDCG@K, MRR, Recall@K, plus bootstrap confidence intervalsThis keeps tuning decisions evidence-driven rather than anecdotal.
Being explicit about scope helps set expectations:
rg and has model/runtime overhead.Use rg/grep for strict exact matching and frankensearch when ranking by
intent and contextual relevance matters.
If you want to embed frankensearch directly in your Rust app, this is the
minimum end-to-end flow:
use std::path::Path;
use std::sync::Arc;
use frankensearch::{
EmbedderStack, IndexBuilder, TwoTierConfig, TwoTierIndex, TwoTierSearcher,
};
asupersync::test_utils::run_test_with_cx(|cx| async move {
// 1) Resolve a verified semantic embedder. HashEmbedder is a control
// double, not a semantic engine; auto_detect without models still
// returns that control stack.
let stack = EmbedderStack::auto_detect_semantic_with(Some(Path::new("./models")))
.expect("production search needs a verified semantic embedder");
// 2) Build an index from documents
IndexBuilder::new("./my_index")
.with_embedder_stack(stack)
.add_document("doc-1", "Rust ownership and borrowing")
.add_document("doc-2", "Structured concurrency with asupersync")
.build(&cx)
.await
.expect("index build should succeed");
// 3) Open and search with the same semantic family
let stack = EmbedderStack::auto_detect_semantic_with(Some(Path::new("./models")))
.expect("search must use the same semantic family the index was built with");
let index = Arc::new(
TwoTierIndex::open(Path::new("./my_index"), TwoTierConfig::default()).unwrap(),
);
let mut searcher = TwoTierSearcher::new(index, stack.fast_arc(), TwoTierConfig::default());
if let Some(quality) = stack.quality_arc() {
searcher = searcher.with_quality_embedder(quality);
}
let (results, metrics) = searcher
.search_collect(&cx, "ownership rules", 10)
.await
.expect("search should succeed");
println!("results={} phase1_ms={:.2}", results.len(), metrics.phase1_total_ms);
#[cfg(feature = "quill")]
{
// `open_hybrid` opens every arm of the index directory and attaches
// the active lexical reader (blue-green roots resolve to their
// active engine dir; foreign or damaged layouts are typed errors).
use frankensearch::LexicalRead;
let parts = frankensearch::open_hybrid(
&cx,
"./my_index",
frankensearch::TwoTierConfig::default(),
)
.await
.expect("open hybrid index");
let lexical = parts.lexical.expect("lexical arm attached");
let lexical_hits = lexical
.search(&cx, "ownership", 10)
.await
.expect("search lexical arm");
assert!(!lexical_hits.is_empty());
}
});
Notes:
EmbedderStack::auto_detect_semantic_with (or explicit model2vec + fastembed). auto_detect / auto_detect_with still return a hash-only stack when no model is present; that is a control policy, not a working semantic engine. IndexBuilder without an explicit stack already refuses that hash-only fallback.quill for the native bulk-built lexical index; use lexical-tantivy only for the explicit Tantivy oracle/comparator lane during migration.cass-compat only when interoperating with the external CASS tool's
schema-v8 Tantivy index at <base>/index/v8/. This foreign-format adapter is
not a Quill fallback and remains outside default builds.search_collect_with_text or full search(...) when you need negation filtering (-term) and rerank text access.TwoTierConfig explicit in code for reproducible behavior across environments.facade stage runs integration.rs::real_models_two_tier_search_yields_refined_through_the_public_api (potion fast tier + MiniLM quality tier from the registered cache; INITIAL then REFINED with the quality tier searched). Reproduce locally with FRANKENSEARCH_REQUIRE_SEMANTIC_E2E=1 cargo test -p frankensearch --features hybrid --test integration -- real_models.These are practical CPU-only reference numbers for a healthy local setup.
Treat them as orientation targets, not hard SLAs. Rows marked ledger are
the newest committed measurements in docs/PERF_LEDGER.md; rows marked
receipt come from the committed latency receipt
docs/evidence/perf/library-two-tier-latency-20260903-thinkstation1.json
(release profile, AMD Ryzen Threadripper PRO 5975WX / 64 threads, registered
potion-multilingual-128M + all-MiniLM-L6-v2, a 1,000-document / 659 KB
synthetic prose corpus, 50 timed queries after 5 warm-ups); rows marked
product receipt come from docs/evidence/perf/fsfs-latency-20260903-thinkstation1.json,
the same corpus as files driven through the release fsfs binary (index,
cold no-daemon searches, and one-request-per-connection queries against the
query daemon). Both are regenerated by QUALITY_GATE_STAGES=perf scripts/quality-gate.sh. Rows marked target have no committed measurement
and are design budgets:
| Operation | Typical Envelope | Basis |
|---|---|---|
Fast hash embedding (hash_embed_fnv, tokenize + FNV) | ~2 μs | ledger, 2026-07-04 |
| Fast model embedding (potion-128M, short query) | ~0.1 ms | receipt, 2026-09-03 (p50 0.10 ms, p95 0.17 ms) |
| Quality model embedding (MiniLM, short query) | ~5 ms | receipt, 2026-09-03 (p50 5.1 ms, p95 6.1 ms); at index time both tiers together cost ≈16 ms per document |
| Vector search (fast tier, top-10; 1K and 10K docs) | ~0.1 ms / ~0.3 ms | receipt, 2026-09-03 (p50 0.07 ms on 1K docs; p50 0.29 ms on 10K docs from library-two-tier-latency-10k-20260903-thinkstation1.json, where INITIAL is p50 1.0 ms, REFINED delivery p50 7.6 ms / p99 11.0 ms, and indexing 10,000 docs / 6.5 MB costs 190 s, 16.8 ms per document for both tiers, with 5.4 MB fast, 7.9 MB quality and 91 MB lexical artifacts) |
| Lexical search (Quill, 1K docs) | ~0.2 ms | receipt, 2026-09-03 (p50 0.23 ms, p95 0.38 ms) |
| RRF fusion (1,000 + 1,000 candidates) | ~23 μs | ledger, 2026-07-04 |
Phase 1 initial delivery (library TwoTierSearcher, hybrid Quill + potion, warm) | < 1 ms | receipt, 2026-09-03 (p50 0.40 ms, p95 0.58 ms, p99 2.4 ms) against the < 15 ms target; the fsfs in-process path measured 18–19 ms on a 65-file corpus on 2026-09-01 and is not yet receipted |
Phase 2 refined delivery (library TwoTierSearcher, INITIAL + REFINED) | ~6 ms | receipt, 2026-09-03 (p50 5.5 ms, p95 6.8 ms, p99 7.8 ms over the 40 of 50 queries that refined; the 10 short-keyword queries were answered by the lexical arm alone) against the ~150 ms target |
| Index build, both vector tiers + Quill (1,000 docs, 659 KB) | ~17 s | receipt, 2026-09-03 (17.2 s wall: 15.8 s embedding both tiers, 1.3 s Quill; artifacts 536 KB fast, 792 KB quality, 74 MB lexical; 1.29 GB RSS at the end) |
fsfs index (1,000 files / 660 KB, both vector tiers + Quill + catalog) | ~14 s | product receipt, 2026-09-03 (14.1 s wall, 26.9 MB on disk) |
Daemon-served fsfs search (warm query daemon, one request per connection, INITIAL + REFINED) | ~13 ms | product receipt, 2026-09-03 (p50 12.7 ms, p95 13.9 ms, p99 14.8 ms over 50 queries, 0 cache hits; :ready round trip 1.1 ms). Before the adaptive accept poll landed the same run measured p50 50 ms: the daemon slept 50 ms between empty accept polls |
Daemon-served fsfs search --rerank (cross-encoder over the refined head) | ~250–400 ms | product receipt, 2026-09-03 (p50 412 ms over 20 queries, all applied, with the host at a 15-minute load of 63; an earlier run of the same lane at load 9 measured p50 233 ms, p95 320 ms: the int8 cross-encoder is CPU-bound and shares the box) |
Watch mode: file written → ingested into both tiers (fsfs index --watch) | ~0.7 s | product receipt, 2026-09-03 (20 files written one at a time: event-to-applied p50 725 ms, p95 848 ms, max 886 ms = the 500 ms debounce plus p50 224 ms ingest; all 20 searchable from a fresh process after the watcher's graceful exit; host-pressure sampling pinned for the measurement, since a saturated host pauses the watcher by design) |
Cold process start (fsfs search without a running daemon, INITIAL + REFINED) | ~3.3–3.6 s | product receipt, 2026-09-03 (3.55 s median of 3 runs at load 63; 3.32–3.34 s at load 9): potion vocabulary load dominates |
What changes the envelope the most:
lexical, rerank, ann)The engine is intentionally designed to degrade gracefully:
| Condition | Behavior | What Caller Sees |
|---|---|---|
| Quality refinement timeout | Phase 2 aborts safely | SearchPhase::RefinementFailed { error: SearchTimeout, ... } |
| Quality embedder errors | Initial results preserved | SearchPhase::RefinementFailed { ... } |
fast_only=true | Skip quality phase by design | only Initial phase + skip_reason="fast_only" |
| No quality embedder configured | Skip quality phase | only Initial phase + skip_reason="no_quality_embedder" |
| Fast embedder fails but lexical succeeds | lexical-only fallback | valid Initial results from lexical path |
| Fast embedder fails and no lexical fallback | hard failure | search returns error |
| Lexical backend failure | semantic continues | search continues without lexical contribution |
Practical implication: phase-1 UX can still stay responsive even when higher-cost quality paths fail.
The shipping fsfs CLI applies a stricter mode contract than the reusable
fallback-capable library primitive: Full and FastOnly require an admitted
vector generation and matching real fast embedder before Initial is emitted.
Only explicitly selected LexicalOnly bypasses that readiness boundary. A
quality-tier failure remains progressive and emits actionable
RefinementFailed output while preserving the admitted Initial results.
Use this as a pragmatic hardening pass before rollout:
semantic, hybrid, full, etc.) and toolchain.fast_only in latency-critical paths, full two-tier where quality matters).FRANKENSEARCH_MODEL_DIR to a stable writable path with enough disk.FRANKENSEARCH_LOG) and capture phase timings.scripts/quality-gate.sh (or dsr quality --tool frankensearch), which covers
cargo fmt --checkcargo check --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningscargo test --workspace --lib --exclude frankensearch-quill-gauntlet plus the fsfs test binariesPublication runs on a real host with configured Cargo registry credentials.
GitHub Actions does not publish this repository. The two release lines are
versioned independently: v* tags identify fsfs binary releases and crates-v*
tags identify library bundles. Published versions are fsfs 1.10.0 and
frankensearch 0.5.0 (2026-09-08); the changelog lists every member and the index rebuild
requirement. The GitHub releases record published bundles and validation receipts.
Run scripts/check_crates_publish_contract.sh --mode gate --scope workspace
against the clean intended revision and the live registry. Build-verify every
package archive and complete the workspace publish dry run before uploading:
cargo publish --locked --workspace --dry-run \
--exclude optimize-params --exclude frankensearch-quill-gauntlet
cargo publish --locked --workspace \
--exclude optimize-params --exclude frankensearch-quill-gauntlet
Cargo publishes in dependency order and waits for registry availability. Verify the public archive checksums and source revisions, then run consumers without workspace paths or source replacement. An occupied version is not evidence that the intended source was published; verify it before reusing it. Crate-bundle GitHub releases must not replace the binary release as GitHub's latest release, because the installer and updater use that endpoint.
GitHub Actions is not used for this repository: every workflow under
.github/workflows/ is disabled (owner decision 2026-09-01). The gate that must pass
before any release lives in the repository and runs on a real host:
scripts/quality-gate.sh # fmt, check, clippy -D warnings, lib tests,
# Quill/Tantivy witness, fsfs tests, real-model e2e,
# executable quick-start gate
dsr quality --tool frankensearch # the same gate plus the packaging/installer
# contract checks, driven by dsr
Stages can be selected with QUALITY_GATE_STAGES=fmt,check,clippy,tests and
the registered model cache with QUALITY_GATE_MODEL_DIR. The end-to-end stage
fails closed when the two registered models are absent unless
QUALITY_GATE_ALLOW_MODEL_SKIP=1 is set. Release validation requires the real
models and does not use that skip. Releases use dsr or the documented real-host
build and publication commands with the same gates;
docs/fsfs-packaging-release-install-contract.md and
docs/crates-publishing-contract.md are the authoritative recipes.
The default quill stage runs the existing native engine witness and all tests
beside its independent oracle, plus the typed-query seed and replay regressions.
It inventories the complete all-feature gauntlet
and checks executed names and terminal counts against that inventory. The
90-second execution budget excludes compilation; missing Tantivy, ignored
required tests, an empty selection, timeout or incomplete output fails the stage.
This is bounded native correctness coverage, not full Quill conformance or a
performance verdict.
Before releasing changes to Quill, its analyzer/contracts or the gauntlet, also run the complete default and all-feature nonignored suites on the validation host:
QUALITY_GATE_STAGES=quill-full scripts/quality-gate.sh
# When changing the bounded driver or its selection, prove its failure paths:
QUALITY_GATE_STAGES=quill-probes scripts/quality-gate.sh
The full lane keeps the expensive evidence-assembly tests and their original assertions. Existing ignored tests retain the nightly, isolated-process or fixture-review prerequisites printed in the inventory; they are never counted as passes. E6's scaled-corpus and real-model quality campaigns and the QG performance ratchet remain separate requirements. A passing bounded lane does not override a failing full lane.
| Symptom | Likely Cause | What To Check |
|---|---|---|
| Initial results are slow | Candidate budget too high, cold cache, oversized corpus | candidate multipliers, model cache warm-up, corpus and index size |
| No refined phase arrives | fast_only enabled, no quality embedder, timeout | FRANKENSEARCH_FAST_ONLY, quality model availability, quality_timeout_ms |
RefinementFailed appears often | quality model unstable/slow, timeout budget too tight | model path/logs, timeout config, CPU contention |
| Results feel exact-match heavy | lexical overweight via candidate mix, weak semantic model tier | embedder stack selection, query class budgets, quality tier availability |
| Results feel semantically off | embedding model mismatch for corpus domain | switch model tier, tune blend weight, add rerank |
Negation queries (-term) behave unexpectedly | missing text provider in convenience path | use search_collect_with_text/search(...) with text callback |
| Output parsing issues in automation | wrong format for downstream parser | use --format jsonl or --format toon consistently |
| High memory usage | large index + quality/rerank/ANN enabled | feature set, f16 defaults, ANN config, corpus scope |
| Legacy or foreign lexical layout detected at open | pre-flip Tantivy directory or damaged FSLX segment | rebuild-on-detect re-derives the Quill index from canonical storage; foreign layouts are typed errors, fsfs doctor reports the lexical subsystem |
fsfs doctor reports semantic.quality_generation as a warning | quality model absent at index time, so the generation is fast-only | install and verify all-minilm-l6-v2, then re-index (Quick Start); searches stop at INITIAL until then |
sequenceDiagram
autonumber
participant U as User/Caller
participant S as TwoTierSearcher
participant C as Canonicalizer+Parser
participant F as Fast Embedder
participant L as Lexical Backend
participant V as Vector Index
participant R as RRF Fusion
participant Q as Quality Embedder
participant B as Blend Stage
participant X as Optional Reranker
U->>S: search(query, k)
S->>C: canonicalize + classify + parse
par Fast semantic path
S->>F: embed(query)
F-->>S: fast query vector
S->>V: search_fast(top_k * multiplier)
V-->>S: semantic candidates
and Lexical path (if enabled)
S->>L: BM25 search(top_k * multiplier)
L-->>S: lexical candidates
end
S->>R: fuse(lexical, semantic, rrf_k)
R-->>S: initial ranked results
S-->>U: SearchPhase::Initial
alt fast_only OR no quality embedder
S-->>U: done (initial only)
else quality refinement enabled
S->>Q: embed(query) with timeout
alt quality success
Q-->>S: quality query vector
S->>V: quality_scores_for_indices(...)
V-->>S: quality scores
S->>B: blend(quality_weight)
opt rerank feature enabled
S->>X: rerank(top_n)
X-->>S: reranked results
end
S-->>U: SearchPhase::Refined
else quality timeout/failure
S-->>U: SearchPhase::RefinementFailed
end
end
These are crate feature flags from frankensearch/Cargo.toml:
| Goal | Recommended Feature Set | Why |
|---|---|---|
| Fastest dev loop / CI smoke checks | default (hash) | zero model downloads, minimal deps |
| Better semantic quality without lexical | semantic | enables hash + model2vec + fastembed |
| Hybrid retrieval (semantic + BM25) | hybrid | adds the Quill BM25 lexical arm (the post-flip lexical default) on top of semantic recall |
| Persistent local indexing | persistent | hybrid + storage for durable metadata/queues |
| Durable + self-healing stack | durable | persistent + durability |
| Lexical-only library build | quill | pure-Rust Quill BM25 engine alone; since the flip, the lexical feature already selects Quill |
| Tantivy oracle/migration lane | lexical-tantivy | explicit Tantivy-backed lexical path; cass-compat aliases it for external CASS schema-v8 interop |
| Full capability surface | full | durable + rerank + ann + download + graph + api |
| Full stack + FTS5 storage backend | full-fts5 | full + fts5 for advanced local SQL FTS paths |
Quick examples:
# Hybrid local search library build
cargo build -p frankensearch --features hybrid
# Full stack with ANN + rerank + download
cargo build -p frankensearch --features full
# Full stack plus FTS5
cargo build -p frankensearch --features full-fts5
Best for interactive UX where fast first answer matters most.
export FRANKENSEARCH_FAST_ONLY=true
export FRANKENSEARCH_QUALITY_WEIGHT=0.7
export FRANKENSEARCH_RRF_K=60
export FRANKENSEARCH_QUALITY_TIMEOUT=250
Operational effect:
Initial quickly and skips/limits expensive refinement behaviorBest for offline analysis, report generation, or high-precision ranking.
export FRANKENSEARCH_PRESSURE_PROFILE=performance
export FRANKENSEARCH_FAST_ONLY=false
export FRANKENSEARCH_QUALITY_WEIGHT=0.85
export FRANKENSEARCH_RRF_K=40
export FRANKENSEARCH_QUALITY_TIMEOUT=1200
Operational effect:
Best for constrained laptops or multi-tenant CI hosts.
export FRANKENSEARCH_PRESSURE_PROFILE=strict
export FRANKENSEARCH_FAST_ONLY=true
export FRANKENSEARCH_QUALITY_TIMEOUT=200
export FRANKENSEARCH_HNSW_THRESHOLD=200000
Operational effect:
TwoTierConfig::optimized() TOML RecipeFor library consumers using TwoTierConfig::optimized(), place a file at
data/optimized_params.toml:
quality_weight = 0.8
rrf_k = 50.0
candidate_multiplier = 4
quality_timeout_ms = 800
fast_only = false
explain = false
hnsw_ef_search = 100
hnsw_ef_construction = 200
hnsw_m = 16
hnsw_threshold = 50000
mrl_search_dims = 0
mrl_rescore_top_k = 30
Use this when you want deterministic, checked-in tuning presets instead of host-specific env var drift.
| Area | Source File | Purpose |
|---|---|---|
| Facade crate | frankensearch/src/lib.rs | Top-level public API surface and re-exports |
| Index build workflow | frankensearch/src/index_builder.rs | High-level corpus-to-index pipeline |
| Progressive orchestration | crates/frankensearch-fusion/src/searcher.rs | Phase 1/2 flow, fallback paths, telemetry |
| Rank fusion | crates/frankensearch-fusion/src/rrf.rs | Reciprocal Rank Fusion implementation |
| Two-tier blending | crates/frankensearch-fusion/src/blend.rs | Fast/quality score normalization and blending |
| Two-tier index wrapper | crates/frankensearch-index/src/two_tier.rs | Fast/quality index alignment and lookup |
| Top-k vector search | crates/frankensearch-index/src/search.rs | Heap-based top-k selection and scoring paths |
| On-disk vector format | crates/frankensearch-index/src/quantization.rs | FSVI quantization; mmap via mapped_file.rs |
| Core config knobs | crates/frankensearch-core/src/config.rs | TwoTierConfig, defaults, env overrides |
| Core result types | crates/frankensearch-core/src/types.rs | SearchPhase, ScoredResult, hit structs |
| Query classification | crates/frankensearch-core/src/query_class.rs | Query-type detection and adaptive budgets |
| Eval/statistics | crates/frankensearch-core/src/metrics_eval.rs | nDCG/MRR/Recall/MAP + bootstrap helpers |
| Embedder auto-detect | crates/frankensearch-embed/src/auto_detect.rs | Fast/quality model discovery and stack setup |
| Storage ingest/queue | crates/frankensearch-storage/src/pipeline.rs | Storage-backed ingestion, queue processing, embedding sinks |
| Durability repair layer | crates/frankensearch-durability/src/fsvi_protector.rs | Protect/verify/repair flows for vector artifacts |
| FSFS CLI entry | crates/frankensearch-fsfs/src/lib.rs | Standalone CLI product wiring |
| FSFS runtime orchestration | crates/frankensearch-fsfs/src/runtime.rs | Command dispatch, search/index execution, stream emission |
| Shared TUI shell | crates/frankensearch-tui/src/shell.rs | Reusable shell loop/navigation/overlay plumbing |
| Ops telemetry storage | crates/frankensearch-ops/src/storage.rs | Control-plane telemetry persistence/materialization |
| Native lexical engine | crates/frankensearch-quill/src/index.rs | Quill index lifecycle: seal/flush, FSLX segments, MaxScore/block-max WAND clause thresholds |
| Quill query execution | crates/frankensearch-quill/src/argus.rs | BM25 query execution, sealed cursors, block-max pruning |
| Quill postings codec | crates/frankensearch-quill/src/quiver.rs | Posting blocks and per-block block-max entries (encode_with_block_max) |
| Quill differential gauntlet | crates/frankensearch-quill-gauntlet/src/runner.rs | Conformance/perf witness certifying Quill against the pinned Tantivy oracle |
| Term | Meaning |
|---|---|
| Two-tier search | Progressive retrieval: fast initial pass, quality refinement pass |
Phase 1 / Initial | First emitted result set, optimized for low latency |
Phase 2 / Refined | Optional improved ranking after quality embedding |
RefinementFailed | Graceful degradation event when Phase 2 errors/times out |
| RRF | Reciprocal Rank Fusion combining lexical + semantic rank lists |
| BM25 | Lexical ranking function used by the lexical backends (Quill and Tantivy) |
| FSLX | Quill's on-disk segment format: framed sections (TERMDICT, POSTINGS, POSITIONS, BLOCKMAX, DOCLEN, IDMAP, IDHASH) with reference validation |
| Delta segment | Quill lightweight segment carrying a generation's upserts/deletes against a base; visibility is delta-resolved at read time |
| Concat-merge | Quill segment merge mode that concatenates same-shape term streams without re-encoding (vs compacting tombstoned rows) |
| FSVI | On-disk vector index format used by frankensearch-index |
f16 quantization | Half-precision storage mode reducing memory footprint |
TwoTierIndex | Wrapper over fast and optional quality vector indexes |
TwoTierSearcher | Main orchestrator that runs retrieval/fusion/refinement |
TwoTierConfig | Primary tuning config for latency/quality behavior |
TwoTierMetrics | Per-search diagnostics (phase timings, candidate counts, skip reason) |
EmbedderStack | Paired fast + optional quality embedder selection object |
Cx | asupersync capability context passed into async operations |
| Knob | Where Set | Primary Impact | Increase Tends To | Decrease Tends To |
|---|---|---|---|---|
quality_weight | TwoTierConfig, FRANKENSEARCH_QUALITY_WEIGHT | Blend balance | Favor quality-tier ranking signal | Favor fast-tier ranking signal |
rrf_k | TwoTierConfig, FRANKENSEARCH_RRF_K | RRF rank sensitivity | Flatten rank differences across sources | Emphasize top ranks more strongly |
candidate_multiplier | TwoTierConfig | Candidate pool size | Improve recall headroom, increase latency/work | Reduce latency/work, may reduce recall |
quality_timeout_ms | TwoTierConfig, FRANKENSEARCH_QUALITY_TIMEOUT | Phase 2 budget | More chances to finish refinement | More RefinementFailed timeouts |
fast_only | TwoTierConfig, FRANKENSEARCH_FAST_ONLY | Phase behavior | Skip Phase 2 entirely (true) | Enable Phase 2 when quality embedder exists (false) |
hnsw_threshold | TwoTierConfig, FRANKENSEARCH_HNSW_THRESHOLD | ANN activation point | Use brute-force for more corpus sizes | Use ANN earlier for large corpora |
hnsw_ef_search | TwoTierConfig | ANN query beam width | Better ANN recall, more latency | Lower latency, potentially lower recall |
mrl_search_dims | TwoTierConfig | MRL scan dimensionality | Better first-pass quality, more compute | Faster first-pass, potentially less quality |
mrl_rescore_top_k | TwoTierConfig | Full-dim rescore scope | Better refined ordering, more compute | Less compute, potentially weaker refinement |
lexical feature | Cargo feature | Hybrid retrieval capability | Better exact-match precision and fallback paths | Semantic-only behavior |
rerank feature | Cargo feature | Cross-encoder rerank | Better top-result precision, higher latency | Lower latency, less fine-grained top ordering |
ann feature | Cargo feature | Approximate nearest-neighbor path | Better scale behavior at large corpus sizes | Simpler exact brute-force behavior |
grep/ripgrep/ctags are excellent for exact text and symbol lookup. frankensearch solves a different problem: semantic intent search over mixed corpora.
| Tool | Strong At | Limitation vs frankensearch |
|---|---|---|
grep | exact substrings | no semantic similarity |
ripgrep | very fast regex search | no embedding-based recall |
ctags | symbol navigation | not document-level semantic ranking |
frankensearch/fsfs | hybrid semantic + lexical, progressive refinement | higher complexity/runtime footprint |
Use both: keep rg for exact matches and use fsfs for intent-level retrieval.
Yes. Search/indexing runs on your machine. Network access is only needed for optional alternate-model downloads and update checks.
fsfs?Yes. Add frankensearch as a dependency and wire your own app/runtime.
Search still works using fast-tier and lexical paths; you get RefinementFailed or fast-only behavior.
Use jsonl for streaming automation and toon if your downstream stack expects TOON semantics.
No. Async/concurrency is built around asupersync and Cx.
Project policy is no direct external merges, but issues and PRs are still useful for bug reports and proposal clarity.
If you are working inside this repository as an internal/automation agent, run the gate before every push (it wraps the individual commands below):
scripts/quality-gate.sh
# or the pieces it runs:
cargo fmt --check
cargo check --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --lib --exclude frankensearch-quill-gauntlet
The real-model CLI lane also checks explicit native quality selection. Provision
all-MiniLM-L6-v2-native alongside the standard Potion and ONNX models before
running the gate. Its native test accepts MINILM_FIXTURE_DIR,
POTION_FIXTURE_DIR, and FASTEMBED_MINILM_FIXTURE_DIR for separate verified
fixture directories; absent or invalid fixtures fail the test.
To exercise an optimized executable through this lane, set FSFS_E2E_BINARY
to the absolute path of the release fsfs built from the source being checked.
The gate still builds and checks the workspace; its executable tests log the
selected binary explicitly.
Useful docs:
AGENTS.mddocs/e2e-artifact-contract.mddocs/dependency-semantics-policy.mddocs/planning/UPGRADE_LOG.md — upgrade history (relocated from repo root)docs/planning/BRIDGE_PLAN_2026-09-02.md — the gap-closing plan from the 2026-09 reality check (what is left between the README and the code, per goal)MIT License (with OpenAI/Anthropic Rider). See LICENSE.
4,051 commits
Rust
97.5%
Shell
2.2%
Two-tier hybrid search for Rust: sub-millisecond initial results via potion-128M, quality-refined rankings in 150ms via MiniLM-L6-v2. Combines lexical (Tantivy BM25) and semantic (vector cosine) search with Reciprocal Rank Fusion. Progressive iterator API, f16 SIMD vector index, feature-gated compilation.
84
stars
4,051
commits
Rust
primary language
Sep 11, 2026
updated
Two-tier hybrid local search for Rust and the fsfs standalone CLI: fast first-pass results, then quality refinement.
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/frankensearch/main/install.sh | bash -s -- --easy-mode
The installer verifies every downloaded archive before replacing a binary. A
missing or malformed checksum, unavailable SHA-256 tool, or mismatch is a hard
failure. If the platform has no full semantic release artifact, the ordinary
path builds the loader-capable default from source; it never silently installs
the model-free lite profile. Use --lite only when that reduced capability is
intentional. Intel macOS is the explicit exception: the pinned ONNX Runtime has
no supported x86_64 Darwin distribution, so ordinary semantic installation
fails with unsupported_platform and points to --lite instead of attempting
a source build that cannot succeed.
The Linux x86-64 full archive requires glibc 2.43 or newer; build from source on older glibc systems for full semantic search. The standard installer provisions and verifies the six registered production models separately; their download is roughly 1.64 GB. The two search tiers alone require roughly 621 MB.
When upgrading a Linux full installation from 1.9.0 or earlier, use the installer
above: those executables' fsfs update selects the MUSL lite archive and loses
semantic support. Older Apple Silicon lite installations should use
install.sh --lite to retain their profile. The 1.9.1 updater preserves both
the compiled ABI and the full/lite profile for subsequent updates.
Semantic indexes written by 1.7/1.8 require an explicit rebuild with the original
configuration: fsfs index /original/source --index-dir /existing/index.
Indexes written by 1.9.0 can be opened directly by 1.9.1.
The 1.10.0 release changes the embedder adapter version and therefore
requires that explicit rebuild for 1.9.x semantic indexes too. Keep the original
documents and configuration available; opening an old generation with the new
producer is refused. Model artifact checksums and native producer fingerprints
are unchanged, but the fast Potion tier has a new adapter fingerprint.
Installer goals:
fsfs currently builds from this workspace and uses the pinned nightly toolchain
(rust-toolchain.toml). Its default feature set compiles the Model2Vec and
FastEmbed loaders, but keeps the large model bytes outside the crate. Normal
workspace and crates.io builds therefore need no model artifacts at compile
time. The documented default path is loader-capable without any feature flags:
cargo +nightly install --path crates/frankensearch-fsfs
fsfs download-models potion-multilingual-128m
fsfs download-models all-minilm-l6-v2
fsfs download-models potion-multilingual-128m --verify
fsfs download-models all-minilm-l6-v2 --verify
fsfs version
fsfs status --format json
fsfs doctor --format json
For an uninstalled development binary, use
cargo build --locked -p frankensearch-fsfs --bin fsfs with the repository's
pinned toolchain, then run target/debug/fsfs with the same model setup.
The Linux DSR quality lane builds in a clean checkout, installs with
cargo install --locked --debug --path crates/frankensearch-fsfs --root <private-root>,
and verifies that the installed executable matches the Cargo build byte for byte.
It uses fresh HOME/XDG directories and checks repeat indexing, both vector
generations, ranked hybrid and vector-only search, and actual Initial/Refined
result records. Its executable quick-start
driver retains each run's artifacts and exercises model, result, authority,
timeout, child/listener and provenance failures. It needs Python 3.11+ and
strace for the warm-query network check.
Replay it with scripts/check_fsfs_executable_quickstart.sh --negative-probes --require-source.
--binary /absolute/path/to/fsfs checks a supplied executable as a smoke test;
its SHA-256 is recorded, while its source revision remains explicitly unknown.
The checker checkout revision cannot establish where that binary was built.
The two downloads use revision-pinned manifests and verify every file before
atomic promotion. Explicit --verify is fail-closed: a missing or corrupt
registered cache returns a typed nonzero error rather than a successful payload.
fsfs status reports manifest states (missing, incomplete, mismatch, or
verified); only fsfs doctor also opens each verified cache through the
compiled Model2Vec/FastEmbed loader. A hard doctor verdict exits nonzero with
one stable subsystem_error report with the failing checks in its context. If the cache is absent or offline mode
forbids acquisition, indexing fails with an actionable typed error; it never
substitutes hash control embeddings for semantic results. Use
--no-default-features alone produces the model-free lite binary. Add
--features semantic-native for the native semantic profile described below.
The default fsfs build can use pure-Rust F32 MiniLM for its quality tier:
fsfs download-models all-MiniLM-L6-v2-native
Set quality_model = "all-MiniLM-L6-v2-native" in the [indexing] section of
your fsfs configuration, then rebuild the index with that configuration.
Indexing, search, append and watch use the selected native producer;
fsfs status verifies its artifacts and fsfs doctor loads the native model.
The CLI supplies and drains its existing blocking pool. Library users of
FsfsRuntime attach their pool with with_native_blocking_pool.
Use an optimized (--release) executable for native inference. Cold model
loading can exceed the default 500 ms quality deadline even in a release build,
depending on the host, and return Initial results with a refinement-timeout
explanation.
In a daemon or TUI, successful initialization is retained even if that first
query times out, so later queries can reuse the loaded model. Each query still
has its own deadline and must match the index's producer identity.
Native and ONNX models have separate installation directories and producer identities. Changing this setting requires re-embedding the corpus. A missing or invalid native quality model leaves a new index fast-only; an existing native quality generation rejects incompatible models and preserves Initial results when refinement fails. The standard build's default quality model remains ONNX.
For an ONNX-free source build, use the explicit native profile:
cargo build --release -p frankensearch-fsfs --locked --no-default-features --features semantic-native
target/release/fsfs download-models
target/release/fsfs doctor
target/release/fsfs index ./documents
target/release/fsfs search "your query"
This profile includes Model2Vec, native quality inference and native reranking,
without the fastembed or ort dependency. It defaults to English native F32
MiniLM; bare download-models provisions the configured fast and quality models.
The optional reranker still needs fsfs download-models ms-marco-minilm-l-6-v2.
Existing ONNX quality generations need re-embedding with the native producer.
Enabling both semantic-native and semantic-loaders keeps the standard ONNX
default. Native-only builds update from source; fsfs update refuses to replace
them with a standard/lite release archive or an unclassified rollback backup.
fsfs update --check and rollback listing remain available. The profile cannot
initialize ONNX models; existing valid cached answers or an explicitly selected
daemon can still serve results from their attested producer.
The pure-Rust native feature also supports the 384-dimensional
paraphrase-multilingual-MiniLM-L12-v2 model for CJK and mixed-language
corpora. Acquisition and activation are both explicit; it is not part of the
default download set and is never selected merely because it is installed:
fsfs download-models paraphrase-multilingual-minilm-l12-v2
fsfs download-models paraphrase-multilingual-minilm-l12-v2 --verify
For fsfs, select it in your configuration and build a fresh index:
[indexing]
quality_model = "paraphrase-multilingual-minilm-l12-v2"
The default fsfs build uses the native multilingual loader for indexing, search, append, watch and doctor with this selection. Use an optimized executable and daemon-backed search: cold initialization can exceed the 500 ms quality budget, while the daemon retains the completed model for subsequent queries.
Library callers compiled with --features native load the verified model
directory with NativeEmbedder::load_multilingual(...) (or the corresponding
NativeEmbeddingModel variant). This producer has a distinct frozen identity
from all-MiniLM-L6-v2, despite sharing its 384-value output dimension. An
existing semantic index therefore cannot be reused or mixed: switch the model,
re-embed the complete corpus, and atomically publish that backfilled generation
before serving queries from it. Identity checks fail closed if vectors from the
two spaces are combined.
To build the full binary profile with Potion and MiniLM embedded, provision the revision-pinned inputs and select the feature explicitly:
scripts/rch-ensure-deps.sh --models-only
scripts/rch-ensure-deps.sh --models-only --check
cargo +nightly install --path crates/frankensearch-fsfs \
--no-default-features --features embedded-models
Provisioning validates every artifact's byte length and SHA-256. Cargo's build script performs no network access. The embedded profile changes distribution, not retrieval semantics: it uses the same loaders and registered model identities as the default source build.
The two semantic search tiers use roughly 621 MB of pinned model artifacts; the standard installer also provisions the other registered production models. First setup time depends on network speed. Later starts reuse the verification receipt while the exact manifest and file states remain unchanged.
# 1) Install
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/frankensearch/main/install.sh | bash -s -- --easy-mode
# 2) Acquire and independently verify both registered semantic tiers
fsfs download-models potion-multilingual-128m
fsfs download-models all-minilm-l6-v2
fsfs download-models potion-multilingual-128m --verify
fsfs download-models all-minilm-l6-v2 --verify
# 3) Index a directory
fsfs index ./my-project
# 4) Search
fsfs search "how does retry backoff work" --limit 5
Plain fsfs index <path> is a one-shot operation: it seals the generation and
exits. Use fsfs watch <path> or fsfs index <path> --watch only when you
explicitly want a long-running incremental watcher. Known limit (bd-z2nfa):
the watcher holds the vector generations' exclusive writer lock for its whole
life, so fsfs search from another process (and the query daemon) is refused
with fsvi.map_lock until the watcher stops; new and changed files are
ingested and become searchable the moment it exits, and the in-process TUI
cockpit can search a watched index.
Example output:
PHASE REFINED: 5 hit(s) for "how does retry backoff work"
vector generation: potion-multilingual-128M class=semantic
1. src/retry.rs score=0.033 [lexical+semantic]
Recover transient network failures with exponential <b>backoff</b>, bounded <b>retries</b>…
2. docs/failures.md score=0.016 [semantic]
…
5 results in 21ms
fsfs index writes two vector generations from the same documents:
vector/index.fsvi (the fast tier, potion) and vector/quality.fsvi (the
quality tier, all-MiniLM-L6-v2, its own embedding space). A search first
fuses the fast tier with Quill BM25 and emits INITIAL, then re-ranks the
head against the quality tier and emits REFINED; with --stream both
phases arrive as separate frames (query.stream.initial_ready, then
query.stream.refined_ready). If the quality model is not installed when
you index, the generation is built fast-only, fsfs doctor reports
semantic.quality_generation as a warning with the recorded reason, and
searches stop at INITIAL until you re-index with the model present.
fsfs status shows both generations (vector_generation_id,
quality_generation_id). Deletes, append-batch, compact, and watch mode
keep the two tiers in step. Both generations carry RaptorQ repair sidecars
(index.fsvi.fec, quality.fsvi.fec) like Quill's segments: fsfs doctor
verifies them as durability.vector_sidecars, fsfs compact restores a
generation whose bytes drifted before merging, and an in-place delete drops
the sidecar until the next compaction re-protects the file. The first search in a shell pays the model load
(about 3 s for potion, plus the MiniLM session for the quality stage); the
query daemon that fsfs search starts by default keeps later searches to tens
of milliseconds and exits on its own after ten idle minutes.
--stream also uses that warm daemon: Initial can arrive while quality
refinement is still running, followed by Refined or RefinementFailed and one terminal
event. JSONL and TOON announce complete cached replays with daemon_cache_hit.
Use --no-daemon for direct execution. A daemon policy or producer mismatch is
an error requiring a daemon restart or explicit direct execution; a request
that has delivered Initial is never silently retried.
The Unix socket transport admits at most 16 clients, bounds each phase frame to 4 MiB and each response to 8 MiB plus 64 KiB, and reports capacity failures explicitly. Slow readers do not hold the shared search state while socket writes drain. Queued and currently written response bytes are bounded by 16 × (8 MiB + 64 KiB); the shared query cache retains at most eight entries of at most 4 MiB of encoded phases each. Search admission to the shared index is serialized. Model/index memory and the active query's candidate set depend on the corpus and requested limit; these transport budgets are not a process memory cap.
frankensearch combines lexical and semantic retrieval with progressive delivery:
quill feature; Tantivy retained behind lexical-tantivy as the conformance oracle/migration lane)Result: responsive first answers plus better final ranking without blocking the UI.
potion-multilingual-128M + all-MiniLM-L6-v2) on first run. The embedded-models build profile additionally embeds those bytes for a zero-download first run (macOS/Windows full assets); the Linux full asset uses the loader + verified-download pathInitial, Refined, RefinementFailed)--stream) with machine-readable outputfsfs explain <rank|R-id|path> against the last search)table, json, jsonl, toon, csvfsfs search --rerank, search.rerank); ANN path via feature flagsfsfs index <dir> --format json emits one completion envelope after durable
publication; --format jsonl emits it as one line. The payload includes actual
file counts, vector model identities, and generation_complete. If embedding
retries are exhausted, semantic_deferred_files and a warning explain why the
published artifacts still need indexing resumed before semantic search.
# Basic search
fsfs search "structured concurrency" --limit 10
# Stream for agents/pipelines
fsfs search "query" --stream --format jsonl
# TOON mode
fsfs search "query" --stream --format toon
# Explain one result of the last search: by rank, R0-style id, or path
fsfs explain 1
# Re-score the refined head with the cross-encoder (one-time model install)
fsfs download-models ms-marco-minilm-l-6-v2
fsfs search "query" --rerank --format json
# Keep index fresh
fsfs index ~/projects --watch
# Health checks
fsfs doctor
Use this sequence to reproduce the core demo + benchmark evidence bundle:
# Progressive CLI behavior and machine-output surfaces
fsfs search "structured concurrency" --limit 10 --format table
fsfs search "structured concurrency" --limit 10 --stream --format jsonl
# Statistical benchmark regression lane (Tier-3 reproducibility anchor)
cargo test -p frankensearch-fsfs --test benchmark_baseline_matrix -- --nocapture
# Graceful-degradation/fault demonstration lane
cargo test -p frankensearch-fsfs --test pressure_simulation_harness \
scenario_long_run_soak_fault_injection_stays_within_drift_thresholds -- --nocapture
The benchmark lane validates deterministic artifact identity fields (dataset_sha256, matrix_sha256, samples_sha256) plus a fixed replay command contract.
Configuration precedence (highest first; fsfs config prints the resolved
source_precedence_applied for the current process):
fsfs.toml over user ~/.config/fsfs/config.toml)--fast-only, FRANKENSEARCH_FAST_ONLY=true, and [search] fast_only = true
disable quality work under the default performance pressure profile. The
highest-precedence value wins, including an explicit false. The strict
and degraded profiles keep quality disabled: a winning fast_only=false
returns a configuration error naming the profile. A hard pause disables quality
after source precedence and records the safety clamp in profile diagnostics.
Common environment variables:
| Variable | Purpose | Example |
|---|---|---|
FRANKENSEARCH_INDEX_DIR | Override index/data directory | ~/.local/share/frankensearch |
FRANKENSEARCH_MODEL_DIR | Override model location | ~/.cache/frankensearch/models |
FRANKENSEARCH_RERANK | Re-score the refined head with the cross-encoder (same as --rerank); needs fsfs download-models ms-marco-minilm-l-6-v2 once | 1 |
FRANKENSEARCH_FAST_ONLY | Disable quality work; follows CLI > environment > config precedence. false requires a profile that permits quality | true |
FRANKENSEARCH_QUALITY_WEIGHT | Blend quality vs fast tier | 0.7 |
FRANKENSEARCH_RRF_K | RRF constant | 60 |
FRANKENSEARCH_LOG | Tracing filter | info |
For full contracts and knobs:
docs/fsfs-config-contract.mddocs/fsfs-dual-mode-contract.mddocs/fsfs-packaging-release-install-contract.mddocs/fsfs-packaging-release-install-contract.md#host-migration-playbooks-priority-projectsdocs/fsfs-packaging-release-install-contract.md#staged-rollout-and-deterministic-fallback-protocoldocs/fsfs-packaging-release-install-contract.md#upgrade-and-migration-compatibility-verification-strategydocs/architecture/Pipeline summary:
Query
-> canonicalize
-> classify
-> fast embed + Quill BM25 (default lexical)
-> RRF fusion (initial)
-> quality embed (top candidates)
-> blend (and optional rerank)
-> refined results
Model path used in the default quality lane:
fsfs search --rerank / search.rerank; library rerank feature): pure-Rust frankentorch cross-encoder (native, ms-marco-MiniLM-L-6-v2 or jina-reranker) re-scores the refined head once fsfs download-models ms-marco-minilm-l-6-v2 has installed the weights; without a verified model the stage is skipped with a typed reason (query.stage.rerank.disabled.unavailable). A FastEmbed/ONNX alternative sits behind fastembed-rerankerfrankensearch is split into focused crates so each concern can evolve independently:
| Crate | Responsibility |
|---|---|
frankensearch-core | Shared types/traits/errors/config, query canonicalization/classification, metrics/eval helpers |
frankensearch-embed | Embedding backends and fallback stack (hash, model2vec, fastembed) |
frankensearch-index | FSVI vector storage, SIMD dot products, top-k search, optional native HNSW ANN |
frankensearch-lexical | Tantivy schema/index/search for BM25 lexical retrieval (conformance oracle + cass-compat interop lane) |
frankensearch-quill | Native pure-Rust BM25 lexical engine (FSLX segments, delta-visible indexing) |
frankensearch-quill-gauntlet | Differential conformance/perf gauntlet certifying Quill against the pinned Tantivy oracle |
frankensearch-fusion | RRF fusion, two-tier orchestration, blending, optional rerank integration |
frankensearch-rerank | Cross-encoder reranking (pure-Rust frankentorch native backend + optional FastEmbed) |
frankensearch-storage | FrankenSQLite metadata persistence, dedup/content-hash tracking, embedding queue |
frankensearch-durability | Repair/protection primitives for index artifacts and segment health |
crates/frankensearch-fsfs | Standalone CLI product around the library stack |
crates/frankensearch-tui | Shared TUI shell/input/theme/replay framework used by fsfs/ops |
crates/frankensearch-ops | Fleet observability/control-plane TUI and telemetry materialization. Experimental (decision 2026-09-02, bd-p6k61): no shipped telemetry source yet; its only producer is its own simulator, nothing depends on it, and no release lane builds it |
This separation gives you two options:
fsfs binary with progressive CLI/TUI workflowsAt execution time, the system follows this shape:
identifier, short keyword, natural language) for adaptive budgets.Initial results quickly.fast_only)Refined or RefinementFailed (graceful degradation path).RRF is rank-based and model-agnostic. It does not require score calibration across systems:
RRF(doc) = Σ_sources 1 / (K + rank(doc, source) + 1)
Default K is 60 (configurable with FRANKENSEARCH_RRF_K / rrf_k).
Why RRF:
During refinement, fast and quality semantic scores are normalized then blended:
blended_score = alpha * quality_score + (1 - alpha) * fast_score
alpha is controlled by quality_weight (default target 0.7).
When ties happen, ranking remains deterministic through stable tie-break logic
(including lexical comparison and doc_id ordering), which helps replayability
and makes diff-based evaluation much cleaner.
Vector data is stored in FSVI files with memory-mapped access:
f16 (good memory/quality tradeoff)f32 paths where neededWhy this matters:
f32 storage in common workloadsThe brute-force search path is optimized around:
This gives strong baseline behavior while ANN remains optional for larger corpora.
The async model uses asupersync and capability context (Cx), not Tokio.
Important implications:
This is useful if you need to embed search inside existing non-Tokio runtimes or strictly controlled execution environments.
Core engineering principles in this project:
Progressive delivery first
Fast initial answer, then quality refinement, instead of blocking on best possible ranking.
Graceful degradation
If quality tier/reranker/model loading fails, search still returns useful initial results.
Determinism and reproducibility
Stable ordering and artifact-driven evaluation support regression tracking and CI gates.
Explicit tradeoffs over hidden magic
Key knobs (rrf_k, blend weight, fast-only mode, candidate multipliers) are visible and tunable.
Practical hybrid retrieval
BM25 and embeddings are treated as complementary signals, not mutually exclusive choices.
frankensearch is especially strong when you need:
jsonl, toon) and explainability hooksIn short: it closes the gap between exact text lookup and semantic retrieval without forcing you into remote services or heavyweight distributed systems.
Common tuning patterns:
Need lower tail latency:
FRANKENSEARCH_FAST_ONLY=true or --fast-only; both work with the default performance profileNeed higher relevance quality:
Need memory efficiency:
Need operational clarity:
The repository includes explicit quality harnesses and statistical checks:
nDCG@K, MRR, Recall@K, plus bootstrap confidence intervalsThis keeps tuning decisions evidence-driven rather than anecdotal.
Being explicit about scope helps set expectations:
rg and has model/runtime overhead.Use rg/grep for strict exact matching and frankensearch when ranking by
intent and contextual relevance matters.
If you want to embed frankensearch directly in your Rust app, this is the
minimum end-to-end flow:
use std::path::Path;
use std::sync::Arc;
use frankensearch::{
EmbedderStack, IndexBuilder, TwoTierConfig, TwoTierIndex, TwoTierSearcher,
};
asupersync::test_utils::run_test_with_cx(|cx| async move {
// 1) Resolve a verified semantic embedder. HashEmbedder is a control
// double, not a semantic engine; auto_detect without models still
// returns that control stack.
let stack = EmbedderStack::auto_detect_semantic_with(Some(Path::new("./models")))
.expect("production search needs a verified semantic embedder");
// 2) Build an index from documents
IndexBuilder::new("./my_index")
.with_embedder_stack(stack)
.add_document("doc-1", "Rust ownership and borrowing")
.add_document("doc-2", "Structured concurrency with asupersync")
.build(&cx)
.await
.expect("index build should succeed");
// 3) Open and search with the same semantic family
let stack = EmbedderStack::auto_detect_semantic_with(Some(Path::new("./models")))
.expect("search must use the same semantic family the index was built with");
let index = Arc::new(
TwoTierIndex::open(Path::new("./my_index"), TwoTierConfig::default()).unwrap(),
);
let mut searcher = TwoTierSearcher::new(index, stack.fast_arc(), TwoTierConfig::default());
if let Some(quality) = stack.quality_arc() {
searcher = searcher.with_quality_embedder(quality);
}
let (results, metrics) = searcher
.search_collect(&cx, "ownership rules", 10)
.await
.expect("search should succeed");
println!("results={} phase1_ms={:.2}", results.len(), metrics.phase1_total_ms);
#[cfg(feature = "quill")]
{
// `open_hybrid` opens every arm of the index directory and attaches
// the active lexical reader (blue-green roots resolve to their
// active engine dir; foreign or damaged layouts are typed errors).
use frankensearch::LexicalRead;
let parts = frankensearch::open_hybrid(
&cx,
"./my_index",
frankensearch::TwoTierConfig::default(),
)
.await
.expect("open hybrid index");
let lexical = parts.lexical.expect("lexical arm attached");
let lexical_hits = lexical
.search(&cx, "ownership", 10)
.await
.expect("search lexical arm");
assert!(!lexical_hits.is_empty());
}
});
Notes:
EmbedderStack::auto_detect_semantic_with (or explicit model2vec + fastembed). auto_detect / auto_detect_with still return a hash-only stack when no model is present; that is a control policy, not a working semantic engine. IndexBuilder without an explicit stack already refuses that hash-only fallback.quill for the native bulk-built lexical index; use lexical-tantivy only for the explicit Tantivy oracle/comparator lane during migration.cass-compat only when interoperating with the external CASS tool's
schema-v8 Tantivy index at <base>/index/v8/. This foreign-format adapter is
not a Quill fallback and remains outside default builds.search_collect_with_text or full search(...) when you need negation filtering (-term) and rerank text access.TwoTierConfig explicit in code for reproducible behavior across environments.facade stage runs integration.rs::real_models_two_tier_search_yields_refined_through_the_public_api (potion fast tier + MiniLM quality tier from the registered cache; INITIAL then REFINED with the quality tier searched). Reproduce locally with FRANKENSEARCH_REQUIRE_SEMANTIC_E2E=1 cargo test -p frankensearch --features hybrid --test integration -- real_models.These are practical CPU-only reference numbers for a healthy local setup.
Treat them as orientation targets, not hard SLAs. Rows marked ledger are
the newest committed measurements in docs/PERF_LEDGER.md; rows marked
receipt come from the committed latency receipt
docs/evidence/perf/library-two-tier-latency-20260903-thinkstation1.json
(release profile, AMD Ryzen Threadripper PRO 5975WX / 64 threads, registered
potion-multilingual-128M + all-MiniLM-L6-v2, a 1,000-document / 659 KB
synthetic prose corpus, 50 timed queries after 5 warm-ups); rows marked
product receipt come from docs/evidence/perf/fsfs-latency-20260903-thinkstation1.json,
the same corpus as files driven through the release fsfs binary (index,
cold no-daemon searches, and one-request-per-connection queries against the
query daemon). Both are regenerated by QUALITY_GATE_STAGES=perf scripts/quality-gate.sh. Rows marked target have no committed measurement
and are design budgets:
| Operation | Typical Envelope | Basis |
|---|---|---|
Fast hash embedding (hash_embed_fnv, tokenize + FNV) | ~2 μs | ledger, 2026-07-04 |
| Fast model embedding (potion-128M, short query) | ~0.1 ms | receipt, 2026-09-03 (p50 0.10 ms, p95 0.17 ms) |
| Quality model embedding (MiniLM, short query) | ~5 ms | receipt, 2026-09-03 (p50 5.1 ms, p95 6.1 ms); at index time both tiers together cost ≈16 ms per document |
| Vector search (fast tier, top-10; 1K and 10K docs) | ~0.1 ms / ~0.3 ms | receipt, 2026-09-03 (p50 0.07 ms on 1K docs; p50 0.29 ms on 10K docs from library-two-tier-latency-10k-20260903-thinkstation1.json, where INITIAL is p50 1.0 ms, REFINED delivery p50 7.6 ms / p99 11.0 ms, and indexing 10,000 docs / 6.5 MB costs 190 s, 16.8 ms per document for both tiers, with 5.4 MB fast, 7.9 MB quality and 91 MB lexical artifacts) |
| Lexical search (Quill, 1K docs) | ~0.2 ms | receipt, 2026-09-03 (p50 0.23 ms, p95 0.38 ms) |
| RRF fusion (1,000 + 1,000 candidates) | ~23 μs | ledger, 2026-07-04 |
Phase 1 initial delivery (library TwoTierSearcher, hybrid Quill + potion, warm) | < 1 ms | receipt, 2026-09-03 (p50 0.40 ms, p95 0.58 ms, p99 2.4 ms) against the < 15 ms target; the fsfs in-process path measured 18–19 ms on a 65-file corpus on 2026-09-01 and is not yet receipted |
Phase 2 refined delivery (library TwoTierSearcher, INITIAL + REFINED) | ~6 ms | receipt, 2026-09-03 (p50 5.5 ms, p95 6.8 ms, p99 7.8 ms over the 40 of 50 queries that refined; the 10 short-keyword queries were answered by the lexical arm alone) against the ~150 ms target |
| Index build, both vector tiers + Quill (1,000 docs, 659 KB) | ~17 s | receipt, 2026-09-03 (17.2 s wall: 15.8 s embedding both tiers, 1.3 s Quill; artifacts 536 KB fast, 792 KB quality, 74 MB lexical; 1.29 GB RSS at the end) |
fsfs index (1,000 files / 660 KB, both vector tiers + Quill + catalog) | ~14 s | product receipt, 2026-09-03 (14.1 s wall, 26.9 MB on disk) |
Daemon-served fsfs search (warm query daemon, one request per connection, INITIAL + REFINED) | ~13 ms | product receipt, 2026-09-03 (p50 12.7 ms, p95 13.9 ms, p99 14.8 ms over 50 queries, 0 cache hits; :ready round trip 1.1 ms). Before the adaptive accept poll landed the same run measured p50 50 ms: the daemon slept 50 ms between empty accept polls |
Daemon-served fsfs search --rerank (cross-encoder over the refined head) | ~250–400 ms | product receipt, 2026-09-03 (p50 412 ms over 20 queries, all applied, with the host at a 15-minute load of 63; an earlier run of the same lane at load 9 measured p50 233 ms, p95 320 ms: the int8 cross-encoder is CPU-bound and shares the box) |
Watch mode: file written → ingested into both tiers (fsfs index --watch) | ~0.7 s | product receipt, 2026-09-03 (20 files written one at a time: event-to-applied p50 725 ms, p95 848 ms, max 886 ms = the 500 ms debounce plus p50 224 ms ingest; all 20 searchable from a fresh process after the watcher's graceful exit; host-pressure sampling pinned for the measurement, since a saturated host pauses the watcher by design) |
Cold process start (fsfs search without a running daemon, INITIAL + REFINED) | ~3.3–3.6 s | product receipt, 2026-09-03 (3.55 s median of 3 runs at load 63; 3.32–3.34 s at load 9): potion vocabulary load dominates |
What changes the envelope the most:
lexical, rerank, ann)The engine is intentionally designed to degrade gracefully:
| Condition | Behavior | What Caller Sees |
|---|---|---|
| Quality refinement timeout | Phase 2 aborts safely | SearchPhase::RefinementFailed { error: SearchTimeout, ... } |
| Quality embedder errors | Initial results preserved | SearchPhase::RefinementFailed { ... } |
fast_only=true | Skip quality phase by design | only Initial phase + skip_reason="fast_only" |
| No quality embedder configured | Skip quality phase | only Initial phase + skip_reason="no_quality_embedder" |
| Fast embedder fails but lexical succeeds | lexical-only fallback | valid Initial results from lexical path |
| Fast embedder fails and no lexical fallback | hard failure | search returns error |
| Lexical backend failure | semantic continues | search continues without lexical contribution |
Practical implication: phase-1 UX can still stay responsive even when higher-cost quality paths fail.
The shipping fsfs CLI applies a stricter mode contract than the reusable
fallback-capable library primitive: Full and FastOnly require an admitted
vector generation and matching real fast embedder before Initial is emitted.
Only explicitly selected LexicalOnly bypasses that readiness boundary. A
quality-tier failure remains progressive and emits actionable
RefinementFailed output while preserving the admitted Initial results.
Use this as a pragmatic hardening pass before rollout:
semantic, hybrid, full, etc.) and toolchain.fast_only in latency-critical paths, full two-tier where quality matters).FRANKENSEARCH_MODEL_DIR to a stable writable path with enough disk.FRANKENSEARCH_LOG) and capture phase timings.scripts/quality-gate.sh (or dsr quality --tool frankensearch), which covers
cargo fmt --checkcargo check --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningscargo test --workspace --lib --exclude frankensearch-quill-gauntlet plus the fsfs test binariesPublication runs on a real host with configured Cargo registry credentials.
GitHub Actions does not publish this repository. The two release lines are
versioned independently: v* tags identify fsfs binary releases and crates-v*
tags identify library bundles. Published versions are fsfs 1.10.0 and
frankensearch 0.5.0 (2026-09-08); the changelog lists every member and the index rebuild
requirement. The GitHub releases record published bundles and validation receipts.
Run scripts/check_crates_publish_contract.sh --mode gate --scope workspace
against the clean intended revision and the live registry. Build-verify every
package archive and complete the workspace publish dry run before uploading:
cargo publish --locked --workspace --dry-run \
--exclude optimize-params --exclude frankensearch-quill-gauntlet
cargo publish --locked --workspace \
--exclude optimize-params --exclude frankensearch-quill-gauntlet
Cargo publishes in dependency order and waits for registry availability. Verify the public archive checksums and source revisions, then run consumers without workspace paths or source replacement. An occupied version is not evidence that the intended source was published; verify it before reusing it. Crate-bundle GitHub releases must not replace the binary release as GitHub's latest release, because the installer and updater use that endpoint.
GitHub Actions is not used for this repository: every workflow under
.github/workflows/ is disabled (owner decision 2026-09-01). The gate that must pass
before any release lives in the repository and runs on a real host:
scripts/quality-gate.sh # fmt, check, clippy -D warnings, lib tests,
# Quill/Tantivy witness, fsfs tests, real-model e2e,
# executable quick-start gate
dsr quality --tool frankensearch # the same gate plus the packaging/installer
# contract checks, driven by dsr
Stages can be selected with QUALITY_GATE_STAGES=fmt,check,clippy,tests and
the registered model cache with QUALITY_GATE_MODEL_DIR. The end-to-end stage
fails closed when the two registered models are absent unless
QUALITY_GATE_ALLOW_MODEL_SKIP=1 is set. Release validation requires the real
models and does not use that skip. Releases use dsr or the documented real-host
build and publication commands with the same gates;
docs/fsfs-packaging-release-install-contract.md and
docs/crates-publishing-contract.md are the authoritative recipes.
The default quill stage runs the existing native engine witness and all tests
beside its independent oracle, plus the typed-query seed and replay regressions.
It inventories the complete all-feature gauntlet
and checks executed names and terminal counts against that inventory. The
90-second execution budget excludes compilation; missing Tantivy, ignored
required tests, an empty selection, timeout or incomplete output fails the stage.
This is bounded native correctness coverage, not full Quill conformance or a
performance verdict.
Before releasing changes to Quill, its analyzer/contracts or the gauntlet, also run the complete default and all-feature nonignored suites on the validation host:
QUALITY_GATE_STAGES=quill-full scripts/quality-gate.sh
# When changing the bounded driver or its selection, prove its failure paths:
QUALITY_GATE_STAGES=quill-probes scripts/quality-gate.sh
The full lane keeps the expensive evidence-assembly tests and their original assertions. Existing ignored tests retain the nightly, isolated-process or fixture-review prerequisites printed in the inventory; they are never counted as passes. E6's scaled-corpus and real-model quality campaigns and the QG performance ratchet remain separate requirements. A passing bounded lane does not override a failing full lane.
| Symptom | Likely Cause | What To Check |
|---|---|---|
| Initial results are slow | Candidate budget too high, cold cache, oversized corpus | candidate multipliers, model cache warm-up, corpus and index size |
| No refined phase arrives | fast_only enabled, no quality embedder, timeout | FRANKENSEARCH_FAST_ONLY, quality model availability, quality_timeout_ms |
RefinementFailed appears often | quality model unstable/slow, timeout budget too tight | model path/logs, timeout config, CPU contention |
| Results feel exact-match heavy | lexical overweight via candidate mix, weak semantic model tier | embedder stack selection, query class budgets, quality tier availability |
| Results feel semantically off | embedding model mismatch for corpus domain | switch model tier, tune blend weight, add rerank |
Negation queries (-term) behave unexpectedly | missing text provider in convenience path | use search_collect_with_text/search(...) with text callback |
| Output parsing issues in automation | wrong format for downstream parser | use --format jsonl or --format toon consistently |
| High memory usage | large index + quality/rerank/ANN enabled | feature set, f16 defaults, ANN config, corpus scope |
| Legacy or foreign lexical layout detected at open | pre-flip Tantivy directory or damaged FSLX segment | rebuild-on-detect re-derives the Quill index from canonical storage; foreign layouts are typed errors, fsfs doctor reports the lexical subsystem |
fsfs doctor reports semantic.quality_generation as a warning | quality model absent at index time, so the generation is fast-only | install and verify all-minilm-l6-v2, then re-index (Quick Start); searches stop at INITIAL until then |
sequenceDiagram
autonumber
participant U as User/Caller
participant S as TwoTierSearcher
participant C as Canonicalizer+Parser
participant F as Fast Embedder
participant L as Lexical Backend
participant V as Vector Index
participant R as RRF Fusion
participant Q as Quality Embedder
participant B as Blend Stage
participant X as Optional Reranker
U->>S: search(query, k)
S->>C: canonicalize + classify + parse
par Fast semantic path
S->>F: embed(query)
F-->>S: fast query vector
S->>V: search_fast(top_k * multiplier)
V-->>S: semantic candidates
and Lexical path (if enabled)
S->>L: BM25 search(top_k * multiplier)
L-->>S: lexical candidates
end
S->>R: fuse(lexical, semantic, rrf_k)
R-->>S: initial ranked results
S-->>U: SearchPhase::Initial
alt fast_only OR no quality embedder
S-->>U: done (initial only)
else quality refinement enabled
S->>Q: embed(query) with timeout
alt quality success
Q-->>S: quality query vector
S->>V: quality_scores_for_indices(...)
V-->>S: quality scores
S->>B: blend(quality_weight)
opt rerank feature enabled
S->>X: rerank(top_n)
X-->>S: reranked results
end
S-->>U: SearchPhase::Refined
else quality timeout/failure
S-->>U: SearchPhase::RefinementFailed
end
end
These are crate feature flags from frankensearch/Cargo.toml:
| Goal | Recommended Feature Set | Why |
|---|---|---|
| Fastest dev loop / CI smoke checks | default (hash) | zero model downloads, minimal deps |
| Better semantic quality without lexical | semantic | enables hash + model2vec + fastembed |
| Hybrid retrieval (semantic + BM25) | hybrid | adds the Quill BM25 lexical arm (the post-flip lexical default) on top of semantic recall |
| Persistent local indexing | persistent | hybrid + storage for durable metadata/queues |
| Durable + self-healing stack | durable | persistent + durability |
| Lexical-only library build | quill | pure-Rust Quill BM25 engine alone; since the flip, the lexical feature already selects Quill |
| Tantivy oracle/migration lane | lexical-tantivy | explicit Tantivy-backed lexical path; cass-compat aliases it for external CASS schema-v8 interop |
| Full capability surface | full | durable + rerank + ann + download + graph + api |
| Full stack + FTS5 storage backend | full-fts5 | full + fts5 for advanced local SQL FTS paths |
Quick examples:
# Hybrid local search library build
cargo build -p frankensearch --features hybrid
# Full stack with ANN + rerank + download
cargo build -p frankensearch --features full
# Full stack plus FTS5
cargo build -p frankensearch --features full-fts5
Best for interactive UX where fast first answer matters most.
export FRANKENSEARCH_FAST_ONLY=true
export FRANKENSEARCH_QUALITY_WEIGHT=0.7
export FRANKENSEARCH_RRF_K=60
export FRANKENSEARCH_QUALITY_TIMEOUT=250
Operational effect:
Initial quickly and skips/limits expensive refinement behaviorBest for offline analysis, report generation, or high-precision ranking.
export FRANKENSEARCH_PRESSURE_PROFILE=performance
export FRANKENSEARCH_FAST_ONLY=false
export FRANKENSEARCH_QUALITY_WEIGHT=0.85
export FRANKENSEARCH_RRF_K=40
export FRANKENSEARCH_QUALITY_TIMEOUT=1200
Operational effect:
Best for constrained laptops or multi-tenant CI hosts.
export FRANKENSEARCH_PRESSURE_PROFILE=strict
export FRANKENSEARCH_FAST_ONLY=true
export FRANKENSEARCH_QUALITY_TIMEOUT=200
export FRANKENSEARCH_HNSW_THRESHOLD=200000
Operational effect:
TwoTierConfig::optimized() TOML RecipeFor library consumers using TwoTierConfig::optimized(), place a file at
data/optimized_params.toml:
quality_weight = 0.8
rrf_k = 50.0
candidate_multiplier = 4
quality_timeout_ms = 800
fast_only = false
explain = false
hnsw_ef_search = 100
hnsw_ef_construction = 200
hnsw_m = 16
hnsw_threshold = 50000
mrl_search_dims = 0
mrl_rescore_top_k = 30
Use this when you want deterministic, checked-in tuning presets instead of host-specific env var drift.
| Area | Source File | Purpose |
|---|---|---|
| Facade crate | frankensearch/src/lib.rs | Top-level public API surface and re-exports |
| Index build workflow | frankensearch/src/index_builder.rs | High-level corpus-to-index pipeline |
| Progressive orchestration | crates/frankensearch-fusion/src/searcher.rs | Phase 1/2 flow, fallback paths, telemetry |
| Rank fusion | crates/frankensearch-fusion/src/rrf.rs | Reciprocal Rank Fusion implementation |
| Two-tier blending | crates/frankensearch-fusion/src/blend.rs | Fast/quality score normalization and blending |
| Two-tier index wrapper | crates/frankensearch-index/src/two_tier.rs | Fast/quality index alignment and lookup |
| Top-k vector search | crates/frankensearch-index/src/search.rs | Heap-based top-k selection and scoring paths |
| On-disk vector format | crates/frankensearch-index/src/quantization.rs | FSVI quantization; mmap via mapped_file.rs |
| Core config knobs | crates/frankensearch-core/src/config.rs | TwoTierConfig, defaults, env overrides |
| Core result types | crates/frankensearch-core/src/types.rs | SearchPhase, ScoredResult, hit structs |
| Query classification | crates/frankensearch-core/src/query_class.rs | Query-type detection and adaptive budgets |
| Eval/statistics | crates/frankensearch-core/src/metrics_eval.rs | nDCG/MRR/Recall/MAP + bootstrap helpers |
| Embedder auto-detect | crates/frankensearch-embed/src/auto_detect.rs | Fast/quality model discovery and stack setup |
| Storage ingest/queue | crates/frankensearch-storage/src/pipeline.rs | Storage-backed ingestion, queue processing, embedding sinks |
| Durability repair layer | crates/frankensearch-durability/src/fsvi_protector.rs | Protect/verify/repair flows for vector artifacts |
| FSFS CLI entry | crates/frankensearch-fsfs/src/lib.rs | Standalone CLI product wiring |
| FSFS runtime orchestration | crates/frankensearch-fsfs/src/runtime.rs | Command dispatch, search/index execution, stream emission |
| Shared TUI shell | crates/frankensearch-tui/src/shell.rs | Reusable shell loop/navigation/overlay plumbing |
| Ops telemetry storage | crates/frankensearch-ops/src/storage.rs | Control-plane telemetry persistence/materialization |
| Native lexical engine | crates/frankensearch-quill/src/index.rs | Quill index lifecycle: seal/flush, FSLX segments, MaxScore/block-max WAND clause thresholds |
| Quill query execution | crates/frankensearch-quill/src/argus.rs | BM25 query execution, sealed cursors, block-max pruning |
| Quill postings codec | crates/frankensearch-quill/src/quiver.rs | Posting blocks and per-block block-max entries (encode_with_block_max) |
| Quill differential gauntlet | crates/frankensearch-quill-gauntlet/src/runner.rs | Conformance/perf witness certifying Quill against the pinned Tantivy oracle |
| Term | Meaning |
|---|---|
| Two-tier search | Progressive retrieval: fast initial pass, quality refinement pass |
Phase 1 / Initial | First emitted result set, optimized for low latency |
Phase 2 / Refined | Optional improved ranking after quality embedding |
RefinementFailed | Graceful degradation event when Phase 2 errors/times out |
| RRF | Reciprocal Rank Fusion combining lexical + semantic rank lists |
| BM25 | Lexical ranking function used by the lexical backends (Quill and Tantivy) |
| FSLX | Quill's on-disk segment format: framed sections (TERMDICT, POSTINGS, POSITIONS, BLOCKMAX, DOCLEN, IDMAP, IDHASH) with reference validation |
| Delta segment | Quill lightweight segment carrying a generation's upserts/deletes against a base; visibility is delta-resolved at read time |
| Concat-merge | Quill segment merge mode that concatenates same-shape term streams without re-encoding (vs compacting tombstoned rows) |
| FSVI | On-disk vector index format used by frankensearch-index |
f16 quantization | Half-precision storage mode reducing memory footprint |
TwoTierIndex | Wrapper over fast and optional quality vector indexes |
TwoTierSearcher | Main orchestrator that runs retrieval/fusion/refinement |
TwoTierConfig | Primary tuning config for latency/quality behavior |
TwoTierMetrics | Per-search diagnostics (phase timings, candidate counts, skip reason) |
EmbedderStack | Paired fast + optional quality embedder selection object |
Cx | asupersync capability context passed into async operations |
| Knob | Where Set | Primary Impact | Increase Tends To | Decrease Tends To |
|---|---|---|---|---|
quality_weight | TwoTierConfig, FRANKENSEARCH_QUALITY_WEIGHT | Blend balance | Favor quality-tier ranking signal | Favor fast-tier ranking signal |
rrf_k | TwoTierConfig, FRANKENSEARCH_RRF_K | RRF rank sensitivity | Flatten rank differences across sources | Emphasize top ranks more strongly |
candidate_multiplier | TwoTierConfig | Candidate pool size | Improve recall headroom, increase latency/work | Reduce latency/work, may reduce recall |
quality_timeout_ms | TwoTierConfig, FRANKENSEARCH_QUALITY_TIMEOUT | Phase 2 budget | More chances to finish refinement | More RefinementFailed timeouts |
fast_only | TwoTierConfig, FRANKENSEARCH_FAST_ONLY | Phase behavior | Skip Phase 2 entirely (true) | Enable Phase 2 when quality embedder exists (false) |
hnsw_threshold | TwoTierConfig, FRANKENSEARCH_HNSW_THRESHOLD | ANN activation point | Use brute-force for more corpus sizes | Use ANN earlier for large corpora |
hnsw_ef_search | TwoTierConfig | ANN query beam width | Better ANN recall, more latency | Lower latency, potentially lower recall |
mrl_search_dims | TwoTierConfig | MRL scan dimensionality | Better first-pass quality, more compute | Faster first-pass, potentially less quality |
mrl_rescore_top_k | TwoTierConfig | Full-dim rescore scope | Better refined ordering, more compute | Less compute, potentially weaker refinement |
lexical feature | Cargo feature | Hybrid retrieval capability | Better exact-match precision and fallback paths | Semantic-only behavior |
rerank feature | Cargo feature | Cross-encoder rerank | Better top-result precision, higher latency | Lower latency, less fine-grained top ordering |
ann feature | Cargo feature | Approximate nearest-neighbor path | Better scale behavior at large corpus sizes | Simpler exact brute-force behavior |
grep/ripgrep/ctags are excellent for exact text and symbol lookup. frankensearch solves a different problem: semantic intent search over mixed corpora.
| Tool | Strong At | Limitation vs frankensearch |
|---|---|---|
grep | exact substrings | no semantic similarity |
ripgrep | very fast regex search | no embedding-based recall |
ctags | symbol navigation | not document-level semantic ranking |
frankensearch/fsfs | hybrid semantic + lexical, progressive refinement | higher complexity/runtime footprint |
Use both: keep rg for exact matches and use fsfs for intent-level retrieval.
Yes. Search/indexing runs on your machine. Network access is only needed for optional alternate-model downloads and update checks.
fsfs?Yes. Add frankensearch as a dependency and wire your own app/runtime.
Search still works using fast-tier and lexical paths; you get RefinementFailed or fast-only behavior.
Use jsonl for streaming automation and toon if your downstream stack expects TOON semantics.
No. Async/concurrency is built around asupersync and Cx.
Project policy is no direct external merges, but issues and PRs are still useful for bug reports and proposal clarity.
If you are working inside this repository as an internal/automation agent, run the gate before every push (it wraps the individual commands below):
scripts/quality-gate.sh
# or the pieces it runs:
cargo fmt --check
cargo check --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --lib --exclude frankensearch-quill-gauntlet
The real-model CLI lane also checks explicit native quality selection. Provision
all-MiniLM-L6-v2-native alongside the standard Potion and ONNX models before
running the gate. Its native test accepts MINILM_FIXTURE_DIR,
POTION_FIXTURE_DIR, and FASTEMBED_MINILM_FIXTURE_DIR for separate verified
fixture directories; absent or invalid fixtures fail the test.
To exercise an optimized executable through this lane, set FSFS_E2E_BINARY
to the absolute path of the release fsfs built from the source being checked.
The gate still builds and checks the workspace; its executable tests log the
selected binary explicitly.
Useful docs:
AGENTS.mddocs/e2e-artifact-contract.mddocs/dependency-semantics-policy.mddocs/planning/UPGRADE_LOG.md — upgrade history (relocated from repo root)docs/planning/BRIDGE_PLAN_2026-09-02.md — the gap-closing plan from the 2026-09 reality check (what is left between the README and the code, per goal)MIT License (with OpenAI/Anthropic Rider). See LICENSE.
4,051 commits
Rust
97.5%
Shell
2.2%