跳转至

扩展应用:i18n、外部 API、OpenAI、全文搜索、脚本引擎与换栈

这一章是「应用长大以后」的六个方向,每个都只带路、不深挖—— 书里的原则写得很白:别一开始就把这些组件全装上,等用例被证明了再加。 每一节回答同一个问题:这个需求在 Rust 生态里落到哪个 crate 上、要注意什么坑。 主线例子:给工单系统加「智能小助手」——用户问「有没有重复的票」, 它走全文搜索找候选、调 OpenAI 比对、把答案按用户语言渲染出来: 六个方向在这一条链上各出现一次。

1. 顶层全景

  对人的扩展:i18n/l10n(locale、CLDR、ICU;icu crate / i18ify)
    对世界的扩展:reqwest(出向 HTTP)→ OpenAI(system/user prompt + completion)
      对数据的扩展:全文搜索 = 索引 + 查询语言(tantivy,纯 Rust)
        对平台的扩展:嵌入脚本引擎(boa_engine;暴露实体、沙箱、假定输入是坏的)
          对架构的扩展:换栈不换命——Mongo 驱动 / async-GraphQL / Tonic(gRPC)

  图说:从近到远——先伺候人,再接通世界,然后数据、平台,最后连架构都可以换。

2. i18n 与 l10n:locale 是规则包,翻译只是冰山一角

随国家/语言变的东西有一长串: 标点、货币符号、数字分隔符、日期时间格式、 时区、文字方向……这些约定打包成 locale,用语言标签标记—— 标签必须带上地区(英语有英美澳多套规则,「一套语言一套规则」不够)1。

程序员的底线义务是:会变的东西都按 locale 函数传语言标签, 数字日期永远按当地规则显示。但这不解决「用户看不懂语言」——那要靠 提前由人工翻译、l10n 专家审校的文案库,运行时注入,而不是机翻现凑2。

真正的坑在变量: 「您上次登录后有 n 条新评论」——n 要替换,而且 阿拉伯语等语言不止一种复数形式,n 不同词形不同。「这不是替换静态文案, 是动态、语义正确的渲染」——必须靠为 i18n 而生的技术。3

行业标准在这里: Unicode 联盟(为全世界文字制定统一编码与规则的国际组织)维护 CLDR(Common Locale Data Repository)—— 收齐了格式化/解析(日期、数字、货币、单位)、文字(字符、复数、性别、 搜索规则、书写方向)、国家(历法、周惯例)的全部规则;ICU 是实现这些 规范的库族。Rust 侧有 icu crate(内置 CLDR 数据):示例里 DateTimeFormatter::try_new(locale!("es"), YMD::long()) 就把 2020-09-12 渲成西班牙语长格式「12 de septiembre de 2020」。项目小可用轻量的 i18ify(纯消息替换)。4

3. 出向 HTTP:reqwest,以及「值得专属集成」的服务

应用不只收请求,还要发请求(邮编、地理编码、新闻聚合……)。 reqwest 是 Rust 生态里受尊敬的 HTTP 客户端:异步、JSON/multipart、 HTTPS、cookie、甚至 WebAssembly 都能跑(第 6 章的 BDD 测试已经用它打过 自家后端)5。

但不是所有服务都满足于「裸 HTTP」: 有些大名气的服务值得专门的 Rust 封装——让开发者不碰底层请求。下一节就是那个典型。

4. OpenAI 集成:两个 prompt 与一次 completion

裸调: POST api.openai.com/v1/chat/completions,Bearer 带 API key, JSON 体里 model: "gpt-4o" + messages 数组。消息角色两种:

角色 干什么
system prompt 定整体框架/人设(「You are a helpful assistant.」)
user prompt 当前的具体任务

一轮问答叫一次 completion(给定环境下的回答)。6

封装路线: 实验性的 openai crate(写书时 alpha,随时破坏性变更)—— Credentials::from_env() 读 OPENAI_KEY/OPENAI_BASE_URL, ChatCompletion::builder("gpt-4o", messages).credentials(...).create().await 一句出结果,.choices.first() 拿回答(role + content)。 书里给的心安理由:API 细节也许会变,概念不会——永远可以查最新文档。7

5. 全文搜索:tantivy 与一门会失败的查询语言

