Dicklesworthstone/frankensearch

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

embeddings
full-text-search
hybrid-search
information-retrieval
nlp
progressive-search
reciprocal-rank-fusion
rust
search
semantic-search
simd
tantivy
two-tier
vector-search
Browse cluster: Vector Databases & Semantic Search

README

frankensearch

frankensearch - Two-tier hybrid local search for Rust

Crates.io License: MIT+Rider

Two-tier hybrid local search for Rust and the fsfs standalone CLI: fast first-pass results, then quality refinement.

Quick Navigation

Install In One Line

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:

  • zero-friction first run
  • auto-configured model cache path
  • sane defaults for interactive usage

Cargo Install (Developer Path)

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.

Native quality embeddings in fsfs

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.

Opt-in multilingual native embeddings

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.

Quick Start

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.

What It Does

frankensearch combines lexical and semantic retrieval with progressive delivery:

  • lexical BM25 for exact keyword precision (native Quill engine via the quill feature; Tantivy retained behind lexical-tantivy as the conformance oracle/migration lane)
  • fast semantic tier for immediate relevant hits
  • quality semantic tier for reranked refinement
  • reciprocal rank fusion (RRF) to combine sources robustly

Result: responsive first answers plus better final ranking without blocking the UI.

Core Features

  • Release binaries ship the Model2Vec/FastEmbed loaders; the installer downloads and verifies the two default models (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 path
  • Progressive search phases (Initial, Refined, RefinementFailed)
  • Agent-friendly streaming (--stream) with machine-readable output
  • Result explanation surfaces (fsfs explain <rank|R-id|path> against the last search)
  • Multiple output formats: table, json, jsonl, toon, csv
  • Watch/incremental indexing mode for local corpus updates
  • Portable SIMD vector search + quantized FSVI storage
  • Opt-in cross-encoder reranking of the refined head (fsfs search --rerank, search.rerank); ANN path via feature flags

CLI At A Glance

fsfs 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

Reproducible Showcase Suite

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

Configuration precedence (highest first; fsfs config prints the resolved source_precedence_applied for the current process):

  1. CLI flags
  2. environment variables
  3. config files (project fsfs.toml over user ~/.config/fsfs/config.toml)
  4. built-in defaults

--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:

VariablePurposeExample
FRANKENSEARCH_INDEX_DIROverride index/data directory~/.local/share/frankensearch
FRANKENSEARCH_MODEL_DIROverride model location~/.cache/frankensearch/models
FRANKENSEARCH_RERANKRe-score the refined head with the cross-encoder (same as --rerank); needs fsfs download-models ms-marco-minilm-l-6-v2 once1
FRANKENSEARCH_FAST_ONLYDisable quality work; follows CLI > environment > config precedence. false requires a profile that permits qualitytrue
FRANKENSEARCH_QUALITY_WEIGHTBlend quality vs fast tier0.7
FRANKENSEARCH_RRF_KRRF constant60
FRANKENSEARCH_LOGTracing filterinfo

For full contracts and knobs:

  • docs/fsfs-config-contract.md
  • docs/fsfs-dual-mode-contract.md
  • docs/fsfs-packaging-release-install-contract.md
  • docs/fsfs-packaging-release-install-contract.md#host-migration-playbooks-priority-projects
  • docs/fsfs-packaging-release-install-contract.md#staged-rollout-and-deterministic-fallback-protocol
  • docs/fsfs-packaging-release-install-contract.md#upgrade-and-migration-compatibility-verification-strategy
  • docs/architecture/

How It Works

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:

  • fast tier: potion-128M (or fallback)
  • fusion: RRF over lexical + semantic ranks
  • quality tier: MiniLM
  • optional final rerank (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-reranker

Architecture Breakdown

frankensearch is split into focused crates so each concern can evolve independently:

CrateResponsibility
frankensearch-coreShared types/traits/errors/config, query canonicalization/classification, metrics/eval helpers
frankensearch-embedEmbedding backends and fallback stack (hash, model2vec, fastembed)
frankensearch-indexFSVI vector storage, SIMD dot products, top-k search, optional native HNSW ANN
frankensearch-lexicalTantivy schema/index/search for BM25 lexical retrieval (conformance oracle + cass-compat interop lane)
frankensearch-quillNative pure-Rust BM25 lexical engine (FSLX segments, delta-visible indexing)
frankensearch-quill-gauntletDifferential conformance/perf gauntlet certifying Quill against the pinned Tantivy oracle
frankensearch-fusionRRF fusion, two-tier orchestration, blending, optional rerank integration
frankensearch-rerankCross-encoder reranking (pure-Rust frankentorch native backend + optional FastEmbed)
frankensearch-storageFrankenSQLite metadata persistence, dedup/content-hash tracking, embedding queue
frankensearch-durabilityRepair/protection primitives for index artifacts and segment health
crates/frankensearch-fsfsStandalone CLI product around the library stack
crates/frankensearch-tuiShared TUI shell/input/theme/replay framework used by fsfs/ops
crates/frankensearch-opsFleet 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:

  • use the top-level library as a drop-in engine in your own app
  • run the full standalone fsfs binary with progressive CLI/TUI workflows

Query Lifecycle (Detailed)

At execution time, the system follows this shape:

  1. Canonicalize and parse the query
  • Normalize text, strip noise, preserve user intent.
  • Classify query type (identifier, short keyword, natural language) for adaptive budgets.
  1. Run Phase 1 retrieval
  • Generate fast embedding (or fallback).
  • Run lexical BM25 search (if configured).
  • Run vector top-k on the fast index.
  • Fuse lexical + semantic candidates with RRF.
  • Emit Initial results quickly.
  1. Run Phase 2 refinement (unless fast_only)
  • Generate quality embedding.
  • Re-score or refine top candidates with quality tier.
  • Blend fast and quality semantic scores.
  • Optionally rerank with cross-encoder.
  • Emit Refined or RefinementFailed (graceful degradation path).

Algorithms Used

1. Reciprocal Rank Fusion (RRF)

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:

  • robust to score-scale mismatch between BM25 and vector similarity
  • simple, stable, and strong in practice
  • rewards documents that appear in multiple retrieval channels

2. Two-Tier Score Blending

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).

3. Deterministic Ordering

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.

Index Format and Retrieval Internals

FSVI Storage

Vector data is stored in FSVI files with memory-mapped access:

  • primary default quantization: f16 (good memory/quality tradeoff)
  • optional f32 paths where needed
  • explicit metadata and format checks to catch corruption early

Why this matters:

  • lower memory footprint than full f32 storage in common workloads
  • fast startup and read paths via mmap
  • predictable, portable on-disk format for local search use

Vector Top-K Implementation

The brute-force search path is optimized around:

  • SIMD dot products
  • bounded heap selection for top-k
  • NaN-safe total ordering
  • two-phase work: score first, materialize doc ids for winners

This gives strong baseline behavior while ANN remains optional for larger corpora.

Runtime and Concurrency Model

The async model uses asupersync and capability context (Cx), not Tokio.

Important implications:

  • host-controlled runtime ownership: your app provides runtime/cancellation context
  • cancellation-aware search phases and timeouts
  • no hard Tokio coupling in public contracts

This is useful if you need to embed search inside existing non-Tokio runtimes or strictly controlled execution environments.

Design Principles

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.

Why This Is Useful

frankensearch is especially strong when you need:

  • intent-level recall beyond exact grep matching
  • low-latency first results for interactive workflows
  • local-first operation with optional model download only
  • agent-friendly output contracts (jsonl, toon) and explainability hooks
  • one reusable engine across multiple products instead of bespoke search stacks

In short: it closes the gap between exact text lookup and semantic retrieval without forcing you into remote services or heavyweight distributed systems.

Tuning Playbook

Common tuning patterns:

  • Need lower tail latency:

    • run fast-only: FRANKENSEARCH_FAST_ONLY=true or --fast-only; both work with the default performance profile
    • reduce candidate budget and rerank depth
    • keep lexical enabled for exact-match recovery
  • Need higher relevance quality:

    • keep quality tier enabled
    • increase candidate multiplier
    • enable reranking for top-N only
  • Need memory efficiency:

    • stay on f16 index quantization
    • use ANN selectively above practical corpus thresholds
  • Need operational clarity:

    • emit JSONL/TOON for automation
    • monitor phase timings and drift through test harness artifacts

Quality and Evaluation Discipline

The repository includes explicit quality harnesses and statistical checks:

  • IR metrics: nDCG@K, MRR, Recall@K, plus bootstrap confidence intervals
  • profile comparisons with deterministic manifests
  • benchmark/e2e artifact contracts for repeatable regression analysis

This keeps tuning decisions evidence-driven rather than anecdotal.

Limits and Tradeoffs

Being explicit about scope helps set expectations:

  • Hybrid search is more complex than plain rg and has model/runtime overhead.
  • Quality-tier refinement improves ranking but increases latency.
  • ANN helps at larger scale but adds index lifecycle complexity.
  • Semantic quality depends on corpus characteristics and embedding model fit.

Use rg/grep for strict exact matching and frankensearch when ranking by intent and contextual relevance matters.

Library Integration Quickstart (Rust)

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:

  • Use 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.
  • Enable quill for the native bulk-built lexical index; use lexical-tantivy only for the explicit Tantivy oracle/comparator lane during migration.
  • Enable 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.
  • Use search_collect_with_text or full search(...) when you need negation filtering (-term) and rerank text access.
  • Keep TwoTierConfig explicit in code for reproducible behavior across environments.
  • This path is proven with the real models, not doubles: the gate's 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.

Baseline Performance Envelope (Reference)

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:

OperationTypical EnvelopeBasis
Fast hash embedding (hash_embed_fnv, tokenize + FNV)~2 μsledger, 2026-07-04
Fast model embedding (potion-128M, short query)~0.1 msreceipt, 2026-09-03 (p50 0.10 ms, p95 0.17 ms)
Quality model embedding (MiniLM, short query)~5 msreceipt, 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 msreceipt, 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 msreceipt, 2026-09-03 (p50 0.23 ms, p95 0.38 ms)
RRF fusion (1,000 + 1,000 candidates)~23 μsledger, 2026-07-04
Phase 1 initial delivery (library TwoTierSearcher, hybrid Quill + potion, warm)< 1 msreceipt, 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 msreceipt, 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 sreceipt, 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 sproduct 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 msproduct 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 msproduct 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 sproduct 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 sproduct 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:

  • query class and candidate budget
  • corpus size and document length distribution
  • enabled features (lexical, rerank, ann)
  • model tier selection and cache state

Failure Modes and Degradation Behavior

The engine is intentionally designed to degrade gracefully:

ConditionBehaviorWhat Caller Sees
Quality refinement timeoutPhase 2 aborts safelySearchPhase::RefinementFailed { error: SearchTimeout, ... }
Quality embedder errorsInitial results preservedSearchPhase::RefinementFailed { ... }
fast_only=trueSkip quality phase by designonly Initial phase + skip_reason="fast_only"
No quality embedder configuredSkip quality phaseonly Initial phase + skip_reason="no_quality_embedder"
Fast embedder fails but lexical succeedslexical-only fallbackvalid Initial results from lexical path
Fast embedder fails and no lexical fallbackhard failuresearch returns error
Lexical backend failuresemantic continuessearch 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.

Production Deployment Checklist

Use this as a pragmatic hardening pass before rollout:

  1. Pin explicit feature set (semantic, hybrid, full, etc.) and toolchain.
  2. Decide runtime mode per environment (fast_only in latency-critical paths, full two-tier where quality matters).
  3. Set FRANKENSEARCH_MODEL_DIR to a stable writable path with enough disk.
  4. Enable structured logs/metrics (FRANKENSEARCH_LOG) and capture phase timings.
  5. Run quality gates: scripts/quality-gate.sh (or dsr quality --tool frankensearch), which covers
    • cargo fmt --check
    • cargo check --workspace --all-targets
    • cargo clippy --workspace --all-targets -- -D warnings
    • cargo test --workspace --lib --exclude frankensearch-quill-gauntlet plus the fsfs test binaries
    • the Quill native witness against real Quill and pinned Tantivy, plus its oracle-validator negatives
    • the real-model quick-start lane and the executable quick-start gate against the built binary
  6. Run benchmark and quality harnesses on representative corpora before release.
  7. Validate degradation behavior by intentionally forcing quality timeout/failure.
  8. For large corpora, evaluate ANN thresholding and memory budget explicitly.
  9. Keep reproducible artifacts for before/after tuning comparisons.

Crates.io Publishing

Publication 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.

Quality Gate (dsr, not GitHub Actions)

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.

Troubleshooting by Symptom

SymptomLikely CauseWhat To Check
Initial results are slowCandidate budget too high, cold cache, oversized corpuscandidate multipliers, model cache warm-up, corpus and index size
No refined phase arrivesfast_only enabled, no quality embedder, timeoutFRANKENSEARCH_FAST_ONLY, quality model availability, quality_timeout_ms
RefinementFailed appears oftenquality model unstable/slow, timeout budget too tightmodel path/logs, timeout config, CPU contention
Results feel exact-match heavylexical overweight via candidate mix, weak semantic model tierembedder stack selection, query class budgets, quality tier availability
Results feel semantically offembedding model mismatch for corpus domainswitch model tier, tune blend weight, add rerank
Negation queries (-term) behave unexpectedlymissing text provider in convenience pathuse search_collect_with_text/search(...) with text callback
Output parsing issues in automationwrong format for downstream parseruse --format jsonl or --format toon consistently
High memory usagelarge index + quality/rerank/ANN enabledfeature set, f16 defaults, ANN config, corpus scope
Legacy or foreign lexical layout detected at openpre-flip Tantivy directory or damaged FSLX segmentrebuild-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 warningquality model absent at index time, so the generation is fast-onlyinstall and verify all-minilm-l6-v2, then re-index (Quick Start); searches stop at INITIAL until then

Sequence Diagram (Mermaid)

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

Feature-Flag Decision Table

These are crate feature flags from frankensearch/Cargo.toml:

GoalRecommended Feature SetWhy
Fastest dev loop / CI smoke checksdefault (hash)zero model downloads, minimal deps
Better semantic quality without lexicalsemanticenables hash + model2vec + fastembed
Hybrid retrieval (semantic + BM25)hybridadds the Quill BM25 lexical arm (the post-flip lexical default) on top of semantic recall
Persistent local indexingpersistenthybrid + storage for durable metadata/queues
Durable + self-healing stackdurablepersistent + durability
Lexical-only library buildquillpure-Rust Quill BM25 engine alone; since the flip, the lexical feature already selects Quill
Tantivy oracle/migration lanelexical-tantivyexplicit Tantivy-backed lexical path; cass-compat aliases it for external CASS schema-v8 interop
Full capability surfacefulldurable + rerank + ann + download + graph + api
Full stack + FTS5 storage backendfull-fts5full + 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

Config Recipes

1) Latency-First Profile

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:

  • returns Initial quickly and skips/limits expensive refinement behavior
  • predictable low-latency tail for chat/assistant loops

