cocoindex 实战:用 Rust 编写 PDF 批量转 Markdown 的增量处理管线
【免费下载链接】cocoindexIncremental engine for long horizon agents 🌟 Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex
导读
本文围绕 cocoindex 仓库中的 Rust 示例 examples/rust/pdf_to_markdown,完整讲解如何用 cocoindex 的 Rust SDK 构建一条「扫描本地 PDF → 提取文本 → 声明式写出 Markdown」的增量数据处理管线。读完本文,你将掌握cocoindex::fs::walk_items目录扫描、#[cocoindex::function(memo)]组件记忆、DirTarget声明式目录目标以及mount_each!批量挂载这几个核心 API 的组合用法,并理解增量跳过与孤儿输出自动清理的底层机制。该示例是 Python 版 examples/pdf_to_markdown 的 Rust 移植,可作为在 Rust 侧落地 cocoindex 应用的参考模板。
示例概览:它在做什么
该示例的核心逻辑只有三步(对应 src/main.rs):
- 扫描:用
cocoindex::fs::walk_items(&source_dir, &["**/*.pdf"])递归收集目录下所有 PDF 文件,返回(稳定键, FileEntry)对; - 转换:每个 PDF 文件经
#[cocoindex::function(memo)] convert_pdf提取纯文本,再由process_pdf以<stem>.md命名声明输出; - 写出:通过声明式目录目标
DirTarget将结果写到磁盘,由引擎自动完成「写入 / 更新 / 跳过未变化 / 删除已失效输出」的调和(reconcile)。
整个流程全部声明式:应用只描述「期望什么输出」,实际文件系统操作由 cocoindex 的目标状态引擎代为执行。
与 Python 版本的对照
README 中给出了一张逐环节对照表,可以帮你快速定位同一套模式在两个语言 SDK 中的对应写法:
| 关注点 | Python(main.py) | Rust(本示例) |
|---|---|---|
| 文件来源 | localfs.walk_dir(**/*.pdf) | cocoindex::fs::walk_items(**/*.pdf) |
| PDF → Markdown | docling(PDF → Markdown,ML 流水线) | lopdf文本提取 |
| 单文件计算 | @coco.fn(memo=True) process_file | #[cocoindex::function(memo)] convert_pdf |
| 输出 | localfs.declare_file(<stem>.md) | DirTarget::declare_file(<stem>.md) |
与 Python 版的关键差异(README 明确标注):Python 版使用docling(一个重量级 ML 文档理解流水线)来获得高保真度的 PDF→Markdown 转换;Rust 生态中目前没有等价物,因此本移植用lopdf做纯文本提取。结果是:输出是纯文本而非结构丰富的 Markdown,转换质量随 PDF 本身的排版而异。声明式目录目标与<stem>.md命名规则则与 Python 版完全一致。
这一差异也体现在依赖上:本示例的 Cargo.toml 中,除了cocoindex与tokio,只额外引入lopdf = "0.40",注释明确写着它是「Python docling 转换器的 Rust 原生替代」。
运行方式
在 examples/rust/pdf_to_markdown 目录下直接执行:
cargo run # ./pdf_files -> ./out cargo run -- /path/to/pdfs ./out # 自定义源目录 / 输出目录命令行参数在parse_args(src/main.rs)中解析:
- 第一个参数为源 PDF 目录,缺省为 crate 目录下的
pdf_files(通过env!("CARGO_MANIFEST_DIR")定位); - 第二个参数为输出目录,缺省为 crate 目录下的
out。
仓库自带的示例数据是 pdf_files 下的两个 PDF:1706.03762v7.pdf(Attention Is All You Need 论文)与rfc8259.pdf(JSON 规范)。运行后会在输出目录生成1706.03762v7.md与rfc8259.md,并在控制台打印转换统计与任务结束时的stats摘要。
源码拆解:逐段理解管线
1. PDF 文本提取:pdf_to_text
fn pdf_to_text(content: &[u8]) -> Result<String> { let doc = Document::load_mem(content) .map_err(|e| Error::engine(format!("failed to parse PDF: {e}")))?; let pages: Vec<u32> = doc.get_pages().keys().copied().collect(); if pages.is_empty() { return Ok(String::new()); } doc.extract_text(&pages) .map_err(|e| Error::engine(format!("failed to extract PDF text: {e}"))) }它接收 PDF 的原始字节,用lopdf解析文档、枚举页号并提取文本;空文档返回空字符串;解析或提取失败则包装为Error::engine交给引擎统一处理。从注释看,这正是paper_metadata、pdf_embedding等 Rust 示例使用的同一套 Rust 原生 PDF 处理路径。
2. 带 memo 的转换函数:convert_pdf
#[cocoindex::function] async fn convert_pdf(_ctx: &Ctx, file: &FileEntry) -> Result<String> { let content = file.content()?; tokio::task::spawn_blocking(move || pdf_to_text(&content)) .await .map_err(|e| Error::engine(format!("PDF parse task panicked: {e}")))? }FileEntry::content()读取文件字节(fs.rs 中定义,惰性读取并带内容缓存)。由于lopdf解析是同步的 CPU 密集操作,代码用tokio::task::spawn_blocking把它挪到阻塞线程池,避免卡住异步执行器——这是 Rust 示例处理重计算任务的通用手法。
#[cocoindex::function]宏会为函数生成组件化调用的包装;convert_pdf作为process_pdf的调用对象,天然带有 memo 快路径(详见下文「增量机制」小节)。
3. 单文件处理组件:process_pdf
#[cocoindex::function] async fn process_pdf(ctx: &Ctx, file: FileEntry, target: DirTarget) -> Result<()> { let markdown = convert_pdf(ctx, &file).await?; let outname = format!("{}.md", file.stem()); target.declare_file(ctx, &outname, markdown.as_bytes())?; Ok(()) }file.stem()返回去扩展名的文件名(如1706.03762v7),由此得到<stem>.md输出名;target.declare_file(ctx, name, content)只是声明期望目标目录中存在该文件,真正写入在调和阶段完成(fs.rs);- 注释指出:该组件按「每个文件」挂载,组件级 memo 快路径会跳过内容未变的 PDF,从而避免重复解析。
declare_file对文件名有严格校验(fs.rs):必须是非空的相对路径,不允许绝对路径、盘符前缀或..路径穿越,测试dir_target_name_validation_rejects_traversal_and_absolute对此有专门覆盖。
4. 主函数:组装 App 并运行
let app = App::builder("PdfToMarkdown") .db_path(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".cocoindex_db")) .build() .await?; let stats = app .run(move |ctx| { async move { let target = DirTarget::mount(&ctx, &output_dir)?; let files = cocoindex::fs::walk_items(&source_dir, &["**/*.pdf"])?; println!("converting {} PDF(s) from {}", files.len(), source_dir.display()); mount_each!(files, |file| process_pdf(ctx, file, target)).await?; Ok(()) } }) .await?; println!("{stats}");App::builder构建应用,状态数据库默认放在 crate 目录下的.cocoindex_db(LMDB),增量信息(目标状态、memo 指纹等)都持久化在这里,这正是「重启后仍能增量」的基础;DirTarget::mount(&ctx, &output_dir)挂载声明式目录目标。从实现看(fs.rs),挂载时默认create_parent_dirs=true自动建目录,并注册一个以目录路径为稳定键的目标状态提供者cocoindex/localfs/dir/<dir>;mount_each!(files, |file| process_pdf(ctx, file, target))把「文件集合」按单个文件逐个挂载为组件实例;若把DirTarget换成 Live 目录源,还能获得文件系统变化时自动增删文件的能力(fs_livefeature,参考 fs.rs 中的LiveDirWalker);app.run(...)结束后返回的stats会被打印,便于观察命中/写入的统计信息。
增量机制:memo 快路径与声明式目标
这个示例之所以是「增量」的,靠的是两套互相配合的机制:
组件 memo 快路径。#[cocoindex::function]生成的组件会先计算指纹——由函数所在模块、函数名、代码哈希与非ctx参数序列化结果共同构成(mount.rs)。引擎在真正执行组件之前检查该指纹(Component::execute_once→memo_key_fingerprint),命中则整体跳过,包括子调用与目标状态声明,直接回放上一次的结果。因此当某个 PDF 自上次运行以来未变化时,process_pdf/convert_pdf不会被重新执行,自然也不会重写对应的.md文件。README 中「unchanged files are memo-skipped」描述的正是这一行为。
声明式目录目标的调和。fs.rs 对DirTarget的语义描述得很清楚:
- 新文件或内容变化的文件 → 写入/更新;
- 内容未变化的文件 → 跳过(不重写,避免无谓的 IO 与 mtime 抖动);
- 上一次运行声明过、但本次没有再声明的文件(例如源 PDF 被删除)→ 从磁盘删除。
底层由FileHandler的reconcile(fs.rs)实现:对每个文件比较期望内容的指纹与上一次记录的指纹,完全一致且没有缺失风险时直接返回「无操作」;否则生成写/删动作,交由dir_sink在阻塞线程池中批量执行(自动创建父目录、写文件、删除NotFound时静默忽略)。这意味着你删掉pdf_files里的某个 PDF 再跑一次,对应的out/*.md会被自动清掉,无需手写清理逻辑。
输出质量说明与适用边界
需要留意 README 强调的限制:由于 Rust 版用lopdf做纯文本提取(而非docling的结构化理解),输出是扁平文本,不是带标题层级、列表、表格等结构的富 Markdown;对复杂双栏排版、扫描件(需 OCR)或表格密集型 PDF,文本顺序与可读性可能不理想,「quality varies by PDF」。如果你需要高保真文档理解,应优先考虑 Python 版 examples/pdf_to_markdown(依赖docling>=2.0.0)。而如果你的目标是把论文、规范等文本型 PDF 批量清洗成可检索文本,再喂给 embedding、摘要或 RAG 流水线,这个 Rust 示例就是一套轻量、可增量、低依赖的现成方案——同类思路在仓库的 paper_metadata 与 pdf_embedding 示例中也有体现。
小结
通过这个示例可以看到 cocoindex Rust SDK 的典型使用姿势:fs::walk_items扫描 +#[cocoindex::function]声明计算组件 +DirTarget声明输出,三者组合即可获得「自动增量、自动清理孤儿输出」的批处理管线。整个应用只有约 90 行(src/main.rs),并且不需要手写任何文件比对或状态管理逻辑——增量语义由引擎在.cocoindex_db中持久化的目标状态与组件指纹统一保障。如果你需要把同样的模式推广到 PDF 以外的文件类型,改动点也集中在walk_items的 glob 模式与process_pdf的转换逻辑两处。
【免费下载链接】cocoindexIncremental engine for long horizon agents 🌟 Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考