- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
本文基于 Read the Docs 的设计文档 In-doc search UI 展开,完整解读“边输入边搜索”(search as you type)功能的设计目标、前端主题无关架构、后端 Elasticsearch 候选方案的优劣对比,以及里程碑规划;并结合当前仓库中 搜索 API v3、doc-embed 前端注入脚本 等源码,说明这一设计最终在 Read the Docs 中的落地形态。读完本文,你将理解一个面向数千个第三方主题的“主题无关”搜索 UI 该如何设计,以及前端事件流、ES 分词器选型、特性开关(Feature Flag)在这一功能中的具体作用。
一、背景:为托管文档提供即时的搜索体验
Read the Docs 托管了大量 Sphinx 文档,让读者能快速找到所需信息是其核心诉求之一。设计文档的开篇即说明了出发点:
- 平台已经升级到最新版本的 Elasticsearch;
- 计划为所有托管文档实现 “search as you type” 能力——用户在搜索框中一开始输入,就立即获得建议结果,并以简洁、极简的前端呈现;
- 该设计是一个 GSoC 2019 项目,文档本身即是对该功能的详细设计说明。
原文明确标注了一个警告框:
本设计文档描述的是尚未实现的未来功能(原文写作时点)。
需要说明的是,该文档定稿于 2019 年,其“未实现”的状态仅指当时的时间线。从当前仓库看,search-as-you-type 能力后来以“搜索弹窗(search modal)”等形式落地,本文第四节会给出仓库内的实现证据。理解这一点,可以把本文当作“设计思路 + 后续演进”来读,而不是孤立的历史文档。
原文还引用了一个演示动图(/_static/images/design-docs/in-doc-search-ui/in-doc-search-ui-demo.gif),但该文件已不在当前仓库的图片目录中(docs/_static/images/design-docs/ 下目前仅有 flyout 相关截图),因此本文不插入该图。
二、目标与非目标(Goals and Non-Goals)
设计文档给出了清晰的目标边界,这是整个方案可行性的前提:
项目目标
- 支持 search-as-you-type / 自动补全界面——核心功能目标;
- 兼容所有(或几乎全部)Sphinx 主题——Read the Docs 上各项目使用各自主题的搜索框,UI 必须“主题无关”(theme agnostic);
- JavaScript 体验向下兼容到 IE11,无法支持时优雅降级(graceful degradation);
- 项目维护者需要能够显式开启(opt-in)或关闭(opt-out)该功能——这是平台级功能的基本礼仪;
- (可选)允许维护者通过自定义 CSS / JS 文件调整部分样式,保留定制自由度。
非目标
- 首期只面向 Sphinx 文档:原因是 Read the Docs 并未把 MkDocs 文档索引进 Elasticsearch 索引。这条非目标直接界定了首期范围——任何依赖 ES 索引的行为都不覆盖 MkDocs 站点。
三、现有搜索实现基础
设计并非从零开始。文档指出,平台已有详细的服务器端搜索(Server Side Search)架构说明,包括如何把文档索引进 Elasticsearch,详见 Server side search。该文档同时列出了平台搜索的既有能力,例如:
- 跨子项目(subprojects)搜索;
- 按文档标题/章节索引,命中结果直达具体小节;
- 通过 配置文件 的
search配置项自定义排序(search.ranking)、开关搜索弹窗等; - 平台搜索无结果时回退到项目内置的 Sphinx 搜索。
这些既有能力构成了 in-doc search UI 的地基:前端只需对接已有的搜索 API,重点解决“即时性”与“跨主题呈现”两个新问题。搜索语法与 API 的完整说明见 server-side-search/syntax 与 server-side-search/api。
四、前端设计:主题无关的 vanilla JS 方案
4.1 技术选型
前端要求“主题无关”,因为每个托管项目可能使用任意的 Sphinx 主题。设计文档说明团队探索过多个第三方库,但没有一个完全满足需求,因此倾向于使用 vanilla JavaScript(原生 JS)实现,其相对第三方库的优势是:
- 对 DOM 有更强的控制力;
- 性能收益(无额外库开销)。
4.2 交互架构
原文给出的实现路径是:
- 利用 JavaScript 的
querySelector()选中“每个主题都存在的搜索框”; - 为其添加事件监听器,监听输入变化(change/input 事件);
- 一有变化就向平台后端发起搜索查询,后端返回建议(suggestions);
- 用
document.createElement()和node.removeChild()动态创建/移除节点,刻意避免在 DOM 中留下空的<div>残留。
这套“监听输入 → 请求 API → 动态渲染建议”的链路,正是典型 search-as-you-type 客户端模式,对后端的要求是低延迟、可高频调用(这一点对第六节的后端选型至关重要)。
4.3 CSS / JS 的分发方式
文档列出了把所需 JS/CSS 注入到所有托管项目的三种候选方案:
- 直接写入现有 embed 文件:把 CSS 加进
readthedocs-doc-embed.css、JS 加进readthedocs-doc-embed.js,随现有机制自动包含。当前仓库中确实存在这类分发文件,例如 readthedocs-doc-embed.js(webpack 打包后的产物)与 readthedocs-doc-embed.css; - 独立打包:把 in-doc search 打包成自包含的 CSS/JS 文件,以与
readthedocs-doc-embed.*类似的方式引入; - 打包成 Sphinx 扩展:按项目粒度开启最方便;准备向更大范围推广时,可以再决定是“默认全量开启”还是“像 404 扩展(sphinx-notfound-page)那样作为 opt-in 功能”提供。
4.4 仓库现状佐证:doc-embed 如何接管 Sphinx 搜索
从当前仓库的 readthedocs-doc-embed.js 源码结构看,embed 脚本的 search 模块已经实现了“主题无关接管搜索”的完整模式,可以印证上述设计思路的实际落地:
- 通过
window.READTHEDOCS_DATA获取project、version、language等运行时数据,并检测docsearch_disabled特性开关(对应目标 4 的 opt-out 能力); - 仅在 Sphinx 构建器(
is_sphinx_builder())上启用,MkDocs 项目打印 “Server side search is disabled.”——与设计文档“首期仅 Sphinx”的非目标一致; - 劫持 Sphinx 主题暴露的
Search.query全局函数:保存原函数为Search.query_fallback,替换为调用平台搜索 API(proxied_api_host + "/api/v2/search/?q=...&project=...&version=...")的异步 fetch;请求失败或无结果时回退Search.query_fallback,即项目内置的 Sphinx 搜索; - 结果渲染使用
document.createElement生成li/a/div.context等节点,对highlights字段做 XSS 防护(SafeString包装 +innerHTML),并给高亮<span>统一加highlighted类——正是设计文档中“动态创建/移除节点、高亮匹配词”思想的实现。
这说明前端侧的设计(主题无关、劫持既有搜索入口、高亮渲染、优雅回退)已经完整进入生产代码,只是交互形态从“搜索框下方建议列表”演进为下一节所述的搜索弹窗。
五、UI/UX:两种建议呈现方式
文档给出了展示建议的两种交互方案:
- 搜索框下方下拉建议(传统 autocomplete 形态);
- 点击搜索字段时打开全屏(full page)搜索界面。
从仓库现状看,Read the Docs 最终采用的是方案 2 的变体:Server side search 的 “Search as you type” 一节描述了当前产品形态——搜索弹窗支持边输入边出结果,并保存最近搜索;用户按/键即可唤起。是否选择全屏界面,还涉及目标 2(兼容所有主题):下拉方案更容易被各主题的布局(z-index、容器裁剪、响应式)破坏,全屏/弹窗方案的 DOM 挂载点可控性更强。原文并未给出最终结论,属于“可以推断”的演进方向。
六、后端设计:两种 ES 候选方案对比
search-as-you-type 对后端的本质要求是:高频、小查询量下仍能低延迟返回“前缀/部分前缀匹配”的建议。文档对比了 Elasticsearch 的两种机制:
6.1 Edge NGram Tokenizer(边沿 n-gram 分词器)
| 优点 | 缺点 |
|---|---|
| 对“可能以任意顺序出现的单词”做自动补全时,比 Completion Suggester 更有效 | 需要更大的磁盘空间 |
| 相当快:大部分工作在索引时完成,自动补全耗时低 | |
| 支持对匹配词做高亮(highlighting) |
6.2 Completion Suggester(完成建议器)
| 优点 | 缺点 |
|---|---|
| 为速度优化,非常快 | 匹配总是从文本开头开始:"Hel"能匹配"Hello, World",但匹配不到"World Hello" |
| 不需要大磁盘空间 | 不支持匹配词高亮 |
| 官方文档指出:快速查找的构建代价高,且数据保存在内存中 |
两者的核心取舍可以概括为:Edge NGram 用磁盘换“任意位置前缀匹配 + 高亮”,Completion Suggester 用“仅词首匹配”换低开销,但其 in-memory 特性在大索引下反而是内存隐患。文档将其列为开放问题之一(见第九节),没有在此下定论。
6.3 仓库现状佐证:搜索 API v3 与特性开关
设计文档中“Is our existing Search API sufficient?”的开放问题,在当前仓库中已有了明确答案——专门的 Search API v3:
- readthedocs/search/api/v3/views.py 定义了
SearchAPI视图,其类注释明确指出该 API “会被匿名用户以及我们的 search-as-you-type 扩展使用,因此需要提高限流阈值”,限流设置为100/minute(匿名与已认证用户各一个 throttle,见 views.py)。这直接回应了前端“每次输入变化都发请求”的高频调用场景; - 视图强制
q查询参数,响应额外携带projects与最终query字段(views.py),并对响应打 cache tag(项目 slug + 版本 +rtd-search索引标签),保证文档重建或索引更新时缓存被清除(views.py); - 查询执行前有特性开关判断:readthedocs/search/api/v3/utils.py 的
should_use_advanced_query()通过项目特性Feature.DEFAULT_TO_FUZZY_SEARCH(定义于 readthedocs/projects/models.py)决定该用哪种查询策略。这一 Feature 机制正是设计文档目标 4(维护者可 opt-in/opt-out)在平台层的工程实现方式; - 前端侧的 API 契约见 server-side-search/api:
GET /api/v3/search/接受q、page、page_size(默认 50)参数,返回分页结果,每条含highlights(HTML 转义、匹配词包在<span>中)与blocks(可锚定到具体小节的 section 块)——“高亮匹配词”这一 Edge NGram 的卖点在 API 层已是一等公民。
此外,从源码结构看,search-as-you-type 的形态并不局限于文档阅读页:readthedocs/projects/filters.py 中ProjectListFilterSet的 docstring 说明它同时为 Dashboard 项目列表提供 “search-as-you-type lookup filter”,即平台自身的界面也在复用这一交互模式。而 CHANGELOG.rst 中 “Javascript client: search-as-you-type API response” 的变更记录,也印证了 JS 客户端为 search-as-you-type 响应做过专门适配。
七、里程碑(Milestones)
设计文档附带了 GSoC 2019 时间线,完整继承如下:
| 里程碑 | 截止日期 |
|---|---|
| 项目的本地实现 | 2019 年 6 月 12 日 |
| 在 Read the Docs 托管的测试项目上、基于 RTD Search API 实现 in-doc search | 2019 年 6 月 20 日 |
| 在 docs.readthedocs.io 上实现 in-doc search | 2019 年 6 月 20 日 |
| 友好的用户试用:用户可在自己的文档中启用该功能 | 2019 年 7 月 5 日 |
| 对排名前 10 的 Sphinx 主题做额外 UX 测试 | 2019 年 7 月 15 日 |
| UI 定稿 | 2019 年 7 月 25 日 |
| 改进搜索后端以获得高效、快速的搜索结果 | 2019 年 8 月 10 日 |
里程碑本身透露了两个设计要点:一是先本地、再测试项目、再全站的灰度推广路径(呼应 4.3 的分发方案讨论);二是对 Top-10 主题做专项 UX 测试,说明“跨主题兼容”被当作与功能本身同等重要的验收标准。
八、开放问题(Open Questions)
文档最后留下了四个未决问题,它们也是后续实现走向的线索:
- 依赖 jQuery、第三方库,还是纯 vanilla JavaScript?(从 readthedocs-doc-embed.js 当前仍是 jQuery + 原生 API 混用的打包产物看,仓库现状偏向渐进式原生 JS,但保留 jQuery 兜底;此为从源码结构看出的推断)
- 子项目(subprojects)是否要纳入搜索范围?
- 现有 Search API 是否足够?(由 Search API v3 的落地可以回答:足够,并按 search-as-you-type 的使用模式调整了限流与缓存)
- 采用 edge ngrams 还是 completion suggester?
九、小结
这篇设计文档的价值在于完整呈现了一个平台级搜索 UI 的决策链路:
- 范围收敛:首期只做 Sphinx(非目标),兼容所有主题(目标),IE11 优雅降级;
- 前端模式:
querySelector定位各主题的搜索框 → 监听输入 → vanilla JS 动态渲染建议并自动清理 DOM;分发上优先复用readthedocs-doc-embed.*,或做成可 opt-in 的 Sphinx 扩展; - 后端取舍:Edge NGram(索引期开销、支持任意位置前缀与高亮、占盘大) vs Completion Suggester(更快、占内存、仅词首匹配、无高亮);
- 可控性:以特性开关让维护者 opt-in/opt-out,这正是当前仓库中
Feature机制与docsearch_disabled开关所体现的工程惯例。
对读者而言,这份“设计文档 + 仓库源码”的组合是一个很好的案例:先读 docs/dev/design/in-doc-search-ui.rst 理解 why 与 trade-off,再对照 Search API v3、doc-embed 脚本 与 Server side search 用户文档 理解 how 与 what shipped,即可获得从设计意图到生产实现的完整视角。
- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
相关推荐
如何用Nix构建mermaid-ascii?flake.nix可复现构建指南
如何用Nix构建mermaid ascii?flake.nix可复现构建指南 mermaid ascii 是一款能将 Mermaid 图表直接渲染为终端 ASC
CLI开发工具Read the Docs Embed APIv3:跨站点嵌入文档内容的设计方案与源码实现
Read the Docs Embed APIv3:跨站点嵌入文档内容的设计方案与源码实现 本文围绕 Read the Docs 的设计文档 embed api
后端文档Read the Docs 文档 URL 解析设计:从保留路径问题到 unresolver 的查找实现
Read the Docs 文档 URL 解析设计:从保留路径问题到 unresolver 的查找实现 本文围绕 Read the Docs 仓库中的设计文档
后端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考