- 后端
- AI 应用
- NLP
【免费下载链接】xberg
Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with CLI, REST API, and MCP server.
批量文档提取是 Xberg(基于 Rust 核心的 Polyglot 文档智能引擎)最常用的接口之一,而“空批次”是其最容易被忽略却必须正确处理的边界场景。本篇以仓库中的 Elixir 片段文档 extract_batch_empty_inputs.md 为骨架,完整讲解extract_batch在空输入下的行为契约,并结合 Rust 引擎实现 与 E2E 测试 还原其底层原理。读完本文,你将掌握:空批次调用应得到什么返回、为何引擎对空批次做短路处理、缓存层如何豁免空批次,以及如何在本地用 Elixir 完整复现与验证。
空批次场景从何而来:批量 API 的边界问题
extract_batch的设计目标是一次调用处理多个文档输入(字节数组或 URI)。但在真实流水线中,输入列表并非永远非空——典型触发场景包括:
- 动态上游过滤:检索、去重或权限过滤之后,候选文档列表可能被清空;
- 参数化调度:批量任务由配置驱动,配置为空时直接得到
[]; - 批量写入/同步作业:对增量数据做批量提取,某轮增量恰好为 0;
- 编排层的占位调用:在统一接口处先发一次“空探测”以验证链路与配置。
如果引擎对空输入报错或产生异常返回,调用方就必须额外写分支规避,这会显著增加流水线的脆弱性。因此,Xberg 将空批次定义为合法且安全的调用:返回成功、结果为空、不产生任何副作用。
这一点在仓库中被明确固化。片段文档所属的 fixture 文件 empty_inputs.json 中声明了两条核心断言:not_error(调用不得报错)与count_equals 0(results字段计数必须为 0):
{ "id": "extract_batch_empty_inputs", "call": "extract_batch", "input": { "inputs": [] }, "assertions": [ { "type": "not_error" }, { "type": "count_equals", "value": 0, "field": "results" } ] }从片段文档出发:Elixir 侧的空批次调用
片段文档给出的核心示例非常简洁:
result = Xberg.extract_batch_async([]) IO.inspect(result.results)这一写法直接调用的是 Rustler NIF 层的extract_batch_async/2(见 native.ex)。而在实际工程中,推荐使用同一模块下更高层的封装Xberg.extract_batch/1,它负责把 Elixir 关键字列表参数序列化为 JSON 后再进入 NIF(见 xberg.ex):
{:ok, result} = Xberg.extract_batch(inputs: []) IO.inspect(result.results) # [] IO.inspect(result.summary) # %{inputs: 0, results: 0, errors: 0, ...}高层封装对inputs的处理是:二进制字符串直接透传,非二进制(如这里的 Elixir 列表)则通过Jason.encode!序列化;config未传时保持nil。也就是说,Xberg.extract_batch(inputs: [])与片段中的 NIF 直接调用在语义上等价——都是提交一个空批次。
值得强调两点:
- 返回值是
{:ok, result}而非裸结果。片段中的IO.inspect(result.results)隐含了这一点;E2E 测试同样使用{:ok, result} = Xberg.extract_batch(inputs: [])的模式匹配(见 batch_test.exs)。空批次不会走{:error, reason}分支。 results为空列表而非nil。这保证了调用方可以直接Enum.each(result.results, ...)或length(result.results),无需判空。
返回契约:ExtractionResult 与 ExtractionSummary
空批次返回的对象是统一的提取结果信封ExtractionResult。其结构定义在 types.rs,关键字段如下:
| 字段 | 类型 | 空批次时的值 | 说明 |
|---|---|---|---|
results | Vec<ExtractedDocument> | [] | 按发现顺序排列的提取文档 |
errors | Vec<ExtractionErrorItem> | [](序列化时省略) | 非致命的逐输入错误 |
summary | ExtractionSummary | 见下 | 本次操作的聚合计数 |
crawl_final_urls | Vec<String> | [] | URL 摄取时经重定向后的最终地址 |
crawl_redirect_count | usize | 0 | 抓取或爬取过程中跟随的重定向总数 |
crawl_unique_normalized_urls | Vec<String> | [] | 爬取发现的唯一规范化 URL |
聚合计数ExtractionSummary(定义于 types.rs)包含:
inputs:调用方提交的输入数量——空批次为0;results:成功产生的提取结果数——空批次为0;errors:逐输入错误数——空批次为0;remote_urls:解析为远程 HTTP(S) 的 URI 输入数;pages_crawled:被爬取或抓取的 HTML 页面数;documents_downloaded:从 URL 下载并提取的非 HTML 文档数。
E2E 测试中对空批次的断言正是assert length(result.results) == 0,与summary.results == 0的语义一致。这也意味着:“成功”与“有结果”是两件事——{:ok, _}只代表调用整体成功,具体产出要看summary与results。这一点对调用方后续的日志与指标统计非常关键。
Rust 引擎底层:空输入的短路与缓存豁免
Elixir 绑定只是薄封装,真正的行为由 Rust 引擎决定。调用链为:
Xberg.extract_batch/1 └─ Xberg.Native.extract_batch_async/2 (Rustler NIF) └─ Engine::extract_batch (crates/xberg/src/engine/mod.rs#L95-L101) └─ extract_impl::extract_batch (crates/xberg/src/engine/extract_impl.rs#L217) └─ extract_batch_uncached ├─ extract_batch_concurrent (tokio 并发路径) └─ extract_batch_sequential (wasm32/无 tokio 路径)从源码可以梳理出空批次在引擎内的三个关键处理点:
1. 进度事件照常发出,但不会报错
extract_batch一开始就通过ProgressSink发出batch_start事件(fraction 为 0.0),随后校验配置与取消令牌。空批次不触发任何错误,最终会正常走到完成路径并发出batch_complete事件。
2. 缓存键对空批次返回None
batch_content_cache_key的第一行就是空输入检查(extract_impl.rs):
fn batch_content_cache_key(inputs: &[ExtractInput], base_config: &ExtractionConfig) -> Option<String> { if inputs.is_empty() { return None; } // ... }结合外层逻辑(extract_impl.rs):只有存在缓存键、且批次无错误时,结果才会写入CacheBackend。因此空批次既不会查缓存也不会写缓存,天然避免了“空结果被缓存”带来的语义污染。这是刻意设计:空批次本身无需计算,缓存没有任何收益。
3. 并发路径对input_count == 0直接短路
在 tokio 并发路径extract_batch_concurrent中,输出结果会先以输入数初始化summary.inputs,随后立即检查输入数(extract_impl.rs):
let mut output = ExtractionResult { summary: ExtractionSummary { inputs: input_count, ..Default::default() }, ..Default::default() }; if input_count == 0 { return Ok(output); }这意味着空批次不会创建任何 worker 任务、不会初始化线程池,直接从入口返回summary.inputs == 0、results为空的结果。而在无 tokio 运行时或 wasm32 目标的顺序路径extract_batch_sequential(extract_impl.rs)中,for循环天然对空输入零迭代,同样返回空结果。
此外,从代码注释可见extract_batch的并发路径在 wasm32 上被刻意替换为顺序路径——因为 wasm32 下 extractor 的 future 是!Send的,无法在JoinSet上并发执行。这与空批次本身无关,但说明空批次行为在所有目标平台上是一致的:无论走哪条路径,返回结果完全相同。
测试证据:fixture、E2E 与契约测试三层验证
空批次行为在仓库中有三层测试覆盖:
第一层:批量 fixture 断言。empty_inputs.json 声明not_error与results计数为 0,这是所有语言绑定共用的规格来源,也是本文所依据的片段文档的生成源头。
第二层:Elixir E2E 测试。batch_test.exs 中的extract_batch_empty_inputs用例:
describe "extract_batch_empty_inputs" do test "extract_batch_empty_inputs" do {:ok, result} = Xberg.extract_batch(inputs: []) assert length(result.results) == 0 end end第三层:契约 fixture 佐证。与空批次并列的批量契约用例(如 api_extract_batch_bytes_with_config.json 展示带每输入配置的字节批次、api_extract_batch_uri.json 展示 URI 批次)共同定义了extract_batch的完整输入面:字节与 URI 混用、未知 MIME、缺失 URI、部分失败等场景都有独立断言。空批次只是这张契约表中的一个成员,与“全部缺失”(results == 0, errors == 2)等场景形成对照,帮助开发者区分“没有输入”与“输入全部失败”两种截然不同的语义。
实操:在本地用 Elixir 完整验证
环境准备
- Elixir 1.14+ 与 Erlang/OTP 26+(见 packages/elixir/README.md 的系统要求);
- 在
mix.exs中声明依赖(当前仓库 mix.exs 的版本为 1.3.0):
def deps do [ {:xberg, "~> 1.3.0"} ] end随后执行mix deps.get。
最小复现
# 1. 空批次:合法调用,空结果 {:ok, result} = Xberg.extract_batch(inputs: []) IO.inspect(result.results) # [] IO.inspect(result.summary.inputs) # 0 IO.inspect(result.summary.results) # 0 IO.inspect(result.summary.errors) # 0 # 2. 对照:正常批次 inputs = [ %{"kind" => "uri", "uri" => "report.pdf"}, %{"kind" => "uri", "uri" => "notes.txt"} ] case Xberg.extract_batch(inputs: inputs) do {:ok, output} -> Enum.each(output.results, &IO.puts(&1.content)) {:error, reason} -> IO.puts(:stderr, "Extraction failed: #{inspect(reason)}") end在仓库内运行 E2E 测试
仓库已内置批量的 Elixir E2E 测试套件,运行方式:
cd e2e/elixir && mix test test/batch_test.exs其中extract_batch_empty_inputs用例会直接验证空批次行为;其余用例(extract_batch_bytes_happy、extract_batch_uri_partial_failure等)可作为对照,观察summary.results/summary.errors在不同输入下的计数差异。
工程建议
基于上述契约,在接入extract_batch时建议遵循:
- 无需对空列表做上游拦截——空批次是合法调用,直接透传即可,引擎会短路返回;
- 用
summary而非异常判断成败——{:ok, result}不保证有产出,判断“是否有结果”应看result.summary.results; - 空批次不会污染缓存——由于缓存键在空输入时返回
None,重复的空批次调用不会产生缓存读写,也不会与真实批次的缓存互相干扰; - 区分“无输入”与“全失败”——空批次
summary.errors == 0,而输入全部缺失时(见extract_batch_uri_all_missing用例)summary.errors等于输入数,二者在告警与日志中应区别对待。
小结
extract_batch的空批次行为看似是微不足道的边角,实则是批量 API 健壮性的试金石:它在 Elixir 侧表现为{:ok, %{results: [], summary: %{inputs: 0}}},在 Rust 引擎侧由“缓存键豁免 + 并发路径短路”双保险保证零开销返回,并由 fixture、E2E 与契约测试三层固定。理解这一契约,能让你在构建文档流水线时少写一层防御分支,并把注意力放在真正需要关心的summary.results/summary.errors统计上。如需进一步阅读,可深入 extract_impl.rs、types.rs 与 batch_test.exs 三处源码继续追溯。
- 后端
- AI 应用
- NLP
【免费下载链接】xberg
Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with CLI, REST API, and MCP server.
相关推荐
xberg 批量提取的空输入边界:深入解析 extract_batch 与空批次行为
xberg 批量提取的空输入边界:深入解析 extract_batch 与空批次行为 导读 :本文聚焦 xberg 文档化测试(alef fixture)中一个
后端AI 应用NLP使用 xberg Dart 绑定批量提取字节输入:extract_batch 契约测试与 Rust 引擎剖析
使用 xberg Dart 绑定批量提取字节输入:extract_batch 契约测试与 Rust 引擎剖析 本文围绕 xberg 仓库中的 Dart 契约测试
后端AI 应用NLPKornia 零尺寸语义统一:`resize`/`rescale` 空输出返回与退化输入防护的实现解析
Kornia 零尺寸语义统一: resize / rescale 空输出返回与退化输入防护的实现解析 kornia.geometry.transform.res
计算机视觉人工智能深度学习图像处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考