2) Quality-First Profile

Best 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:

  • gives quality tier and rerank more room to improve ordering
  • increases median and tail latency

3) Memory/CPU-Conservative Profile

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:

  • reduces expensive quality-phase work
  • avoids ANN build/search overhead for smaller corpora

Optional: TwoTierConfig::optimized() TOML Recipe

For 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.

Reference Appendix

Key Source Files

AreaSource FilePurpose
Facade cratefrankensearch/src/lib.rsTop-level public API surface and re-exports
Index build workflowfrankensearch/src/index_builder.rsHigh-level corpus-to-index pipeline
Progressive orchestrationcrates/frankensearch-fusion/src/searcher.rsPhase 1/2 flow, fallback paths, telemetry
Rank fusioncrates/frankensearch-fusion/src/rrf.rsReciprocal Rank Fusion implementation
Two-tier blendingcrates/frankensearch-fusion/src/blend.rsFast/quality score normalization and blending
Two-tier index wrappercrates/frankensearch-index/src/two_tier.rsFast/quality index alignment and lookup
Top-k vector searchcrates/frankensearch-index/src/search.rsHeap-based top-k selection and scoring paths
On-disk vector formatcrates/frankensearch-index/src/quantization.rsFSVI quantization; mmap via mapped_file.rs
Core config knobscrates/frankensearch-core/src/config.rsTwoTierConfig, defaults, env overrides
Core result typescrates/frankensearch-core/src/types.rsSearchPhase, ScoredResult, hit structs
Query classificationcrates/frankensearch-core/src/query_class.rsQuery-type detection and adaptive budgets
Eval/statisticscrates/frankensearch-core/src/metrics_eval.rsnDCG/MRR/Recall/MAP + bootstrap helpers
Embedder auto-detectcrates/frankensearch-embed/src/auto_detect.rsFast/quality model discovery and stack setup
Storage ingest/queuecrates/frankensearch-storage/src/pipeline.rsStorage-backed ingestion, queue processing, embedding sinks
Durability repair layercrates/frankensearch-durability/src/fsvi_protector.rsProtect/verify/repair flows for vector artifacts
FSFS CLI entrycrates/frankensearch-fsfs/src/lib.rsStandalone CLI product wiring
FSFS runtime orchestrationcrates/frankensearch-fsfs/src/runtime.rsCommand dispatch, search/index execution, stream emission
Shared TUI shellcrates/frankensearch-tui/src/shell.rsReusable shell loop/navigation/overlay plumbing
Ops telemetry storagecrates/frankensearch-ops/src/storage.rsControl-plane telemetry persistence/materialization
Native lexical enginecrates/frankensearch-quill/src/index.rsQuill index lifecycle: seal/flush, FSLX segments, MaxScore/block-max WAND clause thresholds
Quill query executioncrates/frankensearch-quill/src/argus.rsBM25 query execution, sealed cursors, block-max pruning
Quill postings codeccrates/frankensearch-quill/src/quiver.rsPosting blocks and per-block block-max entries (encode_with_block_max)
Quill differential gauntletcrates/frankensearch-quill-gauntlet/src/runner.rsConformance/perf witness certifying Quill against the pinned Tantivy oracle