朴素做法的问题: 拿字符串去数据库字段里 LIKE——难写、慢、不准。 正解是旁路索引系统:(索引 = 为快速查找预排好的目录,不是数据库本体)工单创建/更新的同时进索引;搜索走索引、支持模糊匹配。 收益三条:对(索引/分片/缓存调优后)高性能;词不必打对、跳词也能短语命中; 索引什么、权重几何,全由开发者控盘。代价三条:系统复杂度 + 一份新的运维; 索引与真实内容的映射未必好设计。8

技术选型: Lucene / Elasticsearch / Solr 都行,但都要养 JVM; 纯 Rust 的 tantivy 无缝集成,而且性能普遍显著超过 Lucene。 两件事:① 建索引(schema 定义字段——add_text_field("title", TEXT | STORED), STORED 标志决定结果里能不能取回该字段;IndexWriter 缓冲默认示例给 100MB, add_document 后必须 commit() 才发布);② 服务查询 (reader → searcher → QueryParser(字段列表) → parse_query → TopDocs::with_limit(10) → 按 DocAddress 取回文档)。 贴士:给文档存一个原文 URL/ID 字段,用户能从结果一步跳到原票。9

查询语言(强到会失败): 布尔(AND/OR/括号)、包含(+必现 / -必无)、 范围([]闭区间 / {}开区间)、集合 IN、日期(RFC3339)、通配符(、前缀 Rus)、 加权(^2.0 偏好 Axum 胜过 Actix)、词距(~1 允许隔一个词)。 解析可能失败——parse_query 的 Result 必须先查再用(示例里那个 ?)。10

6. 脚本引擎:把业务逻辑交给用户(boa_engine)

场景: 工单系统里算勉强,IoT 平台顺理成章——高级用户想自己定义 设备联动逻辑;或「定时生成报表、套模板、发邮件」这类用户自编自动化。 嵌入 boa_engine(可嵌入的 JS 引擎) 后:加逻辑不用重编应用 (编译由引擎现场完成),脚本与应用本体解耦交付,有权限的用户随时增删。11

机制五步(示例): 定义 JS 函数(文件或用户输入)→ 建 Boa Context → context.eval 灌入函数定义 → 用 ObjectInitializer 造一个带属性的 JS 对象、注册成全局属性 → 再 eval 一次真正调用,返回值回到 Rust (value.as_number(),示例断言拿到 12.0)。反向同理:把应用实体暴露给 脚本读写,Rust 干重活,业务逻辑全在用户的脚本里。12

红字安全清单(全章最要紧的注释): 用户输入当恶意代码防—— 暴露的变量不许泄露敏感信息、不许摸到应用的其他部分;Rust 侧永远假定 脚本是坏的、跑不出结果,而且这不能让应用 panic;返回值必查有效性。 沙箱与消毒是生命线。13

7. 换栈三件套:数据库、API 形态、传输协议

MongoDB 官方驱动: BSON 转换、CRUD、库/集合管理、schema 验证、 聚合管道、索引、多文档事务、时序集合、地理空间、大文件(GridFS)—— 「在 Rust 里用 MongoDB 毫无妥协」;适合数据不合规整的场景。14

GraphQL(async-graphql): 客户端点名要数据形态时上(第 2 章说过 「真需要再上」);Rust 实现给每个主流框架都配了集成 crate (Poem/Actix/Warp/Tide/Rocket/Axum)。 配置 GraphQL 不是小事:灵活不能白拿——「提供合理的灵活性,同时限制 复杂度和深度,以防 DoS」(恶意深层查询能拖死服务器)。15

gRPC(Tonic): Google 出身、多语言多平台,把远程函数调用得像本地 一样(RPC 的本义);流式、性能、统计、追踪、中间件全家桶,天生配微服务。 Tonic 已实现,且 Axum 对 Tonic 及其中间件有原生支持——老熟人再会。16

8. 主线例子走一遍:智能查重小助手

  用户(法语界面)问:「这张新票是不是重复的?」
      │  ① i18n:界面按 fr-FR locale 渲染;问题文本进 user prompt
      ▼
  后端:reqwest 不必出场——直接 tantivy parse_query(标题+正文,^加权标题)
      → TopDocs 取 5 张候选票
      │  ② 索引若没 commit 过新票,这里就漏——索引与库的同步是隐形契约
      ▼
  OpenAI:system prompt(「你是工单查重助手,只输出 JSON」)
      + user prompt(候选票列表+新票)→ completion 回比对结论
      │  ③ openai crate alpha;API key 走环境变量
      ▼
  回答按 locale 渲染回法语界面
  ——若客户后来要「自己定义查重规则」:把候选暴露给 boa_engine 脚本;
     若数据变得很不规整:评估 Mongo;若客户端要自选字段:上 async-graphql;
     服务间通信要流式强契约:换 Tonic。

  图说:一条用户请求,把六个扩展方向串成决策链。
  (候选数、prompt 内容为演示编的;crate 名与机制出自原文。)

