Customers

Pricing
Introducing r-1: Reducto’s new SOTA document parsing model
Parse
RAG
September 4, 2026

Turn a Complex 10-K into Grounded RAG with r-1

Build a grounded RAG pipeline for Apple’s 10-K with r-1. Learn table-aware document parsing, embedding-ready chunking, retrieval, and page-level citations.

r-1 is Reducto’s new unified parsing model, built to read the full page in context across layouts, tables, figures, and formatting. It delivers a 20% lower error rate than our previous agentic models at only 1¢ per page, making it a stronger and more efficient ingestion layer for RAG.

Many apparent retrieval or generation failures begin earlier, when the source document is parsed. If a table is flattened, a footnote is separated from the number it qualifies, or a multi-column page is read in the wrong order, the rest of the system faithfully indexes and retrieves an incomplete representation.

Apple's 2023 10-K is a useful stress test because it combines long narrative sections, dense financial tables, cross-referenced footnotes, and multi-page structure in one document. In this cookbook, you will use r-1 to turn that filing into embedding-ready chunks, retrieve the most relevant evidence for a question, and generate an answer that points back to the original pages and bounding boxes.

What you'll build

  • Table-aware, embedding-optimized chunks
  • Local vector index
  • Top-k retrieval
  • Grounded answer with page-level sources

The example deliberately uses an in-memory vector index so you can run the entire pipeline with only Reducto and one model provider. In production, replace the in-memory index with the vector database you already use without changing the parsing and citation logic.

Setup

Start by signing up for a Reducto Studio account and generating your Reducto API key.

We’ll use this Apple 2023 10-K as the sample document. Set your API keys and install the SDKs below.

Note: r-1 is currently in preview and requires the V3 Parse API. API requests must set settings.model to "r-1"; requests that omit it use legacy Parse. New Studio pipelines use r-1 by default. See the r-1 guide and review configuration compatibility before adapting an existing workflow.

python
export REDUCTO_API_KEY="your-reducto-key" export OPENAI_API_KEY="your-openai-key" export GENERATION_MODEL="gpt-4.1-mini" pip install reducto openai numpy requests

This cookbook uses OpenAI for embeddings and answer generation, but those layers are interchangeable. The important artifact is the structured r-1 output, including the embedding-optimized text, original content, blocks, pages, and bounding boxes.

Step 1: Parse and chunk the 10-K with r-1

For RAG, the embedding model needs text optimized for semantic search, while the final answer and source viewer need the original structured content and page positions.

The configuration below uses variable chunking to preserve semantic boundaries, HTML to retain complex financial-table structure, and embedding optimization to make those tables easier to retrieve. It filters repetitive headers, footers, and page numbers from the searchable text while retaining the underlying blocks and their metadata.

python
import os from reducto import Reducto DOCUMENT_URL = "https://d18rn0p25nwr6d.cloudfront.net/CIK-0000320193/faab4555-c69b-438a-aaf7-e09305f87ca3.pdf" reducto = Reducto(api_key=os.environ["REDUCTO_API_KEY"]) parsed = reducto.parse.run( input=DOCUMENT_URL, settings={ "model": "r-1", }, formatting={ "table_output_format": "html", "merge_tables": True, }, retrieval={ "chunking": { "chunk_mode": "variable", "chunk_size": 1200, "chunk_overlap": 150, }, "embedding_optimized": True, "filter_blocks": ["Header", "Footer", "Page Number"], }, ) print(f"Parsed {parsed.usage.num_pages} pages") print(f"Review the result in Studio: {parsed.studio_link}")

Tip: Use chunk.embed for vector search and chunk.content when displaying evidence or sending retrieved context to the generation model. The blocks array preserves the page and bounding box for every element.

Step 2: Turn the Parse response into citation-ready records

Large filings may return their chunks through a result URL instead of inline. This helper handles either response and keeps the searchable text, original content, pages, and bounding boxes together in each record.

