Introduction
Vectorless, reasoning-based RAG that runs in the browser via WASM.
Most retrieval systems split a document into chunks, embed them, and search for whatever looks closest to your question. This one doesn’t. It builds a table of contents, hands that to an LLM, and lets it pick which section to read, the way a person uses an index.
It runs wherever JS does: the browser through WASM, plus Bun, Node, and Cloudflare Workers.
In a browser the PDF is read and the tree is built on the user’s machine. The file is never uploaded. The only thing that leaves is the slice of text the model asked to read, and it goes to your LLM endpoint. See Browser for what that looks like.
import { buildIndex, ask } from "vectorless-js";
import { pdfToMarkdown } from "vectorless-js/parse";
const doc = buildIndex(await pdfToMarkdown(bytes), "report.pdf");
const { answer, citations } = await ask(doc, "What was Q1 revenue?", "/api/llm");
How it works
PDF
| liteparse, compiled to WASM. Runs on your machine.
v
markdown
| buildIndex: headings become nodes.
v Deterministic, no LLM, free.
tree
| The only smart part. The model reads the table of
v contents and fetches the one section it picks.
answer + citations
A 28-page SEC filing comes to 66 KB of text. The LLM sees 1.6 KB of it: the section titles and the line numbers where they start. It picks a section off that list, then fetches only those lines.
Why vectorless
| Chunk and embed | This |
|---|---|
| chunk, embed, similarity search | build a tree, let the LLM read it, fetch a section |
| chunk boundaries cut through context | the document’s own sections stay intact |
| needs a vector database | the tree is just JSON |
| finds text that looks similar | finds the section that answers the question |
| similarity scores | citations you can trace back |
What it includes
buildIndex builds the tree from Markdown headings. No LLM call, so it is free.
Real documents are less tidy, and buildIndexAuto handles the two ways they go wrong. A PDF may give every heading the same rank, leaving the titles correct and the nesting missing, so an LLM assigns the depths. A contract may have no headings at all, so an LLM infers the outline from the text. Either way the same deterministic builder assembles the tree. How it works walks through both.
Three functions fill the tree out once it exists. addNodeSummaries writes a summary per node, generateDocDescription writes one line for the whole document, and thinTree merges nodes too small to earn their own entry.
ask runs an OpenAI-compatible tool-use loop and hands back an answer plus citations. With more than one document, routeDocuments reads the descriptions and decides which are worth opening, using the same reasoning approach as the rest of the pipeline.