Glossary

TermMeaning
Two-tier searchProgressive retrieval: fast initial pass, quality refinement pass
Phase 1 / InitialFirst emitted result set, optimized for low latency
Phase 2 / RefinedOptional improved ranking after quality embedding
RefinementFailedGraceful degradation event when Phase 2 errors/times out
RRFReciprocal Rank Fusion combining lexical + semantic rank lists
BM25Lexical ranking function used by the lexical backends (Quill and Tantivy)
FSLXQuill's on-disk segment format: framed sections (TERMDICT, POSTINGS, POSITIONS, BLOCKMAX, DOCLEN, IDMAP, IDHASH) with reference validation
Delta segmentQuill lightweight segment carrying a generation's upserts/deletes against a base; visibility is delta-resolved at read time
Concat-mergeQuill segment merge mode that concatenates same-shape term streams without re-encoding (vs compacting tombstoned rows)
FSVIOn-disk vector index format used by frankensearch-index
f16 quantizationHalf-precision storage mode reducing memory footprint
TwoTierIndexWrapper over fast and optional quality vector indexes
TwoTierSearcherMain orchestrator that runs retrieval/fusion/refinement
TwoTierConfigPrimary tuning config for latency/quality behavior
TwoTierMetricsPer-search diagnostics (phase timings, candidate counts, skip reason)
EmbedderStackPaired fast + optional quality embedder selection object
Cxasupersync capability context passed into async operations

