From a 10-K filing to an answer
A technical summary of how the system works and what it showed, then the same story in five plain steps, then the detail.
Technical summary
How retrieval-augmented generation works here, and what the benchmarks taught about chunking. Numbers come from the live benchmarks.
This is a retrieval-augmented generation (RAG) system over 250 real SEC 10-K filings. Each filing is parsed into a tree of sections, paragraphs, tables and sentences, cut into 58,926 passages of at most 256 tokens, and each passage is embedded as a 384-number vector with the open gte-small model. A question is spell-corrected against the corpus vocabulary, embedded with the same model, and matched against every passage by meaning, by exact words, or both; the best passages, within a reading budget, go to Claude, which answers citing them. The project's real subject is the middle step: how the chunking and search choices decide what evidence the model ever gets to see, measured against questions with known answers.
1. How it works, end to end
- Ingest. 10-Ks from SEC EDGAR, parsed to canonical text with Items 1, 1A, 7 and 7A kept, every heading, paragraph, table and sentence boundary recorded.
- Chunk. Structure-aware chunks: never cross a section, pack whole paragraphs, split long ones on sentences, at most 256 tokens counted by the embedding model's own tokenizer.
- Embed. gte-small (384 dimensions) offline in a GitHub Actions job; the same model runs in a Supabase Edge Function for questions (vectors agree at 0.998 cosine).
- Store. Postgres with pgvector (half-precision vectors) plus a full-text index, on Supabase's free tier.
- Retrieve. Exact vector search over every passage (~0.1 s at this size), all-words keyword search, or both fused by reciprocal rank with keywords weighted 1.5×.
- Generate. The top passages, optionally with their neighbours or section, within a token budget, go to Claude with an instruction to answer only from them and cite each claim.
Questions that need every company side by side (“who has the best margin?”) are answered from the companies' reported XBRL figures instead, because no passage of text contains that comparison.
2. What RAG is, and where it actually fails
A language model can only cite what retrieval hands it. If the right passage is not retrieved, the best model in the world cannot answer, and it may answer anyway from what it was given. So retrieval quality sets the ceiling for answer quality.
Measured here: in every configuration the right company's filing was in the top five for every question. The misses were never “wrong company”; they were “right filing, wrong passage”. That is precisely the part chunking controls: what a passage is, and therefore what can be matched and handed over.
3. Chunk types
- Fixed-size windows cut every N tokens regardless of content. Simple and predictable, but 96% of their cuts land mid-sentence and at 128 tokens 16% of answers were split across two passages.
- Structure-aware follows the document: sections, then paragraphs, then sentences. Mid-sentence cuts fall to 1% and every answer stayed in one passage.
- Also built, not yet benchmarked: recursive splitting, sentence packing, semantic breakpoints (cut where meaning shifts) and hierarchical units (retrieve small, expand to the parent section).
Size is a separate axis. Smaller chunks give a sharper match and cost less to read; larger chunks keep more context but their vectors average several topics and the match blurs.
4. Single-sentence answers (48 questions)
Best overall: structure-aware 256, answer in the top five 48% of the time while reading 895 tokens; the best fixed-size setup needed 2,400 tokens for the same rate.
Structure-aware beat fixed-size by 7 points on average at equal size, most at small sizes (48% against 33% at 128 tokens). With the reading budget held at 2,000 tokens, structure-aware 256 found 60%, the most of any setup.
Lesson: for fact look-ups, cut small and along the document's structure. Bigger chunks only win when you let them send much more text.
5. Multi-sentence answers (26 questions)
These answers need two to four consecutive sentences, and the bar is higher: the whole answer has to reach the model. Best from the top five passages alone: structure-aware 256 (54% of answers complete). Adding each result's neighbouring passages (“find small, read big”) raised the best to 58% with structure-aware 384, at 3,287 tokens read.
| Setup | Whole answer, top 5 | + neighbours | Tokens read (+ neighbours) |
|---|---|---|---|
| fixed-size 128 | 12% | 23% | 636 (1,753) |
| fixed-size 256 | 35% | 38% | 1,280 (3,397) |
| fixed-size 384 | 38% | 54% | 1,920 (4,962) |
| fixed-size 480 | 42% | 58% | 2,400 (5,926) |
| structure-aware 128 | 35% | 42% | 464 (1,284) |
| structure-aware 256 | 54% | 54% | 828 (2,342) |
| structure-aware 384 | 54% | 58% | 1,224 (3,287) |
| structure-aware 480 | 50% | 50% | 1,496 (3,754) |
Lesson: structure beats size here too. Structure-aware 256 got the whole answer to the model 54% of the time reading 828 tokens; the best fixed-size setup (fixed-size 480) managed 42% reading 2,400. Adding neighbouring passages mostly repairs blind cuts: it lifted fixed-size chunks by 12 points on average but structure-aware ones by only 3 points, at roughly three times the text. So the same setting serves both kinds of question; widen it or add neighbours only when answers routinely run past a paragraph.
6. Words, meaning, or both
At the recommended chunking, keyword search alone found 40% of single-sentence answers, meaning alone 44%, and both fused 48%. Keywords nail exact names and figures; meaning catches paraphrase.
In live use, “match any word” keyword search pulled in generic cost and margin text and was slow (about 15,000 candidate passages to score per question). Requiring every word made it precise and took it from about 3 seconds to a few milliseconds. The app now searches by meaning by default, with words and both one click away, and the Compare search methods tab shows all three side by side.
7. Engineering lessons
- At ~60,000 passages, exact vector search (~0.1 s) beat an approximate index: no accuracy loss, and the index's filtered scan was slower.
- Correct spelling before embedding: a typo (“tarrifs”) changed the question's vector enough to return unrelated passages.
- Drop fragments: heading-only passages (under 16 tokens) match many questions and answer none.
- Measure against known answers, mechanically: gold sentences located in the stored text, so “found” needs no AI judge.
- Cost: everything runs on free tiers; only Claude's written answers are billed, about 1–5 cents each.
8. What I would do next
- Report nDCG@10 alongside recall, so results compare with BEIR and MTEB.
- Run the FinanceBench open sample on the companies in this corpus.
- Score Claude's answers for faithfulness to the passages (RAGAS-style).
- Benchmark overlap, semantic and hierarchical chunking, and add a re-ranker after retrieval.
- Grow the question set: 48 + 26 questions means one question moves a score by 2–4 points.
In five plain steps
- 1
Collect the filings
250 annual reports (10-K) from 250 companies, downloaded from the SEC. We keep four sections: Business, Risk Factors, Management's discussion and Market risk. The originals stay at the SEC; each result links back to them. - 2
Cut them into passages (chunking)
A whole filing is far too long to compare with a question, so each one is cut along its own headings and paragraphs into short passages of up to 256 tokens (about 190 words): 58,926 passages in total, . Where the cuts fall matters: a passage should hold one complete thought. See the cuts in a real filing. - 3
Turn each passage into numbers (vectors)
A small open model (gte-small) turns every passage into a list of 384 numbers that captures what it means. Passages about similar things get similar numbers, which is what lets a search find meaning rather than only matching words. - 4
Find the passages that fit your question
Your question gets the same treatment: spelling is corrected, then it becomes 384 numbers too. By default the search finds the passages closest in meaning and keeps the best ten. You can also ask for passages containing every word of the question, or both lists combined. - 5
Write the answer from those passages only
Claude reads just those passages and writes a short answer, citing each claim with the number of the passage it came from. On every search, “How we got there” shows each of these steps with its numbers. - Why there is also Compare & rank. Some questions, like “who has the best profit margin?”, can't be answered from passages, because no single passage compares every company. For those, Compare & rank uses the figures each company reported to the SEC instead.
The details
The corpus
Annual reports (Form 10-K) filed with the U.S. Securities and Exchange Commission. Source filings stay at the SEC: each one is fetched, fingerprinted and parsed, and never re-hosted. What is stored is a canonical text of the sections in scope, its document tree, and a link back to the original with the source file's SHA-256 so the exact bytes can be verified.
- In scope: Item 1 (Business), 1A (Risk Factors), 7 (Management's Discussion) and 7A (Market Risk).
- Not in scope: financial statements and exhibits. Reported figures come from the SEC's structured XBRL data, which is exact, instead of from parsed tables.
- Data: SEC EDGAR, public records. This project is not affiliated with or endorsed by the SEC.
One representation, many chunkings
Every filing is parsed once into a document tree (document, item, section, paragraph, table, list) with stable character offsets into one canonical text. A chunking strategy is then just a list of boundaries over that text. Because every strategy, every labelled answer and every context window is a span over the same characters, they can be compared fairly, and nothing is copied.
Strategies
- Fixed: token windows of a given size and overlap, ignoring structure.
- Recursive: splits on paragraph, then sentence, then token boundaries to a target size.
- Sentence: whole sentences packed up to a limit.
- Structure-aware: follows items, sections and blocks, and never crosses a section.
- Semantic: starts a new chunk where adjacent sentences stop being similar.
- Hierarchical: small units are retrieved, then expanded through the tree at query time within a token budget.
What is measured
Chunk quality (no retrieval)
Size distribution; how many cuts fall on a sentence, block or section boundary; chunks that cross a section; and answer-span containment: for a labelled answer, is it held whole by one chunk, and if not, how many chunks does it take? This is an upper bound on what any retriever can recover from those chunks.
Retrieval quality
Recall@k (is the right passage among the top k?), MRR (how high is it ranked?), and the full-evidence rate: whether everything needed to answer is inside the context the model actually receives, at a fixed token budget. Budgets matter because larger chunks win at a fixed k simply by covering more text.
Cost
Chunks stored, tokens embedded, index size, tokens of context sent, and latency. Quality without cost is not a trade-off, so the Benchmarks page shows both and marks the configurations that nothing else beats on both (the Pareto frontier).
How to read the results
- There is no universal best chunk size. The sweeps change one parameter at a time and show how quality and cost move together.
- Results come in stages: intrinsic (chunk metrics only), screening (shortlisted configurations on a smaller corpus, used to decide what to run in full), and full (the whole corpus and held-out test questions). Screening is never the final comparison.
- Small question sets are noisy. Differences smaller than the confidence interval are not claimed as wins.
- Chunk sizes stay inside the embedding model's native window (up to 480 tokens). Larger chunks need truncation or averaging several embeddings, which is a second variable, so they are studied in a separate experiment.
What this costs
Four different costs are tracked separately and never added together, because they have different units and different owners.
Bandwidth
Filings and lists downloaded from the SEC, counted on the wire (compressed) and decompressed. The SEC asks for at most 10 requests per second.
Local embedding compute
Text read by the open-source gte-small model on our own machine. The cost is machine time, not an API bill.
Storage
Tables, indexes and compressed text, measured per table. The hosted database holds only the winning production configuration.
Paid generation
Claude calls that write and grade answers. The only category with a dollar cost.