Most RAG pipelines chunk every document, embed every chunk, and pay a vector search on every query β before they've answered anything. treehash parses documents into a tree, hashes every node's address, and resolves most queries by walking straight to the answer. Ingest costs $0 in embedding calls, verified by making the network call itself fail if attempted β not asserted.
Chunk the corpus, embed every chunk, store every vector, re-embed on every edit β that's the fixed cost of a conventional pipeline before "hello world" runs once. treehash's structural resolver never embeds anything at ingest; the optional semantic tier only computes vectors per-query, on demand, and throws them away.
A conventional RAG pipeline chunks every document, embeds every chunk, and pays a similarity search over every vector for every query. treehash resolves structure first.
Sections, paragraphs, tables, table cells, images, lists β not a flattened text blob. Every node's address (WHERE) hashes separately from its content (WHAT), enabling content-addressed dedup across documents.
"revenue in Q1 2025", "the third chapter", "section 4" resolve to an address directly via a rule-based resolver β a stated reason and confidence, or nothing, so the caller can fall back honestly instead of guessing.
Only when nothing resolves does BM25/FTS5 rank text β at section/document granularity, never one vector per paragraph. Pure Python math (mathlib.py), not a library import.
Every "typical pipeline" column below is the honest, unglamorous default β a standard chunker, a standard embedding call, a standard vector store β not a strawman. See benchmarks/ingest_cost/ and BENCHMARKS.md to run the comparison yourself.
| treehash | typical vector-RAG | |
|---|---|---|
| Ingest cost (1M elements) | $0 (verified) | ~$0.22β$1.75 in embedding calls (estimate) |
| Ingest time (1M elements, in-memory) | 7.34s | network round trips per embedding batch |
| Required dependencies | 0 | embedding client + vector-DB client, minimum |
| Structural-lookup latency at scale | flat, O(1)-average to 1M elements | grows with index size |
| What an answer cites | a real address + resolution method | a cosine-similarity score |
| Re-embedding cost on a document edit | only the changed node re-hashes | re-chunk and re-embed the affected region |
No model downloads, no API key needed to use the core engine, no sklearn/torch/numpy to even get started. Published on PyPI β this is a real, working install, not a "coming soon."
from treehash import KnowledgeBase kb = KnowledgeBase(domain="finance") # or "medical", "education", or None kb.add_document("10k_2025", filing_text) result = kb.query("What was revenue in Q2 2025?") print(result.method) # "structural" β resolved by address, not search print(result.hits[0]["content"]) # "6.1M" ctx = kb.context_for_llm("What was revenue in Q2 2025?") print(ctx["estimated_tokens"]) # a handful of tokens, not a whole page
Feed context_for_llm()'s output to your own LLM call, or let kb.ask(query, api_key=..., provider="anthropic") do it and hand back the provider-reported token counts β so any reduction shows up as a real number on your bill, not an estimate.
| Extra | Unlocks |
|---|---|
| pip install treehash-rag | core engine β markdown & HTML parsing, both storage backends, BM25 fallback |
| [docx] | Word document parsing (python-docx) |
| [pdf] | PDF parsing incl. tables, images & captions (pdfplumber) |
| [server] | FastAPI HTTP server with auth, rate limiting, request logging |
| [langchain] / [llamaindex] | drop-in retriever adapters for either framework |
| [anthropic] / [openai] | kb.ask() β bring your own key, get real provider-reported token usage; [openai] also unlocks the optional semantic fallback tier |
| [redis] | multi-process rate limiting (TREEHASH_REDIS_URL) β only needed if you set it |
| [postgres] | PostgresBackend β a storage backend reachable from multiple machines, not just one SQLite file |
| [mcp] | the MCP server β plug a kb into Claude, LangGraph, or any MCP-speaking agent host as a tool |
| [all] | everything above |
Every claim on this page is backed by a script anyone can re-run β that's DISCLOSURE_STANDARD.md's whole job. The clearest proof isn't a number β it's watching the number get worked on in public. Three consecutive rounds: find the highest-leverage real gap, fix exactly that, verify zero regression at the individual-case level before keeping it.
A query asking about two sections at once resolved only the first β confidently, with no sign anything was missing. Fixed by collecting every reference in a query, not just the first.
"sction 2" matched nothing β not close enough for BM25 either. Fixed with a narrow edit-distance-1 correction for known anchor words only, tried after exact matching already failed.
"the section discussing limitations and alsoβ¦" scored both halves as one phrase. Fixed by splitting on an explicit connector before any branch runs, resolving each half independently.
Blended accuracy across all 1,104 cases moved 84.4% β 92.0% over the three rounds above, on a bank where every generated case's expected answer traces back to a fact a human already verified β never invented. Full methodology and numbers in accuracy_cases/ITERATION_LOG.md.
A cosine-similarity hit names a nearest neighbor. This names an address β inspectable, hash-chained, exportable as evidence.
answer = kb.ask("What was revenue in Q2 2025?", api_key="sk-...") # recorded automatically ok, bad_index = kb.audit_log.verify() # hash-chain integrity check kb.audit_log.export_csv("audit-trail.csv") # for a human reviewer report = kb.cost_savings_report(input_cost_per_1k=0.15) # real, provider-billed tokens
ask() call is recorded with its literal source addresses, resolution method, confidence, answer, and real provider-billed token usage.cost_savings_report() comes from real provider-billed tokens; the baseline-cost half is honestly labeled as an estimate, since that path is never actually run against a real API.examples/tenk_analysis_audit_trail.py.Deterministic address resolution fits a tool-calling agent better than a human typing a question. treehash exposes that directly as MCP tools.
# pip install treehash[mcp] python3 -m treehash.mcp_server # stdio transport β plug into Claude, LangGraph, any MCP host
get_section, get_table_cell, get_by_address bypass the natural-language resolver entirely, calling the tree/hash index directly. Deterministic by construction, not by a query happening to match a regex.query (natural language) stays available as a fallback for exploratory use with no known address β but it isn't the primary surface here the way it is for a human chatting.ask() is deliberately not exposed as a tool β the calling agent already is the LLM; it doesn't need this to call another one, and a tool that accepts an API key is a credential smell for no benefit.TREEHASH_DATA_DIR/sharding/audit-log config as the HTTP server β an MCP client and Server mode can point at the same data.Every number below was measured, including the ones that don't flatter the project. See benchmark.py, benchmarks/financebench/, and benchmarks/ingest_cost/ to reproduce any of these yourself β the methodology itself is written out as a checklist in DISCLOSURE_STANDARD.md.
| Metric | treehash | naive per-chunk baseline |
|---|---|---|
| Accuracy (hit rate) | 48/48 Β· 100% | 16/48 Β· 33% |
| Blended token reduction | 11.0% | |
| Exact/targeted-lookup reduction | 98β99% | |
| Indexed units | 36 (section-level) | 65 (per-paragraph) |
| Variant type | Hit rate |
|---|---|
| case_punct (case/punctuation noise) | 240/240 Β· 100.0% |
| paraphrase | 252/258 Β· 97.7% |
| synonym substitution | 215/240 Β· 89.6% |
| typo | 221/254 Β· 87.0% |
| multi_hop (two facts, one query) | 81/100 Β· 81.0% |
| vague / underspecified (n=12, below target β see Limitations) | 7/12 Β· 58.3% |
| Blended | 1,016/1,104 Β· 92.0% |
| Corpus size | treehash ingest | treehash $ cost | baseline $ estimate |
|---|---|---|---|
| 100,000 elements | 0.567s | $0.00 (real) | $0.02β$0.17 |
| 1,000,000 elements | 7.34s | $0.00 (real) | $0.22β$1.75 |
BENCHMARKS.md rather than quietly patched. Nobody asked us to test a million elements; we did it anyway, and it found something.
| input tok | output tok | cost | correct | wrong | narrative | |
|---|---|---|---|---|---|---|
| treehash | 4,052 | 1,160 | $0.0013 | 0/25 | 4 | 21 |
| baseline | 4,317 | 1,284 | $0.0014 | 0/25 | 4 | 21 |
page.close(): 69MB, 22Γ smaller.busy_timeout, and BEGIN IMMEDIATE; verified with a real subprocess-based test.TREEHASH_API_KEYS), so one tenant's key can't read another tenant's kb; a single shared token remains a fallback for kbs with no per-kb keys configured.StorageBackend reachable from multiple machines, not just one SQLite file. Found and fixed a real concurrency bug (schema creation races under concurrent first-open) and two round-trip-reduction fixes; its own latency numbers are reported as directional, not a tight bound β see BENCHMARKS.md.In order. The most recent phases are a self-directed loop: expand the test bank, measure, fix the single highest-leverage gap, verify zero regression, repeat.
Zero-dependency core, persistent SQLite backend, three domain packs, multi-format parsing, LangChain/LlamaIndex adapters, an HTTP server.
Added FinanceBench: real SEC 10-K/10-Q filings, real GPT-4o-mini calls, provider-reported token usage β not estimated.
Font-size-only heading detection found 1 section in a real 121-page 10-K; rewritten to combine size, boldness, and SEC pattern matching β 9 sections, 497 subsections. Fixed real multi-process SQLite write loss. Server hardened.
A real 13MB/248-page report used 1.5GB peak RSS to parse. Fixed: 69MB, a 22Γ reduction. Validated against ten real documents and a 110MB combined-PDF stress test.
Bare table-cell values ("21.6") were losing context-ranking to verbose but useless text β labeled with row/header context. Added lightweight stemming.
Added image + caption extraction to both parsers. A hybrid table-detection strategy looked promising on one document, failed on several others, and was documented as not shipped.
Built a free, offline recall checker (no LLM call) and found: a domain alias word matching entire documents' worth of tables; a thin stopword list; a redundant ancestor/descendant ranking bug.
Re-ran the real, billed FinanceBench suite to confirm the free checker's finding wasn't an artifact of its own grading logic. It wasn't.
Investigated a claimed FinanceBench cost regression β the premise was stale; treehash already used fewer tokens. Built a 48-case hand-verified adversarial set (paraphrase/typo/multihop/vague) and found a real accuracy gap. Found and fixed a table-lookup that had quietly stopped being O(1) at scale.
pip install treehash-rag went from "reserved but never published" to actually working β verified in a brand-new virtualenv against the real PyPI index, not local source.
Set out to measure treehash's $0 ingest cost against a chunk-and-embed baseline; building the 1M-element test case exposed a quadratic-scaling bug in MemoryBackend. Fixed: 121s β 7.34s.
Mechanically expanded the 48 hand-verified facts into a >=1,000-case tagged bank. Found and fixed a multi-section resolver gap it surfaced.
Typo-tolerance for anchor words, then a multi-hop connector split β each verified with zero case-level regressions before keeping it. Version bumped and republished to PyPI the same day.
Stated plainly rather than scoped away β every number on this page is something that was actually measured.
vague/underspecified query category still lags (58.3%, n=12) β the smallest, least statistically trustworthy category in the adversarial bank, and the only one below this project's own 0.75 per-category bar. Growing it into a generated category, the way the other five already are, is the honest next step.