Knob Impact Matrix

KnobWhere SetPrimary ImpactIncrease Tends ToDecrease Tends To
quality_weightTwoTierConfig, FRANKENSEARCH_QUALITY_WEIGHTBlend balanceFavor quality-tier ranking signalFavor fast-tier ranking signal
rrf_kTwoTierConfig, FRANKENSEARCH_RRF_KRRF rank sensitivityFlatten rank differences across sourcesEmphasize top ranks more strongly
candidate_multiplierTwoTierConfigCandidate pool sizeImprove recall headroom, increase latency/workReduce latency/work, may reduce recall
quality_timeout_msTwoTierConfig, FRANKENSEARCH_QUALITY_TIMEOUTPhase 2 budgetMore chances to finish refinementMore RefinementFailed timeouts
fast_onlyTwoTierConfig, FRANKENSEARCH_FAST_ONLYPhase behaviorSkip Phase 2 entirely (true)Enable Phase 2 when quality embedder exists (false)
hnsw_thresholdTwoTierConfig, FRANKENSEARCH_HNSW_THRESHOLDANN activation pointUse brute-force for more corpus sizesUse ANN earlier for large corpora
hnsw_ef_searchTwoTierConfigANN query beam widthBetter ANN recall, more latencyLower latency, potentially lower recall
mrl_search_dimsTwoTierConfigMRL scan dimensionalityBetter first-pass quality, more computeFaster first-pass, potentially less quality
mrl_rescore_top_kTwoTierConfigFull-dim rescore scopeBetter refined ordering, more computeLess compute, potentially weaker refinement
lexical featureCargo featureHybrid retrieval capabilityBetter exact-match precision and fallback pathsSemantic-only behavior
rerank featureCargo featureCross-encoder rerankBetter top-result precision, higher latencyLower latency, less fine-grained top ordering
ann featureCargo featureApproximate nearest-neighbor pathBetter scale behavior at large corpus sizesSimpler exact brute-force behavior

Why Not Just grep/ripgrep/ctags?

grep/ripgrep/ctags are excellent for exact text and symbol lookup. frankensearch solves a different problem: semantic intent search over mixed corpora.

ToolStrong AtLimitation vs frankensearch
grepexact substringsno semantic similarity
ripgrepvery fast regex searchno embedding-based recall
ctagssymbol navigationnot document-level semantic ranking
frankensearch/fsfshybrid semantic + lexical, progressive refinementhigher complexity/runtime footprint

Use both: keep rg for exact matches and use fsfs for intent-level retrieval.

FAQ

Does it run fully local?

Yes. Search/indexing runs on your machine. Network access is only needed for optional alternate-model downloads and update checks.

Can I use only the library and skip fsfs?

Yes. Add frankensearch as a dependency and wire your own app/runtime.

What if the quality model is unavailable?

Search still works using fast-tier and lexical paths; you get RefinementFailed or fast-only behavior.

Which output format should agents use?

Use jsonl for streaming automation and toon if your downstream stack expects TOON semantics.

Is this tied to Tokio?

No. Async/concurrency is built around asupersync and Cx.

Contributing

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.md
  • docs/e2e-artifact-contract.md
  • docs/dependency-semantics-policy.md
  • docs/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)

License

MIT License (with OpenAI/Anthropic Rider). See LICENSE.

Contributors

Dicklesworthstone

4,051 commits

Dicklesworthstone/frankensearch

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