9. 作者的判断与证据(分开标)

有出处的: CLDR/ICU 的分工、复数形式陷阱、tantivy 性能对比与 STORED 语义、 GraphQL 的 DoS 警告、脚本引擎安全清单,均为原书原文3891513。

作者自己的评估:

  • 「先不装,用例被证明再加」的克制原则17;
  • 「awesome/crates.io + 最近更新时间」判断生态活跃度的方法;
  • openai crate 处于 alpha 的时效性警告7。

10. 边界与局限

  • 全是导览没有深挖: i18n 的复数规则引擎、GraphQL 的 N+1 与 DataLoader、 gRPC 的 proto 契约治理,各自都是一本书;
  • OpenAI 部分时效极强: 模型名、端点、crate API 到 2026 年多半已变—— 概念(system/user prompt、completion)不变;
  • 脚本引擎的安全清单只有原则没有实现: 真正的沙箱隔离(资源限额、 超时、API 面收敛)需要远比书中示例多的工程。

11. 可带走的

主线例子一行走完: 法语提问 → tantivy 查候选 → OpenAI 比对 → locale 渲染;后续分叉:脚本自定义 → Mongo → GraphQL → Tonic。

  1. locale 是规则包(标点/货币/日期/方向),语言标签必须带地区。
  2. 翻译不是替换字符串:变量与复数形式是硬骨头,交给 CLDR/ICU(icu crate)。
  3. 出向 HTTP 用 reqwest; 大名鼎鼎的服务往往有专属 crate。
  4. OpenAI 三词:system prompt 定人设、user prompt 派活、completion 是回答; key 走环境变量。
  5. 全文搜索 = 索引 + 查询语言;tantivy 纯 Rust 且快过 Lucene; STORED 决定能不能取回字段;commit 才发布文档。
  6. 查询语言强到会解析失败——Result 先查后用。
  7. 嵌入脚本引擎买的是「逻辑与应用的解耦交付」,付的是安全的钱: 沙箱、消毒、假定脚本必坏、返回值必查。
  8. 换栈三件套记名:Mongo 官方驱动、async-GraphQL(限深度防 DoS)、Tonic(gRPC)。
  9. 扩展组件「用例被证明再加」——和第 2 章 YAGNI、第 5 章锤子定律一脉相承。

12. 原文地图