python
import requests def as_dict(value): if hasattr(value, "model_dump"): return value.model_dump() return value def load_chunks(parse_response): if parse_response.result.type == "full": return [as_dict(chunk) for chunk in parse_response.result.chunks] response = requests.get(parse_response.result.url, timeout=120) response.raise_for_status() payload = response.json() if isinstance(payload, dict) and "result" in payload: payload = payload["result"] if isinstance(payload, dict) and "chunks" in payload: payload = payload["chunks"] return payload chunks = load_chunks(parsed) records = [] for index, chunk in enumerate(chunks): blocks = [as_dict(block) for block in chunk.get("blocks", [])] bboxes = [ block["bbox"] for block in blocks if block.get("bbox") is not None ] pages = sorted({ bbox["page"] for bbox in bboxes if bbox.get("page") is not None }) records.append({ "id": f"apple-10k-{index}", "search_text": chunk.get("embed") or chunk["content"], "content": chunk["content"], "pages": pages, "bboxes": bboxes, "blocks": blocks, }) print(f"Prepared {len(records)} citation-ready chunks") print(records[0]["pages"], records[0]["content"][:300])

Step 3: Embed and index the chunks

For a single document, an in-memory cosine-similarity index is enough to make the example fully runnable.

python
import numpy as np from openai import OpenAI ai = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) EMBEDDING_MODEL = "text-embedding-3-small" embedding_response = ai.embeddings.create( model=EMBEDDING_MODEL, input=[record["search_text"] for record in records], ) matrix = np.asarray( [item.embedding for item in embedding_response.data], dtype=np.float64, ) matrix /= np.linalg.norm(matrix, axis=1, keepdims=True)

For a larger corpus, insert the same fields into Pinecone, Elasticsearch, pgvector, Qdrant, or your existing search layer. Keep search_text as the embedded representation, content as the answer context, and pages plus bboxes as metadata.

Step 4: Retrieve the evidence for a question

python
def retrieve(question, k=5): query_response = ai.embeddings.create( model=EMBEDDING_MODEL, input=[question], ) query_vector = np.asarray( query_response.data[0].embedding, dtype=np.float64, ) query_vector /= np.linalg.norm(query_vector) scores = matrix @ query_vector top_indices = np.argsort(scores)[::-1][:k] return [ { "score": float(scores[index]), "record": records[index], } for index in top_indices ] question = ( "How do Apple's Products and Services gross margins compare, " "and what factors could affect gross margins going forward?" ) hits = retrieve(question) for hit in hits: print(hit["score"], hit["record"]["pages"]) print(hit["record"]["content"][:400], "\n")

This question intentionally requires both a financial table and surrounding narrative. It is a better test of the ingestion layer than asking for a phrase that appears verbatim in one paragraph.

Step 5: Generate a grounded answer with citations

python
GENERATION_MODEL = os.getenv( "GENERATION_MODEL", "gpt-4.1-mini", ) def page_label(pages): if not pages: return "page unavailable" return "pages " + ", ".join(str(page) for page in pages) def answer(question, k=5): hits = retrieve(question, k=k) source_records = [] for rank, hit in enumerate(hits, start=1): record = hit["record"] source_records.append( f"[S{rank}] Apple 10-K, {page_label(record['pages'])}\n" f"{record['content']}" ) context = "\n\n".join(source_records) prompt = ( "Answer the question using only the supplied sources. " "Cite every material claim with one or more source labels such as [S1]. " "Do not use outside knowledge. If the sources do not support an answer, " "say that the filing does not provide enough information.\n\n" f"Question:\n{question}\n\nSources:\n{context}" ) response = ai.responses.create( model=GENERATION_MODEL, input=prompt, ) return response.output_text, hits final_answer, cited_hits = answer(question) print(final_answer) print("\nSource map") for rank, hit in enumerate(cited_hits, start=1): record = hit["record"] print(f"S{rank}: {page_label(record['pages'])}") print(f" bounding boxes retained: {len(record['bboxes'])}")

Prefer no code?

You can validate the r-1 parsing and chunking layer in Reducto Studio before writing integration code. Upload the Apple 10-K, select r-1, enable variable chunking and embedding-optimized output, and inspect the original content, searchable text, pages, and bounding boxes side by side.

Where this goes next

  • Replace the local cosine-similarity index with the vector database your team already uses.
  • Add Apple's 10-Qs and investor materials, then filter retrieval by filing type and reporting period.
  • Use the stored page and bounding-box metadata to make every citation open the exact supporting evidence.
  • Add multimodal retrieval when an answer needs the original pixels from a chart or table.

For adjacent examples, see layout and table extraction from a 10-K, the Parse response structure, and chunking methods.


CTA patternReducto logo

Make your first API call in minutes.

Reducto logoLLM Center