embeddings
full-text-search
hybrid-search
information-retrieval
nlp
progressive-search
reciprocal-rank-fusion
rust
search
semantic-search
simd
tantivy
two-tier
vector-search
Browse cluster: Vector Databases & Semantic Search

README

frankensearch

frankensearch - Two-tier hybrid local search for Rust

Crates.io License: MIT+Rider

Two-tier hybrid local search for Rust and the fsfs standalone CLI: fast first-pass results, then quality refinement.

Quick Navigation

Install In One Line

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:

  • zero-friction first run
  • auto-configured model cache path
  • sane defaults for interactive usage

Cargo Install (Developer Path)

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.

Native quality embeddings in fsfs

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.

Opt-in multilingual native embeddings

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.

Quick Start

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.

What It Does

frankensearch combines lexical and semantic retrieval with progressive delivery:

  • lexical BM25 for exact keyword precision (native Quill engine via the quill feature; Tantivy retained behind lexical-tantivy as the conformance oracle/migration lane)
  • fast semantic tier for immediate relevant hits
  • quality semantic tier for reranked refinement
  • reciprocal rank fusion (RRF) to combine sources robustly

Result: responsive first answers plus better final ranking without blocking the UI.

Core Features

  • Release binaries ship the Model2Vec/FastEmbed loaders; the installer downloads and verifies the two default models (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 path
  • Progressive search phases (Initial, Refined, RefinementFailed)
  • Agent-friendly streaming (--stream) with machine-readable output
  • Result explanation surfaces (fsfs explain <rank|R-id|path> against the last search)
  • Multiple output formats: table, json, jsonl, toon, csv
  • Watch/incremental indexing mode for local corpus updates
  • Portable SIMD vector search + quantized FSVI storage
  • Opt-in cross-encoder reranking of the refined head (fsfs search --rerank, search.rerank); ANN path via feature flags

CLI At A Glance

fsfs 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

Reproducible Showcase Suite

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

Configuration precedence (highest first; fsfs config prints the resolved source_precedence_applied for the current process):

  1. CLI flags
  2. environment variables
  3. config files (project fsfs.toml over user ~/.config/fsfs/config.toml)
  4. built-in defaults

--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:

VariablePurposeExample
FRANKENSEARCH_INDEX_DIROverride index/data directory~/.local/share/frankensearch
FRANKENSEARCH_MODEL_DIROverride model location~/.cache/frankensearch/models
FRANKENSEARCH_RERANKRe-score the refined head with the cross-encoder (same as --rerank); needs fsfs download-models ms-marco-minilm-l-6-v2 once1
FRANKENSEARCH_FAST_ONLYDisable quality work; follows CLI > environment > config precedence. false requires a profile that permits qualitytrue
FRANKENSEARCH_QUALITY_WEIGHTBlend quality vs fast tier0.7
FRANKENSEARCH_RRF_KRRF constant60
FRANKENSEARCH_LOGTracing filterinfo

For full contracts and knobs:

  • docs/fsfs-config-contract.md
  • docs/fsfs-dual-mode-contract.md
  • docs/fsfs-packaging-release-install-contract.md
  • docs/fsfs-packaging-release-install-contract.md#host-migration-playbooks-priority-projects
  • docs/fsfs-packaging-release-install-contract.md#staged-rollout-and-deterministic-fallback-protocol
  • docs/fsfs-packaging-release-install-contract.md#upgrade-and-migration-compatibility-verification-strategy
  • docs/architecture/

How It Works

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:

  • fast tier: potion-128M (or fallback)
  • fusion: RRF over lexical + semantic ranks
  • quality tier: MiniLM
  • optional final rerank (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-reranker

Architecture Breakdown

frankensearch is split into focused crates so each concern can evolve independently:

CrateResponsibility
frankensearch-coreShared types/traits/errors/config, query canonicalization/classification, metrics/eval helpers
frankensearch-embedEmbedding backends and fallback stack (hash, model2vec, fastembed)
frankensearch-indexFSVI vector storage, SIMD dot products, top-k search, optional native HNSW ANN
frankensearch-lexicalTantivy schema/index/search for BM25 lexical retrieval (conformance oracle + cass-compat interop lane)
frankensearch-quillNative pure-Rust BM25 lexical engine (FSLX segments, delta-visible indexing)
frankensearch-quill-gauntletDifferential conformance/perf gauntlet certifying Quill against the pinned Tantivy oracle
frankensearch-fusionRRF fusion, two-tier orchestration, blending, optional rerank integration
frankensearch-rerankCross-encoder reranking (pure-Rust frankentorch native backend + optional FastEmbed)
frankensearch-storageFrankenSQLite metadata persistence, dedup/content-hash tracking, embedding queue
frankensearch-durabilityRepair/protection primitives for index artifacts and segment health
crates/frankensearch-fsfsStandalone CLI product around the library stack
crates/frankensearch-tuiShared TUI shell/input/theme/replay framework used by fsfs/ops
crates/frankensearch-opsFleet 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:

  • use the top-level library as a drop-in engine in your own app
  • run the full standalone fsfs binary with progressive CLI/TUI workflows

Query Lifecycle (Detailed)

At execution time, the system follows this shape:

  1. Canonicalize and parse the query
  • Normalize text, strip noise, preserve user intent.
  • Classify query type (identifier, short keyword, natural language) for adaptive budgets.
  1. Run Phase 1 retrieval
  • Generate fast embedding (or fallback).
  • Run lexical BM25 search (if configured).
  • Run vector top-k on the fast index.
  • Fuse lexical + semantic candidates with RRF.
  • Emit Initial results quickly.
  1. Run Phase 2 refinement (unless fast_only)
  • Generate quality embedding.
  • Re-score or refine top candidates with quality tier.
  • Blend fast and quality semantic scores.
  • Optionally rerank with cross-encoder.
  • Emit Refined or RefinementFailed (graceful degradation path).

Algorithms Used

1. Reciprocal Rank Fusion (RRF)

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:

  • robust to score-scale mismatch between BM25 and vector similarity
  • simple, stable, and strong in practice
  • rewards documents that appear in multiple retrieval channels

2. Two-Tier Score Blending

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).

3. Deterministic Ordering

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.

Index Format and Retrieval Internals

FSVI Storage

Vector data is stored in FSVI files with memory-mapped access:

  • primary default quantization: f16 (good memory/quality tradeoff)
  • optional f32 paths where needed
  • explicit metadata and format checks to catch corruption early

Why this matters:

  • lower memory footprint than full f32 storage in common workloads
  • fast startup and read paths via mmap
  • predictable, portable on-disk format for local search use

Vector Top-K Implementation

The brute-force search path is optimized around:

  • SIMD dot products
  • bounded heap selection for top-k
  • NaN-safe total ordering
  • two-phase work: score first, materialize doc ids for winners

This gives strong baseline behavior while ANN remains optional for larger corpora.

Runtime and Concurrency Model

The async model uses asupersync and capability context (Cx), not Tokio.

Important implications:

  • host-controlled runtime ownership: your app provides runtime/cancellation context
  • cancellation-aware search phases and timeouts
  • no hard Tokio coupling in public contracts

This is useful if you need to embed search inside existing non-Tokio runtimes or strictly controlled execution environments.

Design Principles

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.

Why This Is Useful

frankensearch is especially strong when you need:

  • intent-level recall beyond exact grep matching
  • low-latency first results for interactive workflows
  • local-first operation with optional model download only
  • agent-friendly output contracts (jsonl, toon) and explainability hooks
  • one reusable engine across multiple products instead of bespoke search stacks

In short: it closes the gap between exact text lookup and semantic retrieval without forcing you into remote services or heavyweight distributed systems.

Tuning Playbook

Common tuning patterns:

  • Need lower tail latency:

    • run fast-only: FRANKENSEARCH_FAST_ONLY=true or --fast-only; both work with the default performance profile
    • reduce candidate budget and rerank depth
    • keep lexical enabled for exact-match recovery
  • Need higher relevance quality:

    • keep quality tier enabled
    • increase candidate multiplier
    • enable reranking for top-N only
  • Need memory efficiency:

    • stay on f16 index quantization
    • use ANN selectively above practical corpus thresholds
  • Need operational clarity:

    • emit JSONL/TOON for automation
    • monitor phase timings and drift through test harness artifacts

Quality and Evaluation Discipline

The repository includes explicit quality harnesses and statistical checks:

  • IR metrics: nDCG@K, MRR, Recall@K, plus bootstrap confidence intervals
  • profile comparisons with deterministic manifests
  • benchmark/e2e artifact contracts for repeatable regression analysis

This keeps tuning decisions evidence-driven rather than anecdotal.

Limits and Tradeoffs

Being explicit about scope helps set expectations:

  • Hybrid search is more complex than plain rg and has model/runtime overhead.
  • Quality-tier refinement improves ranking but increases latency.
  • ANN helps at larger scale but adds index lifecycle complexity.
  • Semantic quality depends on corpus characteristics and embedding model fit.

Use rg/grep for strict exact matching and frankensearch when ranking by intent and contextual relevance matters.

Library Integration Quickstart (Rust)

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:

  • Use 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.
  • Enable quill for the native bulk-built lexical index; use lexical-tantivy only for the explicit Tantivy oracle/comparator lane during migration.
  • Enable 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.
  • Use search_collect_with_text or full search(...) when you need negation filtering (-term) and rerank text access.
  • Keep TwoTierConfig explicit in code for reproducible behavior across environments.
  • This path is proven with the real models, not doubles: the gate's 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.

Baseline Performance Envelope (Reference)

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:

OperationTypical EnvelopeBasis
Fast hash embedding (hash_embed_fnv, tokenize + FNV)~2 μsledger, 2026-07-04
Fast model embedding (potion-128M, short query)~0.1 msreceipt, 2026-09-03 (p50 0.10 ms, p95 0.17 ms)
Quality model embedding (MiniLM, short query)~5 msreceipt, 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 msreceipt, 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 msreceipt, 2026-09-03 (p50 0.23 ms, p95 0.38 ms)
RRF fusion (1,000 + 1,000 candidates)~23 μsledger, 2026-07-04
Phase 1 initial delivery (library TwoTierSearcher, hybrid Quill + potion, warm)< 1 msreceipt, 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 msreceipt, 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 sreceipt, 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 sproduct 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 msproduct 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 msproduct 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 sproduct 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 sproduct 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:

  • query class and candidate budget
  • corpus size and document length distribution
  • enabled features (lexical, rerank, ann)
  • model tier selection and cache state

Failure Modes and Degradation Behavior

The engine is intentionally designed to degrade gracefully:

ConditionBehaviorWhat Caller Sees
Quality refinement timeoutPhase 2 aborts safelySearchPhase::RefinementFailed { error: SearchTimeout, ... }
Quality embedder errorsInitial results preservedSearchPhase::RefinementFailed { ... }
fast_only=trueSkip quality phase by designonly Initial phase + skip_reason="fast_only"
No quality embedder configuredSkip quality phaseonly Initial phase + skip_reason="no_quality_embedder"
Fast embedder fails but lexical succeedslexical-only fallbackvalid Initial results from lexical path
Fast embedder fails and no lexical fallbackhard failuresearch returns error
Lexical backend failuresemantic continuessearch 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.

Production Deployment Checklist

Use this as a pragmatic hardening pass before rollout:

  1. Pin explicit feature set (semantic, hybrid, full, etc.) and toolchain.
  2. Decide runtime mode per environment (fast_only in latency-critical paths, full two-tier where quality matters).
  3. Set FRANKENSEARCH_MODEL_DIR to a stable writable path with enough disk.
  4. Enable structured logs/metrics (FRANKENSEARCH_LOG) and capture phase timings.
  5. Run quality gates: scripts/quality-gate.sh (or dsr quality --tool frankensearch), which covers
    • cargo fmt --check
    • cargo check --workspace --all-targets
    • cargo clippy --workspace --all-targets -- -D warnings
    • cargo test --workspace --lib --exclude frankensearch-quill-gauntlet plus the fsfs test binaries
    • the Quill native witness against real Quill and pinned Tantivy, plus its oracle-validator negatives
    • the real-model quick-start lane and the executable quick-start gate against the built binary
  6. Run benchmark and quality harnesses on representative corpora before release.
  7. Validate degradation behavior by intentionally forcing quality timeout/failure.
  8. For large corpora, evaluate ANN thresholding and memory budget explicitly.
  9. Keep reproducible artifacts for before/after tuning comparisons.

Crates.io Publishing

Publication 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.

Quality Gate (dsr, not GitHub Actions)

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.

Troubleshooting by Symptom

SymptomLikely CauseWhat To Check
Initial results are slowCandidate budget too high, cold cache, oversized corpuscandidate multipliers, model cache warm-up, corpus and index size
No refined phase arrivesfast_only enabled, no quality embedder, timeoutFRANKENSEARCH_FAST_ONLY, quality model availability, quality_timeout_ms
RefinementFailed appears oftenquality model unstable/slow, timeout budget too tightmodel path/logs, timeout config, CPU contention
Results feel exact-match heavylexical overweight via candidate mix, weak semantic model tierembedder stack selection, query class budgets, quality tier availability
Results feel semantically offembedding model mismatch for corpus domainswitch model tier, tune blend weight, add rerank
Negation queries (-term) behave unexpectedlymissing text provider in convenience pathuse search_collect_with_text/search(...) with text callback
Output parsing issues in automationwrong format for downstream parseruse --format jsonl or --format toon consistently
High memory usagelarge index + quality/rerank/ANN enabledfeature set, f16 defaults, ANN config, corpus scope
Legacy or foreign lexical layout detected at openpre-flip Tantivy directory or damaged FSLX segmentrebuild-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 warningquality model absent at index time, so the generation is fast-onlyinstall and verify all-minilm-l6-v2, then re-index (Quick Start); searches stop at INITIAL until then

Sequence Diagram (Mermaid)

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

Feature-Flag Decision Table

These are crate feature flags from frankensearch/Cargo.toml:

GoalRecommended Feature SetWhy
Fastest dev loop / CI smoke checksdefault (hash)zero model downloads, minimal deps
Better semantic quality without lexicalsemanticenables hash + model2vec + fastembed
Hybrid retrieval (semantic + BM25)hybridadds the Quill BM25 lexical arm (the post-flip lexical default) on top of semantic recall
Persistent local indexingpersistenthybrid + storage for durable metadata/queues
Durable + self-healing stackdurablepersistent + durability
Lexical-only library buildquillpure-Rust Quill BM25 engine alone; since the flip, the lexical feature already selects Quill
Tantivy oracle/migration lanelexical-tantivyexplicit Tantivy-backed lexical path; cass-compat aliases it for external CASS schema-v8 interop
Full capability surfacefulldurable + rerank + ann + download + graph + api
Full stack + FTS5 storage backendfull-fts5full + 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

Config Recipes

1) Latency-First Profile

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:

  • returns Initial quickly and skips/limits expensive refinement behavior
  • predictable low-latency tail for chat/assistant loops