主题 原书章 原文位置
随 locale 变的属性清单 Extending the Application text/84-fm-introduction.txt:68(搜「Punctuation」)
语言标签与地区 同上 text/84-fm-introduction.txt:90(搜「more than one set of rules」)
人工翻译与注入 同上 text/84-fm-introduction.txt:99(搜「professional translators」)
复数形式的坑 同上 text/84-fm-introduction.txt:102(搜「more than one plural」)
CLDR 与 ICU 同上 text/84-fm-introduction.txt:105(搜「Common Locale Data Repository」)、:124(搜「International Components for Unicode」)
icu crate 示例 同上 text/84-fm-introduction.txt:145(搜「DateTimeFormatter」)、:181(搜「12 de septiembre」)
轻量替代 i18ify 同上 text/84-fm-introduction.txt:196(搜「i18ify」)
reqwest 特性 同上 text/84-fm-introduction.txt:202(搜「reqwest」)、:209(搜「Async clients」)
OpenAI 裸调与两种 prompt 同上 text/84-fm-introduction.txt:238(搜「chat/completions」)、:305(搜「System prompt」)、:312(搜「completion」)
openai crate 同上 text/84-fm-introduction.txt:315(搜「alpha」)、:367(搜「ChatCompletion::builder」)
朴素搜索的问题与索引收益 同上 text/84-fm-introduction.txt:428(搜「naive approach」)、:438(搜「highly performant」)、:452(搜「complexity」)
tantivy 与 JVM 之辩 同上 text/84-fm-introduction.txt:482(搜「beating Lucene」)
建索引:schema/STORED/commit 同上 text/84-fm-introduction.txt:505(搜「add_text_field」)、:532(搜「100_000_000」)、:562(搜「commit」)
查询与运算符 同上 text/84-fm-introduction.txt:601(搜「QueryParser」)、:646(搜「Boolean operators」)、:685(搜「Wildcard」)、:695(搜「may fail」)
存原文引用贴士 同上 text/84-fm-introduction.txt:711(搜「jump from the search results」)
boa_engine 五步 同上 text/84-fm-introduction.txt:725(搜「boa_engine」)、:785(搜「Context::default」)、:830(搜「as_number」)
脚本安全清单 同上 text/84-fm-introduction.txt:875(搜「malicious code」)
Mongo 驱动 同上 text/84-fm-introduction.txt:892(搜「BSON」)
async-GraphQL 全家桶与 DoS 同上 text/84-fm-introduction.txt:908(搜「async-graphql-poem」)、:924(搜「denial of service」)
Tonic 与 Axum 同上 text/84-fm-introduction.txt:930(搜「Tonic」)
「用例被证明再加」 同上 text/84-fm-introduction.txt:942(搜「not reasonable to start」)
Points to remember 同上 text/84-fm-introduction.txt:955(搜「ever-growing ecosystem」)
Key terms 同上 text/84-fm-introduction.txt:1093(搜「Index: An optimized storage」)

  1. 出处:「Internationalization and localization」(text/84-fm-introduction.txt:68,搜「Punctuation」)、:90(搜「more than one set of rules is necessary」)。 ↩

  2. 出处:「Applications supporting different locales」(text/84-fm-introduction.txt:96,搜「bare minimum」)、:99(搜「professional translators」)。 ↩

  3. 出处:「Applications supporting different locales」(text/84-fm-introduction.txt:102,搜「more than one plural」)。 ↩↩

  4. 出处:「Applications supporting different locales」(text/84-fm-introduction.txt:105,搜「Common Locale Data Repository」)、:124(搜「International Components for Unicode」);「Rust's support for i18n」(:181,搜「12 de septiembre de 2020」)、:196(搜「i18ify」)。 ↩

  5. 出处:「Calling external APIs」(text/84-fm-introduction.txt:202,搜「reqwest」)、:209(搜「Async clients」)。 ↩

  6. 出处:「OpenAI integration」(text/84-fm-introduction.txt:238,搜「chat/completions」)、:305(搜「System prompt」)、:308(搜「User prompt」)、:312(搜「called a completion」)。 ↩

  7. 出处:「OpenAI integration」(text/84-fm-introduction.txt:315,搜「alpha stage」)、:419(搜「concept will not change」)。 ↩↩

  8. 出处:「Offering full-text search」(text/84-fm-introduction.txt:428,搜「naive approach」)、:438(搜「highly performant」)、:452(搜「increases the complexity」)。 ↩↩

  9. 出处:「Technologies that offer full-text search solutions」(text/84-fm-introduction.txt:482,搜「beating Lucene」);「Offering full-text search」(:505,搜「add_text_field」)、:532(搜「100_000_000」)、:562(搜「commit」)、:711(搜「jump from the search results」)。 ↩↩

  10. 出处:「Offering full-text search」(text/84-fm-introduction.txt:646,搜「Boolean operators」)、:685(搜「Wildcard」)、:691(搜「Word distance」)、:695(搜「may fail」)。 ↩

  11. 出处:「Embedded script engine」(text/84-fm-introduction.txt:722,搜「Internet of Things」)、:728(搜「recompile」)。 ↩

  12. 出处:「Embedded script engine」(text/84-fm-introduction.txt:785,搜「Context::default」)、:825(搜「test(arg)」)、:830(搜「as_number」)、:862(搜「heavy lifting」)。 ↩

  13. 出处:「Embedded script engine」(text/84-fm-introduction.txt:875,搜「malicious code」)。 ↩↩

  14. 出处:「MongoDB's Rust driver」(text/84-fm-introduction.txt:892,搜「Binary JSON」)。 ↩

  15. 出处:「Offer GraphQL endpoints」(text/84-fm-introduction.txt:901,搜「database agnostic」)、:908(搜「async-graphql-poem」)、:924(搜「denial of service」)。 ↩↩

  16. 出处:「Leveraging serialized communication with gRPC」(text/84-fm-introduction.txt:930,搜「Tonic」)。 ↩

  17. 出处:「Conclusion」(text/84-fm-introduction.txt:942,搜「not reasonable to start by adding」)。 ↩