Binary-quantized embedding search over text files or whole directories.
This is a Cargo workspace, split so the reusable engine and the CLI are separate things:
crates/binsg-core— the actual engine: the.bsgfile format, quantization, the incremental directory indexer, and the parallel Hamming-distance search. No dependency on candle, HTTP, or any specific model — it works against anything implementing theTextEmbeddertrait.crates/binsg-candle— aTextEmbedderimplementation backed by a local BERT/MiniLM model viacandle, including the zero-setup auto-download.crates/binsg-cli— thebinsgcommand-line tool, a thin wrapper gluing the two together withclap.
If you just want the CLI, none of this changes how you use it. If you
want to embed binary-quantized search into your own Rust program —
possibly with your own embedding backend instead of MiniLM — depend on
binsg-core directly; see below.
You don't need to fetch the model yourself — the first index or
search call downloads it automatically into --model-dir (default
./model) if it's missing, then never touches the network again.
cargo build --release
cargo run --release -- index ./my-notes --output notes.bsg
cargo run --release -- search "how do I reset my password" notes.bsgFirst run prints a one-time "fetching..." line per file while it grabs
config.json, tokenizer.json, and model.safetensors (~90MB total,
sentence-transformers/all-MiniLM-L6-v2) from HuggingFace. Every run
after that is fully offline.
If you're behind a TLS-intercepting proxy (common on corporate/enterprise
networks) and the download fails with a certificate error, that's what
the native-certs feature on ureq (in binsg-candle) is for — it
makes the downloader trust your OS's certificate store instead of only
a bundled public root list, the same way curl already does. It's on by
default in this repo.
index accepts either a single file or a directory:
cargo run --release -- index ./my-project --output project.bsgDirectories are walked recursively using the same gitignore-aware walker
ripgrep uses — anything your .gitignore excludes is skipped
automatically, along with anything that looks binary (a NUL byte in the
first few KB).
Re-running index against an output file that already exists is an
update, not a rebuild: each file's content is hashed, and only files
whose hash changed since the last run are actually re-embedded. If
nothing changed at all, the embedder is never even constructed, so a
no-op re-index costs milliseconds, not a model load.
cargo run --release -- index ./my-notes --output notes.bsg # first run: embeds everything
echo "one more line" >> my-notes/todo.txt
cargo run --release -- index ./my-notes --output notes.bsg # second run: re-embeds only todo.txtcargo run --release -- search "how do I reset my password" notes.bsg --top-k 5Output is grep-style: similarity path:line: text, most similar
first. Similarity is 1 - hamming_distance / dim_bits, a proxy for
cosine similarity between the original float embeddings — treat it as a
ranking signal, not a calibrated probability.
Depend on binsg-core directly to get the file format, indexer, and
search without the CLI or candle:
[dependencies]
binsg-core = { path = "path/to/binsg/crates/binsg-core" } # or a version, once publisheduse binsg_core::{TextEmbedder, build_index, search_top_k, format::BsgFile, quantize};
struct MyEmbedder; // wire up to whatever produces your embeddings
impl TextEmbedder for MyEmbedder {
fn dim(&self) -> usize { 384 }
fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
todo!("call your own model or API here")
}
}
let stats = build_index(&input_path, &output_path, || Ok(MyEmbedder))?;
let bsg = BsgFile::open(&output_path)?;
let query_bits = quantize(&MyEmbedder.embed("some query")?);
let results = search_top_k(&bsg, &query_bits, 10); // Vec<(entry_index, hamming_distance)>binsg-candle is one TextEmbedder implementation, not the only one
that could exist — swap in a remote API call, a different local model,
whatever fits.
.github/workflows/release.yml builds binsg for Linux, macOS (Intel
and Apple Silicon), and Windows on every pushed version tag (v*) and
attaches each as a downloadable asset on the GitHub Release. Once a repo
exists and a tag is pushed:
git tag v0.1.0
git push origin v0.1.0...GitHub Actions builds and attaches the binaries automatically. Note:
this workflow is written and its build steps verified locally, but the
actual CI run (particularly the Windows build of tokenizers' onig C
dependency) hasn't been exercised on real GitHub Actions runners yet —
worth watching the first tagged run.
packaging/homebrew/binsg.rb is a formula stub that builds from source
via cargo install. It has placeholder url/sha256 fields — fill
those in once a real tagged release exists (the sha256 is the hash of
that release's source tarball), then host it in a
<you>/homebrew-binsg tap repo.
First build compiles candle and its gemm kernels from scratch (a
couple of minutes); incremental builds after that are fast. Both debug
and --release builds were verified to complete successfully.
Cargo.lock pins half to 2.4.1 — at the time this was built, the
newest published half had drifted to a rand_distr version
incompatible with the one candle-core itself expects, surfacing as a
Distribution<f16> trait error. If you ever regenerate the lockfile and
hit that, re-pin with:
cargo update -p half --precise 2.4.1cargo test -p binsg-core covers, entirely offline (no model, no
network): the binary format (quantization, packing, multi-file
round-trip, corruption detection), content hashing, the parallel search
ranking, and the incremental indexer's actual decisions (first-run
embeds everything, an unchanged re-run reuses everything and never
constructs an embedder, a partial change re-embeds only the changed
file, deleted files drop out, .gitignore-excluded and binary-looking
files are skipped) — using a fake embedder so this runs in milliseconds.
The full pipeline — directory walk, .gitignore handling, incremental
reuse, and the auto-download (from both an empty and a
partially-populated model directory) — was additionally verified
manually against the real MiniLM model, both before and after the
workspace split into binsg-core/binsg-candle/binsg-cli.