Jetro: 高性能 Rust JSON 查询引擎
JSON 仍然是现代 Web 的主要数据交换格式,但大规模高效查询往往会成为瓶颈。虽然 jq 是广泛认可的 JSON 处理行业标准,但将其嵌入到高性能 Rust 应用程序中的能力并不总是那么直接。Jetro 应运而生,这是一款用 Rust 编写的新型 JSON 查询引擎,旨在兼具表达能力和易用性。
Jetro 不仅仅是 jq 的克隆,而是对 JSON 查询执行方式的重新思考。通过利用函数式编程范式和规划器驱动(planner-driven)架构,Jetro 旨在为领域特定语言(DSL)提供强大的功能,同时提供一个更精简、可嵌入的引擎,优先考虑性能和资源效率。
架构:规划器驱动执行
Jetro 最显著的技术区别之一在于其查询执行方式。在许多传统的查询语言中,操作链——如映射(mapping)、过滤(filtering)和收集(collecting)——是作为一系列物化阶段执行的。例如,如果一个查询过滤订单列表并仅获取第一个结果,朴素的实现会先处理列表中的每一个订单,然后再将结果传递给下一个阶段。
Jetro 采用了被称为 需求传播(demand propagation) 的技术。需求传播允许引擎从查询链的末端向后读取,从而确定满足查询到底需要什么。引擎不是处理整个数据集,而是让元素以一种延迟的、基于拉取的模型在流水线中流动。
请考虑以下查询:
let out = j.collect("$.orders .map({ id: @.id, total: @.items.map(@.price * @.qty).sum() }) .filter(@.total > 100) .map(@.id) .first()");
在 Jetro 中,这并不会物化所有订单并为每一个订单计算总额。相反,引擎会询问:“我需要一个 ID 吗?是的。我需要一个匹配的订单吗?是的。拉取一个订单,计算其总额,如果它符合过滤条件,则输出 ID 并立即停止。”
这种方法显著降低了 CPU 和内存开销,尤其是在处理大型 JSON 文档时,其中仅需的数据只是其余数据的一个小子集。
表达能力与函数式范式
Jetro 的 DSL 灵感来源于函数式语言,允许开发者构建复杂的数据转换。它支持高级的对象塑造(object-shaping)查询、对象映射和模式匹配。
例如,Jetro 可以在查询中处理复杂的条件逻辑,如下例所示:
{
errors: $.events .drop_while(@.level != 'error') .filter(@.service == 'checkout') .map(match @ with {
{ level: 'error', message: msg, timestamp: ts } -> { kind: 'error', ts: ts, msg: msg },
{ level: 'error', message: msg } -> { kind: 'warning', msg: msg },
_ -> { kind: 'other' }
}) .take(20),
slow_orders: $.orders .filter(@.latency_ms > 500) .map({ id: @.id, latency: @.latency_ms }) .take(10),
first_vip: $.customers .filter(@.tier == 'vip') .map({ id: @.id, region: @.region }) .first()
}
这种表达能力使得转换逻辑可以成为查询的一部分,从而允许应用程序的其他部分接收到一个干净的最终结果。
性能与集成
为了确保最大吞吐量,Jetro 使用 simd-json 作为其主要的 JSON 解析器,利用 SIMD(单指令多数据)指令来加速解析过程。通过将快速解析器与需求传播执行模型相结合,Jetro 避免了不必要的工作,并避免了中间结果的冗余物化。
除了库本身,该项目还为偏好 jq-like 体验的终端用户提供了 jetrocli,并提供了一份详尽的指南(book)用于学习该语言。
标准化的挑战
虽然 Jetro 为 jq 提供了一个强大的替代方案,但社区讨论的更广泛生态系统凸显了一个挑战:缺乏 jq 语言的正式规范。正如 Hacker News 上的一位用户所指出的,存在多种 jq 的实现(包括 Python 的 yq 和 Go 的 yq),但它们通常只能做到“基本兼容”。
"实现之间的细微差别正是它们让你感到棘手的地方..."
Jetro 的方法——创建一个更小、更易于接近的查询语言,同时保持 jq 的函数式能力——通过提供一个单一、一致的引擎,解决了这种歧义,并且该引擎可以直接嵌入到 Rust 应用程序中。