Skip to content

RAG pipeline ​

A minimal retrieval-augmented-generation indexing pass: PDF → per-page chunks → embeddings → vector store.

ts
import { toMarkdownPages } from "@nalinor/mupdf4llm";
import { readFileSync } from "node:fs";
// pseudocode — swap in your embedding model & vector store of choice
import { embed } from "./my-embeddings";
import { upsert } from "./my-vector-store";

async function indexPdf(filePath: string) {
  const buf = readFileSync(filePath);
  const chunks = await toMarkdownPages(buf, { extractWords: true });

  for (const c of chunks) {
    if (!c.text.trim()) continue;
    const vec = await embed(c.text);
    await upsert({
      id: `${filePath}#p${c.metadata.page}`,
      vector: vec,
      payload: {
        source: filePath,
        page: c.metadata.page,
        title: c.metadata.title,
        toc: c.toc_items,
        wordCount: c.words?.length ?? 0,
      },
    });
  }
}

Sub-page chunking ​

For very dense pages, split on section headers:

ts
function sectionChunks(text: string): string[] {
  const out: string[] = [];
  let buf = "";
  for (const line of text.split("\n")) {
    if (/^#{1,3} /.test(line) && buf) {
      out.push(buf);
      buf = "";
    }
    buf += line + "\n";
  }
  if (buf) out.push(buf);
  return out;
}

for (const c of await toMarkdownPages(pdf)) {
  for (const sub of sectionChunks(c.text)) {
    await upsert({ /* ... */ payload: { page: c.metadata.page, text: sub } });
  }
}

Token-bounded chunking ​

Concatenate pages, then run your model's tokenizer + a splitter (@anthropic-ai/tokenizer, tiktoken, etc.). The per-page chunks above already give you a natural seam to recombine.

AGPL-3.0-or-later — inherited from PyMuPDF and pymupdf4llm.