vstash
Local document memory with hybrid retrieval. Single SQLite file. Zero cloud dependencies for search. Beats ColBERTv2 on 5/5 BEIR datasets with the tuned bge-small-rrf-v3 model. ~50 ms p50 at 50K chunks on Apple Silicon with the optional snapvec backend (~73 ms on the default sqlite-vec for dense corpora).
pip install vstash
vstash add paper.pdf notes.md https://example.com/article
vstash search "what's the main argument?"
Retrieval Quality
| Dataset | Docs | vstash (v3) | ColBERTv2 | BM25 | Δ vs ColBERTv2 |
|---|---|---|---|---|---|
| SciFact | 5.2K | 0.9361 | 0.693 | 0.665 | +0.243 |
| NFCorpus | 3.6K | 0.3927 | 0.344 | 0.325 | +0.049 |
| SciDocs | 25.7K | 0.3693 | 0.154 | 0.158 | +0.215 |
| FiQA | 57.6K | 0.7506 | 0.356 | 0.236 | +0.395 |
| ArguAna | 8.7K | 0.7540 | 0.463 | 0.315 | +0.291 |
Absolute NDCG@10 on BEIR via the full production retrieval pipeline (RRF hybrid + adaptive weights + MMR dedup + IDF, 2026-04-19). Tuned model: Stffens/bge-small-rrf-v3 (33M params, 384d). v3 beats ColBERTv2 on 5/5 BEIR datasets and improves macro NDCG@10 by +0.016 absolute over bge-small-rrf-v2 (0.6405 vs 0.6246). Training-time eval uses a batched path that skips MMR/IDF for speed; absolute NDCG@10 differs by a few percent vs the production numbers above, but baseline-vs-final deltas are preserved. See experiments/results/v2_v3_head_to_head.json for the full table (reproduce via python -m experiments.v2_v3_head_to_head) and the methodological note in experiments/hypotheses.md for the pipeline-shift caveat.
How It Works
Query --> Embed --+--> Vector ANN (sqlite-vec) --+
| +--> Adaptive RRF --> MMR Dedup --> Results
+--> FTS5 BM25 ----------------+
- Hybrid search: vector + keyword, fused via Reciprocal Rank Fusion.
- Adaptive RRF: IDF-based per-query weights. Rare terms boost keywords, common terms boost vectors.
- MMR dedup: diverse sections from long documents, not redundant chunks from one.
- Self-tuned, gated:
vstash retrainfine-tunes embeddings from your own disagreement signal; the eval gate refuses regressions.
Install
pip install vstash # SDK + search
pip install 'vstash[ingest]' # + PDF, DOCX, PPTX parsing
pip install 'vstash[serve]' # + web UI (vstash serve)
pip install 'vstash[all]' # everything
Usage
# Ingest: files, folders, URLs
vstash add report.pdf ~/notes/ https://arxiv.org/abs/2310.06825
# Search: local, no API key
vstash search "what is the proposed method?"
# Ask: needs a local LLM, auto-detects Ollama / LM Studio
vstash ask "summarize the key findings"
vstash chat # interactive
# Fine-tune on your own corpus (eval-gated, refuses regressions)
vstash retrain
vstash reindex --model ~/.vstash/models/retrained
Python SDK
from vstash import Memory
mem = Memory(project="my_agent")
mem.add("docs/spec.pdf")
mem.remember("OAuth uses PKCE for public clients", title="auth-notes")
results = mem.search("deployment strategy", top_k=5)
for r in results:
print(r.text, r.score, r.collection, r.tags, r.added_at)
answer = mem.ask("What are the system requirements?")
Commands
vstash add <file/dir/url> Add documents to memory
vstash remember "<text>" Ingest text directly
vstash search "<query>" Semantic search (free, local)
vstash ask "<question>" Answer from your documents (needs LLM)
vstash chat Interactive Q&A
vstash list Show all documents
vstash stats Memory statistics
vstash forget <file> Remove a document
vstash retrain Fine-tune embeddings on your data
vstash reindex Re-embed with a new model
vstash watch <dir> Auto-ingest on file changes
vstash serve Web UI on localhost
vstash check [--repair] Integrity check and repair
vstash config Show configuration
vstash profile <cmd> Manage named profiles
vstash journal <cmd> Cross-session agent memory
MCP Server
16 tools for Claude Desktop, Claude Code, Cursor, or any MCP client:
vstash-mcp # start MCP server
{
"mcpServers": {
"vstash": {
"command": "vstash-mcp"
}
}
}
Self-Supervised Embedding Refinement
vstash can tune its own embedding model to your corpus, without any human labels.
vstash retrain # generate training pairs + fine-tune
vstash reindex --model ~/.vstash/models/retrained
How it works, in one paragraph. When you search your corpus, the vector and keyword halves of the pipeline sometimes rank different documents at the top. Those disagreements are a free signal: the document each half picked is probably relevant, the one only one half picked might not be. vstash turns this into training pairs and fine-tunes the embedding model on them. The run is eval-gated: it evaluates the candidate against the base model on a held-out slice of your corpus and refuses to save a model that performs worse.
The feature is maturing fast. Each release tightens the recipe, lifts the measured numbers, and adds infrastructure that keeps the next iteration honest:
| Release | Training recipe | 5-dataset BEIR macro NDCG@10 | What landed alongside |
|---|---|---|---|
base bge-small |
no fine-tune | 0.6118 | reference |
rrf-v2 |
76k triples, ad-hoc scripts | 0.6246 | first paper-grade result; still the NFCorpus specialist |
rrf-v3 |
60k triples via retrain-multi CLI, temperature=0.5, eval gate |
0.6405 | H-R9 ablation picked the config empirically; H-R7 seeded RNGs make it reproducible; H-R5 reports NDCG@3 + Recall@100 so regressions are visible before they ship |
Both v2 and v3 beat ColBERTv2 on 5/5 BEIR datasets under the current pipeline. v3 improves macro by +0.016 over v2 (+2.6% relative), with the largest per-dataset gain on FiQA (+0.097 absolute). It is a trade, not a strict upgrade: v3 gives up ~0.040 NDCG@10 on NFCorpus vs v2 in exchange for the FiQA and SciFact wins. v2 remains the better pick for keyword-heavy / biomedical corpora where NFCorpus-style retrieval dominates; v3 is the recommended default for everything else. The eval gate also catches losers: hypothesis H-R3 (hard-negative margin filter) regressed macro -2.49pp, the candidate was refused, the branch was closed without merging. The pipeline's job is to refuse bad models, and it does.
See the Retrieval Quality table, docs/retrain.md for the full recipe and per-version breakdown, and experiments/results/v2_v3_head_to_head.json for reproducible numbers.
Requires sentence-transformers, torch, and accelerate:
pip install 'sentence-transformers>=3' torch 'accelerate>=1.1.0'
Privacy
| Component | Data leaves machine? |
|---|---|
| Embeddings (FastEmbed) | Never |
| Search (sqlite-vec + FTS5) | Never |
| Inference (Ollama/LM Studio) | Never |
| Inference (Cerebras/OpenAI) | Yes (query + context sent to API) |
Search is always private. Use a local LLM for fully private answers.
Paper
vstash: Local-First Hybrid Retrieval with Adaptive Fusion for LLM Agents
Adaptive RRF, self-supervised embedding refinement, a negative result on post-RRF scoring, and the production substrate all in one place. PDF build at paper/arxiv/vstash.pdf.
Documentation
| Guide | Description |
|---|---|
| How It Works | Search pipeline, chunking, RRF |
| Configuration | Full TOML reference |
| Embedding Models | Model comparison, vstash retrain |
| MCP Server | 16 tools for LLM agents |
| Experiments | BEIR benchmarks, ablations |
Experiments
| Experiment | Key Result | Command |
|---|---|---|
| BEIR Benchmark | With bge-small-rrf-v3 (current default): 5/5 BEIR datasets beat ColBERTv2. With -rrf-v2 (previous): 4/5 under this script's historical pipeline. See Retrieval Quality for the v3 numbers. |
python -m experiments.beir_benchmark --no-chroma |
| Retrain (eval-gated) | Fine-tune your embedding model on your own corpus, refuses regressions | vstash retrain --help |
| Pipeline latency | At N=50K real BGE corpus on M4 Pro: sqlite-vec 73 ms, snapvec 53 ms, snapvec-ivfpq 51 ms p50; identical NDCG@10 (~0.716) | python -m experiments.vstash_pipeline_ivfpq_bench --n 50000 |
| Relevance Signal | F1=0.996 cross-domain | python -m experiments.relevance_signal_beir |
What's New in v0.39
- Cross-document near-duplicate collapse (v0.39) -- MMR only penalises chunks of the same document, so a corpus holding many near-identical documents (audit logs, mirrored notes, a file ingested repeatedly) could fill every result slot with restatements of one answer.
dedup_threshold=0.95drops a chunk whose cosine similarity to an already-kept, higher-ranked chunk is at or above the threshold, regardless of document. Opt-in: the default leaves ranking untouched. Available onVstashStore.search,Memory.search, MCPvstash_search, and CLIvstash search --dedup 0.95(#509). - Query cache correctness fix (v0.39) -- with
[cache] query_cache_size > 0, a filtered search (filters={"not": {...}}) could be served the cached results of the unfiltered query, because the filter tree was missing from the cache key entirely (#509). - Tier-0 content-hash dedup on
remember(v0.39) -- re-ingesting byte-identical content is now a NOOP instead of a re-chunk plus re-embed (#429). - Atomic content updates + autocommit connection (v0.39) --
Memory.update(text=...)replaces every chunk throughadd_documents_batch, and the SQLite connection runs in autocommit with explicitBEGIN IMMEDIATEso a write lock is never held open across a read (#423, #424, #430). - Frozen result models (v0.39) -- the query LRU cache hands out shared objects, so every result model is now immutable; one caller can no longer corrupt another's cached entry (#427).
- Python 3.13 in CI (v0.39) -- tested on 3.10 through 3.13, matching the classifiers (#431).
What's New in v0.37
Memory.update()— in-place document mutation (v0.37) -- update a stored doc without re-adding it.Memory.update(path, title=..., tags=...)runs a single atomic metadataUPDATE(no re-chunking, no embedding);Memory.update(path, text=...)re-chunks + re-embeds and replaces every chunk while preserving all other metadata. CLIvstash update <path> [--text|--title|--tags](--text -for stdin); MCPvstash_update(#365).Memory.prune()+Memory.compact()(v0.37) -- housekeeping.Memory.prune(before="30d", collection=..., tags=...)deletes filter-scoped documents (an unfiltered call raises rather than wiping everything);Memory.compact(before=...)runs prune + VACUUM + FTS optimize in one pass.beforeaccepts"30d"/"2w"/"24h"or an ISO date. CLIvstash compact; MCPvstash_compact(#366).- Tag filters in search (v0.37) -- pass
tags="alpha"ortags=["alpha", "beta"](OR semantics, comma-anchored soalphanever matchesalphabet) toMemory.search,VstashStore.search, MCPvstash_search, andVstashRetriever.Memory.journal_recallalso gains--after/--beforedate filters. CLIvstash search --tag alpha --tag beta(#106, #364). - MMR dedup speedup (v0.37) -- the dedup hot path now uses swap-pop removal + pre-grouped same-doc siblings, a measured 1.15x–1.19x end-to-end on
store.search()with real BGE-small embeddings. Ranking output is unchanged (#363). - Services-layer architecture (v0.37) -- web / MCP / CLI / SDK now route search and ask through one shared
validate -> embed -> search -> expandpath (vstash/services/), backed by aVstashErrordomain error tree, aVectorBackendProtocol, and a built-in embed-model registry (#326, #327, #328, #330, #334–#336).
What's New in v0.36
chat.ask_full()returningAskResult(v0.36) -- new public API surfaces the reasoning trace and token usage thatask()previously discarded. Cerebrasgpt-oss-120bpopulatesresult.reasoning; Ollama qwen3 thinking-mode usesmessage.thinking; OpenAI-compat servers (vLLM, DeepSeek, Together, xAI Grok, OpenAI o1/o3) readreasoning_content.ask()keeps its-> strcontract via a thin wrapper -- zero call-site change for existing code. Also exposed asMemory.ask_full(). Drives Merken Phase 2 distillation pipeline.- Centralized store construction (v0.36) --
open_store_for_config(cfg)is the single entry point used by CLI, MCP, web, SDK, journal, and federated search. Previously each surface duplicated theStorageConfig -> VstashStorewiring and silently dropped IVFPQ tuning fields on some paths (#297). vec_onlylong-query distance cutoff fix (v0.36) --retrieval_mode="vec_only"now applies the same long-query relaxation ashybrid; ArguAnavec_onlyjumped from NDCG@10 = 0.0013 (1403/1406 zero) to 0.4250. Hybrid mode and all paper / model-card numbers untouched (#304).- Bug fixes (v0.36) --
Memory.add(collection=None)falls back to schema default instead of crashing on the NOT NULL constraint (#296);vstash retrain --synthesize-queriesno longer crashes on Ollama / Cerebras backends (#294); web uploads now persist under~/.vstash/uploads/<uuid>-<safe-name>instead of pointing at deleted temp paths (#295).
What's New in v0.35
bge-small-rrf-lme-v1chat-memory specialist (v0.35) — fine-tuned on 398 labeled LongMemEval queries through the eval-gated retrain loop. +3.79pp R@5 on n=102 holdout vs vanilla BGE-small. Use when your corpus is primarily chat sessions / agent memory.- Eval-gated labeled retrain (v0.35) —
vstash retrain --training-queries train.jsonl --eval-queries eval.jsonlaccepts user-supplied(query, relevant_paths)JSONL and refuses to save fine-tunes that regress NDCG@10 on the holdout. See docs/retrain.md. vstash whymiss analysis (v0.33) — diagnose why an expected document did not surface for a query. Traces vector pool, distance cutoff, FTS match, RRF fusion, MMR, and context-expansion stages with parameter suggestions. Auto-logs misses on empty / low-relevance searches.retrieval_modeenum (v0.33) —Literal["hybrid", "vec_only", "fts_only"] = "hybrid"onMemory.search,Memory.ask,VstashStore.search, and MCP tools.vec_onlyis the symmetric branch tofts_only. Default stayshybrid. Legacyfts_only=Trueboolean was removed in v0.35.- Custom encoder resolver hook (v0.34) —
register_encoder_resolver(fn)lets callers plug LoRA-adapted, locally fine-tuned, or otherwise unnamable encoders into the embed pipeline. See docs/embedding-models.md. - Cosine metric in
vec_chunks(v0.34) — sqlite-vec virtual table now uses cosine distance (was L2 before; v1 DBs migrate in place atomically on first open). Fixed a latent bug where non-unit-normalized models silently mis-ranked. - Persistent embedder daemon (v0.32) —
vstash serve --warmpre-loads the embedding model and exposes/api/embedonlocalhost:8585. CLI and SDK clients auto-detect and delegate; cold start drops from ~2 s to ~5 ms.
See CHANGELOG for full version history.
Metadata
Release files for vstash 0.39.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vstash-0.39.0.tar.gz | 790.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vstash-0.39.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / vstash-0.39.0.tar.gz
| Download URL | vstash-0.39.0.tar.gz |
|---|---|
| Size | 790.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b19f183b8a3cb0ca4a36f6b0f96cc53fa01f1e13deb0c94ed2df4fa1774de61e
|
|
BLAKE2b-256 checksum How to use checksums |
942747ef371c9e17f52c10389e9f490067e025d69d1acf48420e09eb5b5a992d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency logRelease files / vstash-0.39.0-py3-none-any.whl
| Download URL | vstash-0.39.0-py3-none-any.whl |
|---|---|
| Size | 282.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
140a9fd4c566c7315b44cf0031c41c48b4cd15ef44a7b819066a17319181d0cd
|
|
BLAKE2b-256 checksum How to use checksums |
d96583f0716cc48b7f4f08bc1b62a98c51ac24d9355d3537f7f539931d902046
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency log