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.1%
accuracy across 1,108 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 this project's own disclosure standard'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,108 cases moved 84.4% β†’ 92.1% over the three rounds above, plus a new multi_hop_computed category (4/4), 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
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.

  14. 14

    Multi-hop computed-metric resolution

    Instrumented real misses directly and found 67% of numeric FinanceBench questions have no verbatim answer at all β€” they're computed metrics. Built a new resolver that finds raw values by address and computes the answer deterministically; real-filing recall 7.9% β†’ 9.2%, verified against a real example matching FinanceBench's own published answer.

  15. 15

    Fixed a real dedup-ranking bug; one algorithmic experiment tried and reverted

    Found and fixed a genuine logic bug: candidate ranking dropped a high-scoring section any time a near-zero-score descendant was also present, with no regard for relative scores β€” now score-aware. Zero regression, reported plainly even though this specific test set showed no measured movement. Separately, tried swapping piece-selection to full BM25 reasoning it would help β€” it measured worse on real filings (9.2% β†’ 5.6%) and was reverted the same day, not shipped on theory.

Actively working on

What we're improving next

Real numbers, tracked in the open β€” see benchmarks/financebench/README.md for the full trace behind each one.