2) Quality-First Profile

Best 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:

  • gives quality tier and rerank more room to improve ordering
  • increases median and tail latency

3) Memory/CPU-Conservative Profile

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:

  • reduces expensive quality-phase work
  • avoids ANN build/search overhead for smaller corpora

Optional: TwoTierConfig::optimized() TOML Recipe

For 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.

Reference Appendix

Key Source Files

AreaSource FilePurpose
Facade cratefrankensearch/src/lib.rsTop-level public API surface and re-exports
Index build workflowfrankensearch/src/index_builder.rsHigh-level corpus-to-index pipeline
Progressive orchestrationcrates/frankensearch-fusion/src/searcher.rsPhase 1/2 flow, fallback paths, telemetry
Rank fusioncrates/frankensearch-fusion/src/rrf.rsReciprocal Rank Fusion implementation
Two-tier blendingcrates/frankensearch-fusion/src/blend.rsFast/quality score normalization and blending
Two-tier index wrappercrates/frankensearch-index/src/two_tier.rsFast/quality index alignment and lookup
Top-k vector searchcrates/frankensearch-index/src/search.rsHeap-based top-k selection and scoring paths
On-disk vector formatcrates/frankensearch-index/src/quantization.rsFSVI quantization; mmap via mapped_file.rs
Core config knobscrates/frankensearch-core/src/config.rsTwoTierConfig, defaults, env overrides
Core result typescrates/frankensearch-core/src/types.rsSearchPhase, ScoredResult, hit structs
Query classificationcrates/frankensearch-core/src/query_class.rsQuery-type detection and adaptive budgets
Eval/statisticscrates/frankensearch-core/src/metrics_eval.rsnDCG/MRR/Recall/MAP + bootstrap helpers
Embedder auto-detectcrates/frankensearch-embed/src/auto_detect.rsFast/quality model discovery and stack setup
Storage ingest/queuecrates/frankensearch-storage/src/pipeline.rsStorage-backed ingestion, queue processing, embedding sinks
Durability repair layercrates/frankensearch-durability/src/fsvi_protector.rsProtect/verify/repair flows for vector artifacts
FSFS CLI entrycrates/frankensearch-fsfs/src/lib.rsStandalone CLI product wiring
FSFS runtime orchestrationcrates/frankensearch-fsfs/src/runtime.rsCommand dispatch, search/index execution, stream emission
Shared TUI shellcrates/frankensearch-tui/src/shell.rsReusable shell loop/navigation/overlay plumbing
Ops telemetry storagecrates/frankensearch-ops/src/storage.rsControl-plane telemetry persistence/materialization
Native lexical enginecrates/frankensearch-quill/src/index.rsQuill index lifecycle: seal/flush, FSLX segments, MaxScore/block-max WAND clause thresholds
Quill query executioncrates/frankensearch-quill/src/argus.rsBM25 query execution, sealed cursors, block-max pruning
Quill postings codeccrates/frankensearch-quill/src/quiver.rsPosting blocks and per-block block-max entries (encode_with_block_max)
Quill differential gauntletcrates/frankensearch-quill-gauntlet/src/runner.rsConformance/perf witness certifying Quill against the pinned Tantivy oracle

