PyPI version MIT license 0 required dependencies 189 tests, 0 failing

Stop embedding what you can just address.

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.

92.0%
accuracy across 1,104 adversarial cases
$0
verified ingest cost, any corpus size
189
tests, 0 failing
0
required dependencies
v1.0.1
on PyPI, right now
document section section section table table table row table_cell β†’ "12,645" AH_f746c4f6e5766c69
one query, one path, one address hash β€” resolved, not searched
The bill nobody checks

A vector database bills you before it's answered a single question

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.

$0
ingest cost β€” verified by making the network call raise, not just claimed
0
required dependencies β€” no torch, no sklearn, no vector-DB client
O(1)
average-case lookup for a resolved address, measured flat to 1M elements
100%
of answers cite a real address β€” never a similarity score standing in for one
Why vectorless

Three moves instead of one similarity search

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.

01

Universal element tree

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.

02

Resolve, don't search

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

03

Honest fallback

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.

Side by side

treehash vs. a typical chunk-and-embed pipeline

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.

treehashtypical vector-RAG
Ingest cost (1M elements)$0 (verified)~$0.22–$1.75 in embedding calls (estimate)
Ingest time (1M elements, in-memory)7.34snetwork round trips per embedding batch
Required dependencies0embedding client + vector-DB client, minimum
Structural-lookup latency at scaleflat, O(1)-average to 1M elementsgrows with index size
What an answer citesa real address + resolution methoda cosine-similarity score
Re-embedding cost on a document editonly the changed node re-hashesre-chunk and re-embed the affected region
Quickstart

Install it and it runs

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

install
pip install treehash-rag
usage
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.

How it works

What each module actually does

resolver.pytree+hash structural matching
engine.pyKnowledgeBase, query orchestration
storage.pyMemory / SQLite (WAL) / Postgres backends
sharding.pykb-name to backend, stable-hash routing
tree_index.pythe address-hash tree itself
mathlib.pyhand-rolled BM25 / TF-IDF fallback
embeddings.pyoptional 3rd tier: embeddings, off by default
querylog.pyfallthrough-query log + frequency analysis
audit.pyhash-chained audit trail for every ask() call
parsers/*.pymarkdown, html, docx, pdf β†’ elements
domains/*.pyfinance, medical, education vocab β€” open for contribution, validated + contract-tested
server.pyFastAPI + per-KB auth + rate limiting
mcp_server.pyMCP tool wrapper β€” structured query API for agents
llm.pybring-your-own-key LLM bridge
integrations/*.pyLangChain / LlamaIndex retrievers
Install extras β€” the core has zero required dependencies; everything else is opt-in
ExtraUnlocks
pip install treehash-ragcore 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
The moat is the receipts

We publish our own bugs. Most projects don't.

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.

Iteration 1

Multi-section queries silently dropped the second fact

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.

49.0%β†’ 62.0%
189/189 tests, 0 case-level regressions
Iteration 2

A typo on the anchor word broke matching outright

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

66.5%β†’ 87.0%
Zero cases flipped OK→MISS, 52 flipped MISS→OK
Iteration 3

A title-keyword reference swallowed a second fact's text

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

62.0%β†’ 81.0%
Zero cases flipped OK→MISS, 19 flipped MISS→OK

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.

The auditable answer trail

An address, a method, a confidence β€” not a similarity score

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
Retrieval infra for agents, not just chat

An agent already knows what it wants β€” give it an address, not a guess

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
Benchmarks

Three suites β€” synthetic at scale, adversarial at scale, and real filings under real spend

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.

Synthetic corpus β€” generic / medical / finance / education, 48 labeled queries
Metrictreehashnaive per-chunk baseline
Accuracy (hit rate)48/48 Β· 100%16/48 Β· 33%
Blended token reduction11.0%
Exact/targeted-lookup reduction98–99%
Indexed units36 (section-level)65 (per-paragraph)
Adversarial bank β€” 1,104 mechanically-expanded cases, every answer traced to a hand-verified fact
Variant typeHit rate
case_punct (case/punctuation noise)240/240 Β· 100.0%
paraphrase252/258 Β· 97.7%
synonym substitution215/240 Β· 89.6%
typo221/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%
Blended1,016/1,104 Β· 92.0%
Ingest cost β€” treehash (real, verified) vs. a naive chunk-and-embed baseline (published-pricing estimate)
Corpus sizetreehash ingesttreehash $ costbaseline $ estimate
100,000 elements0.567s$0.00 (real)$0.02–$0.17
1,000,000 elements7.34s$0.00 (real)$0.22–$1.75
That 7.34s used to be 121s. Building this exact benchmark surfaced a real quadratic-scaling bug in the in-memory backend β€” fixed, verified against the full test suite and both accuracy sets, and disclosed in BENCHMARKS.md rather than quietly patched. Nobody asked us to test a million elements; we did it anyway, and it found something.
FinanceBench input-token reduction vs baseline, by round Round 1: -15.7%. Round 2: +6.5%. Round 3: +5.6%. Round 4: +6.1%. All measured on the same 25-question real-filing sample with real API-billed tokens. 0% Round 1 βˆ’15.7% Round 2 +6.5% Round 3 +5.6% Round 4 +6.1%
worse than naive baseline better than naive baseline current
Latest paid confirmation β€” 25 questions, 7 real 10-K filings, gpt-4o-mini
input tokoutput tokcostcorrectwrongnarrative
treehash4,0521,160$0.00130/25421
baseline4,3171,284$0.00140/25421
The "4 wrong" are honest declines, not hallucinations. All 4 are multi-year computed-metric questions (3-year average margins, YoY revenue change, balance-sheet PP&E). Both conditions answered "the provided context does not include the specific figures needed" rather than guessing a number β€” the auto-grader can't tell that apart from a confidently wrong answer, but the raw transcripts can.
Hardened for real use

What real documents and real traffic actually broke

Build log

Every phase, found through testing β€” not assumed and patched in the abstract

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.

  1. 1

    Productized rewrite 9b4033c

    Zero-dependency core, persistent SQLite backend, three domain packs, multi-format parsing, LangChain/LlamaIndex adapters, an HTTP server.

  2. 2

    Real-world benchmark harness 34449e6

    Added FinanceBench: real SEC 10-K/10-Q filings, real GPT-4o-mini calls, provider-reported token usage β€” not estimated.

  3. 3

    PDF headings, concurrency, hardening 41ffe9c

    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.

  4. 4

    Memory and capacity 2b69aad

    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.

  5. 5

    Accuracy investigation 2fc93b7

    Bare table-cell values ("21.6") were losing context-ranking to verbose but useless text β€” labeled with row/header context. Added lightweight stemming.

  6. 6

    Images, tables, and a rejected idea 5c68dfb

    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.

  7. 7

    Three real retrieval bugs c580b0e

    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.

  8. 8

    Paid confirmation 6885a3b

    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.

  9. 9

    Cost gap, adversarial set, a real O(n) bug d053abc

    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.

  10. 10

    Published to PyPI 07e53a6

    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.

  11. 11

    Ingest cost, and a real O(nΒ²) bug 4b4af68

    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.

  12. 12

    1,104-case accuracy bank, iteration 1 21396d8

    Mechanically expanded the 48 hand-verified facts into a >=1,000-case tagged bank. Found and fixed a multi-section resolver gap it surfaced.

  13. 13

    Two more fixes, v1.0.1 published 4a5e4ee

    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.

Open, honestly

What's still unsolved

Stated plainly rather than scoped away β€” every number on this page is something that was actually measured.