Glossary

TermMeaning
Two-tier searchProgressive retrieval: fast initial pass, quality refinement pass
Phase 1 / InitialFirst emitted result set, optimized for low latency
Phase 2 / RefinedOptional improved ranking after quality embedding
RefinementFailedGraceful degradation event when Phase 2 errors/times out
RRFReciprocal Rank Fusion combining lexical + semantic rank lists
BM25Lexical ranking function used by the lexical backends (Quill and Tantivy)
FSLXQuill's on-disk segment format: framed sections (TERMDICT, POSTINGS, POSITIONS, BLOCKMAX, DOCLEN, IDMAP, IDHASH) with reference validation
Delta segmentQuill lightweight segment carrying a generation's upserts/deletes against a base; visibility is delta-resolved at read time
Concat-mergeQuill segment merge mode that concatenates same-shape term streams without re-encoding (vs compacting tombstoned rows)
FSVIOn-disk vector index format used by frankensearch-index
f16 quantizationHalf-precision storage mode reducing memory footprint
TwoTierIndexWrapper over fast and optional quality vector indexes
TwoTierSearcherMain orchestrator that runs retrieval/fusion/refinement
TwoTierConfigPrimary tuning config for latency/quality behavior
TwoTierMetricsPer-search diagnostics (phase timings, candidate counts, skip reason)
EmbedderStackPaired fast + optional quality embedder selection object
Cxasupersync capability context passed into async operations

Knob Impact Matrix

KnobWhere SetPrimary ImpactIncrease Tends ToDecrease Tends To
quality_weightTwoTierConfig, FRANKENSEARCH_QUALITY_WEIGHTBlend balanceFavor quality-tier ranking signalFavor fast-tier ranking signal
rrf_kTwoTierConfig, FRANKENSEARCH_RRF_KRRF rank sensitivityFlatten rank differences across sourcesEmphasize top ranks more strongly
candidate_multiplierTwoTierConfigCandidate pool sizeImprove recall headroom, increase latency/workReduce latency/work, may reduce recall
quality_timeout_msTwoTierConfig, FRANKENSEARCH_QUALITY_TIMEOUTPhase 2 budgetMore chances to finish refinementMore RefinementFailed timeouts
fast_onlyTwoTierConfig, FRANKENSEARCH_FAST_ONLYPhase behaviorSkip Phase 2 entirely (true)Enable Phase 2 when quality embedder exists (false)
hnsw_thresholdTwoTierConfig, FRANKENSEARCH_HNSW_THRESHOLDANN activation pointUse brute-force for more corpus sizesUse ANN earlier for large corpora
hnsw_ef_searchTwoTierConfigANN query beam widthBetter ANN recall, more latencyLower latency, potentially lower recall
mrl_search_dimsTwoTierConfigMRL scan dimensionalityBetter first-pass quality, more computeFaster first-pass, potentially less quality
mrl_rescore_top_kTwoTierConfigFull-dim rescore scopeBetter refined ordering, more computeLess compute, potentially weaker refinement
lexical featureCargo featureHybrid retrieval capabilityBetter exact-match precision and fallback pathsSemantic-only behavior
rerank featureCargo featureCross-encoder rerankBetter top-result precision, higher latencyLower latency, less fine-grained top ordering
ann featureCargo featureApproximate nearest-neighbor pathBetter scale behavior at large corpus sizesSimpler exact brute-force behavior

Why Not Just grep/ripgrep/ctags?

grep/ripgrep/ctags are excellent for exact text and symbol lookup. frankensearch solves a different problem: semantic intent search over mixed corpora.

ToolStrong AtLimitation vs frankensearch
grepexact substringsno semantic similarity
ripgrepvery fast regex searchno embedding-based recall
ctagssymbol navigationnot document-level semantic ranking
frankensearch/fsfshybrid semantic + lexical, progressive refinementhigher complexity/runtime footprint

Use both: keep rg for exact matches and use fsfs for intent-level retrieval.

FAQ

Does it run fully local?

Yes. Search/indexing runs on your machine. Network access is only needed for optional alternate-model downloads and update checks.

Can I use only the library and skip fsfs?

Yes. Add frankensearch as a dependency and wire your own app/runtime.

What if the quality model is unavailable?

Search still works using fast-tier and lexical paths; you get RefinementFailed or fast-only behavior.

Which output format should agents use?

Use jsonl for streaming automation and toon if your downstream stack expects TOON semantics.

Is this tied to Tokio?

No. Async/concurrency is built around asupersync and Cx.

Contributing

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.md
  • docs/e2e-artifact-contract.md
  • docs/dependency-semantics-policy.md
  • docs/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)

License

MIT License (with OpenAI/Anthropic Rider). See LICENSE.

Contributors

Dicklesworthstone

4,051 commits

Languages

Rust

97.5%

Shell

2.2%