- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
Read the Docs(即本仓库 readthedocs.org 所驱动的平台)为所有项目的所有页面提供基于 Elasticsearch 的全站全文搜索能力,即Server Side Search(服务端搜索)。它取代了各文档构建工具自带的"客户端搜索",让搜索结果精确落到每个标题(heading)锚点,并支持跨项目、跨子项目检索、自定义排名、特殊查询语法与完整 REST API。本文基于 docs/user/server-side-search/index.rst 及其姊妹文档 语法说明、API 参考 展开,并结合仓库源码(readthedocs/search/目录)深入讲解底层实现,帮助读者掌握从搜索特性、查询语法、API 调用到配置调优的完整实战方案。
一、什么是服务端搜索
Read the Docs 的搜索被称为"服务端搜索"(Server Side Search),因为它由 Read the Docs 平台在服务端完成:文档构建完成后,平台解析生成的 HTML 页面并写入 Elasticsearch 索引;用户搜索时,请求直接打到平台提供的搜索 API / 搜索界面,而不是在浏览器里用 JavaScript 遍历文档自带的静态索引文件。
这一设计带来几个关键优势:
- 覆盖全部项目与版本:所有项目的所有公开页面都被索引,搜索范围不再局限于当前阅读的文档。
- 按标题精确命中:平台索引页面中的每个标题(heading),因此搜索结果可以精确到文档中的具体小节,并带有可跳转的锚点。
- 统一体验:无论底层是 Sphinx、MkDocs 还是其他构建工具,搜索界面与交互保持一致。
在仓库中,搜索功能由readthedocs/search/模块承载:索引文档定义在 readthedocs/search/documents.py,Elasticsearch 查询构造在 readthedocs/search/faceted_search.py,HTML 解析在 readthedocs/search/parsers.py,API 视图在readthedocs/search/api/下。Elasticsearch 连接配置位于 readthedocs/settings/base.py:默认通过ELASTICSEARCH_DSL指向search:9200服务,page_index索引默认 1 个分片、1 个副本,并关闭了ELASTICSEARCH_DSL_AUTO_REFRESH以提升索引写入性能。
二、核心搜索特性
1. 跨子项目搜索
Read the Docs 的子项目(Subprojects)机制允许把多个独立项目托管在同一个域名下。服务端搜索默认把主项目同域名下的所有子项目一并纳入主项目的搜索结果。在 API v2 的旧实现中,这一行为写死在视图逻辑里(readthedocs/search/api/v2/views.py 会遍历Project.objects.filter(superprojects__parent_id=...)把每个子项目加入搜索集合,子项目若不存在同名版本则回退到其默认版本)。
提示:在 API v3 中,搜索主项目时不会自动包含子项目结果,如需包含需显式使用
subprojects:参数(详见下文"查询语法"章节)。
2. 搜索结果精确落到目标内容
平台会索引文档中的每个标题,所以搜索结果可以精确到标题所在的小节,并附带id锚点供直接跳转。这一能力来自索引结构:PageDocument中除页面标题(title)外,还有sections嵌套字段,包含每个小节的id、title与content(readthedocs/search/documents.py),小节内容还启用了with_positions_offsets词向量以加速大文档的高亮。解析时,readthedocs/search/parsers.py 的_parse_sections会把每个小节(含子小节)组织成结构化对象,把标题之下、下一个标题之前的内容归入该小节。
3. 完全控制结果排序(自定义排名)
平台允许通过配置文件为每个页面设置自定义排名(rank),用于让用户始终先看到相关内容、或让旧内容自然下沉。配置项为search.ranking,在项目根目录的.readthedocs.yaml配置文件中设置(详见配置文件 v2 参考):
version: 2 search: ranking: api/v1/*: -1 api/v2/*: 4 ignore: - 404.htmlsearch.ranking的类型是"模式到排名"的映射,默认{}:
- 模式匹配的是构建产物 HTML 的相对路径,例如应写
index.html,而不是docs/index.rst或/en/latest/index.html; - 模式支持通配符:
*匹配一切(含斜杠)、?匹配任意单个字符、[seq]匹配seq中的任意字符; - 排名取值范围为-10 到 10(含端点),越接近 -10 结果越靠后,越接近 10 结果越靠前;
0表示正常排名而不是"无排名"; - 若多个模式同时命中同一页面,最后一个匹配的模式生效;
- 实践建议:优先降低要废弃页面的排名,而不是抬高其他所有页面的排名。
对应的search.ignore用于把匹配的页面彻底排除出搜索索引,类型为模式列表,默认值为['search.html', 'search/index.html', '404.html', '404/index.html']。
这些配置在构建时由 readthedocs/projects/tasks/search.py 落实:process()中先用search_ignore的模式(fnmatch)过滤掉被忽略的页面,再按倒序遍历search_ranking,让最后一个匹配生效(第 79-82 行),最后把页面按每 100 个一批写入索引。
排名在查询阶段的实现位于 readthedocs/search/faceted_search.py:PageSearch.query使用 Elasticsearch 的FunctionScore查询,通过脚本把用户设置的rank(-10~10,共 21 个取值)映射为权重系数[0.01, 0.05, ..., 1, 1.3, ..., 2],再乘上原始相关性分数。映射表的设计保证:最低档 0.8 能把sections.title^2的最高分拉低到接近title^1.5,最高档 1.3 能把最低分抬高到接近最高分——确保"精确命中"始终优先于"排名靠前"的页面。
4. 跨你有权限的项目搜索
在 Dashboard 中,可以一次性搜索所有你有权限访问的项目,不必再为"记不清文档在哪个项目里"发愁。这对应查询参数user:@me,例如:
user:@me test在源码中,SearchExecutor._get_projects_from_user通过Project.objects.for_user(user=request.user)枚举当前用户有权限的项目,并为每个项目取默认版本进行检索(readthedocs/search/api/v3/executor.py)。
5. 特殊查询语法
平台支持完整的查询语法,包括精确短语、前缀、模糊匹配等(详见下文"查询语法"章节)。
6. 可配置
搜索行为可以通过两种途径配置:
- 配置文件:通过
.readthedocs.yaml中的search段配置排名与忽略列表; - 项目设置界面:在项目 Dashboard 的
Settings→ 左侧栏Search中,可开关两个选项:Enable search modal:控制文档页面中是否展示 Read the Docs 搜索弹窗;Show subprojects filter in search modal:控制读者是否可以在搜索弹窗内按子项目过滤结果。
这两个开关对应Project.addons模型上的字段(readthedocs/projects/models.py):search_enabled(默认True)与search_show_subprojects_filter(默认True),另有search_default_filter用于设定默认过滤条件。
7. 开箱即用
平台会覆盖 Sphinx 项目默认的搜索,使上述能力自动生效;若服务端搜索没有返回任何结果,会自动回退到项目内置搜索,避免遗漏。
8. API
搜索能力完全可以通过 API 集成(详见下文"API"章节)。
9. 分析(Analytics)
平台会记录用户的搜索行为,帮助了解用户正在搜索什么,详见搜索分析文档。每次搜索请求都会在 readthedocs/search/api/v3/views.py 的_record_query中被异步记录(通过tasks.record_search_query_batch.delay),包含查询词、涉及的项目/版本列表与结果总数。
三、搜索查询语法
1. 参数(Parameters)
参数形式为name:value,可以出现在查询语句的任意位置;除user外,project与subprojects均可重复出现多次。除参数外的其他文本都会作为搜索内容。如果不想让某个词被解析为参数,可以用反斜杠转义,例如project\:docs。未知参数(如foo:bar)不会被当作参数,无需转义。
| 参数 | 作用 | 示例 |
|---|---|---|
project | 指定要搜索的项目与版本(不含子项目与翻译)。未指定版本时使用该项目默认版本;可重复出现 | project:docs test、project:docs/latest test、project:docs/stable project:dev test |
subprojects | 搜索指定项目及其所有子项目。未指定版本时各项目均用默认版本;指定版本时,拥有该版本的子项目用该版本,没有的则回退默认版本;可重复出现 | subprojects:docs test、subprojects:docs/latest test、subprojects:docs/stable subprojects:dev test |
user | 搜索当前用户(@me)有权限访问的项目;仅支持@me,且只能出现一次,重复时后者覆盖前者 | user:@me test |
参数解析由SearchQueryParser实现(readthedocs/search/api/v3/queryparser.py):它按空白拆分查询串,把形如name:value且name在allowed_arguments(project/subprojects为列表型,user为字符串型)中的片段识别为参数,其余为普通文本;转义后的\:会被还原为:。项目与版本通过{project}/{version}形式拆分(_split_project_and_version,readthedocs/search/api/v3/executor.py),未提供版本时自动使用项目的默认版本。
权限(Permissions)
若用户对某个版本没有权限,或该版本不存在,则该版本不会出现在结果中。API 响应会返回最终搜索实际使用的所有项目,便于前端展示"本次搜索覆盖了哪些项目"。
限制(Limitations)
- 单次搜索最多涉及100 个项目,超出部分会被忽略;
- 该语法仅在使用API v3或全局搜索(
https://app.readthedocs.org/search/)时可用; - 同一项目搜索多个版本不受支持,后出现的版本会覆盖之前的版本。
在源码中,100 项目上限由SearchExecutor的max_projects=100实现:projects属性用islice(..., self.max_projects)截断,并用 dict 保证"每个项目只保留一个版本"(readthedocs/search/api/v3/executor.py)。
2. 特殊查询(Special Queries)
Read the Docs 使用 Elasticsearch 的Simple Query String查询,查询越复杂,结果越精确。对应源码在 readthedocs/search/faceted_search.py:文本查询同时以and与or两种默认操作符构造查询,and命中的得分更高;并为模糊匹配设置了fuzzy_prefix_length=1、fuzzy_max_expansions=15以避免复杂查询超时。
支持的语法:
- 精确短语:用双引号包裹,只返回短语完全匹配的结果。例如
"custom css"、"adding a subproject"、"when a 404 is returned"。 - 前缀查询:词尾加
*,返回包含该前缀词的结果。例如test*、build*。 - 模糊匹配(Fuzziness):词后加
~N表示编辑距离,适合拼写不确定的场景。例如doks~1、test~2、getter~2。 - 词距接近(Proximity):短语后加
~N匹配彼此接近的词。例如"dashboard admin"~2、"single documentation"~1、"read the docs policy"~5。
值得说明的是,查询是否按"高级语法"处理并非只看长度:_is_advanced_query会检测查询中是否含+、|、-、"、*、(、)、~等 Simple Query String 特殊符号(readthedocs/search/faceted_search.py)。此外,当查询为单个词且未使用高级语法时,在DEFAULT_TO_FUZZY_SEARCH特性开关下会走模糊 + 通配符(Wildcard,词尾补*)查询,以支持部分词与子串匹配(第 126-155 行)。
四、搜索 API
1. API v3(推荐)
端点:GET /api/v3/search/,返回指定项目或项目子集的搜索结果列表,结果按小节(section)切分并带有命中词高亮。
请求参数:
| 参数 | 说明 |
|---|---|
q | 搜索查询词(语法见上文,也支持project:、subprojects:、user:参数) |
page | 跳转到指定页 |
page_size | 每页结果数,默认 50 |
响应字段:
| 字段 | 说明 |
|---|---|
type | 结果类型,目前只有page |
project | 项目对象(含slug与alias) |
version | 版本对象(含slug) |
title | 页面标题 |
domain | 结果页面的规范域名 |
path | 结果页面路径 |
highlights | 含命中词子串的对象;文本为 HTML 转义后的内容,命中词包在<span>标签内 |
blocks | 页面内的结果块列表,当前仅section类型(带id锚点的页面小节) |
⚠️安全警告:除
highlights外,响应中其他内容不会做 HTML 转义,集成方在把内容渲染进页面之前必须自行转义,以防 XSS。
示例请求(bash):
curl "https://app.readthedocs.org/api/v3/search/?q=project:docs%20server%20side%20search"示例请求(Python):
import requests URL = 'https://app.readthedocs.org/api/v3/search/' params = { 'q': 'project:docs server side search', } response = requests.get(URL, params=params) print(response.json())示例响应:
{ "count": 41, "next": "https://app.readthedocs.org/api/v3/search/?page=2&q=project:docs%20server+side+search", "previous": null, "projects": [ {"slug": "docs", "versions": [{"slug": "latest"}]} ], "query": "server side search", "results": [ { "type": "page", "project": {"slug": "docs", "alias": null}, "version": {"slug": "latest"}, "title": "Server Side Search", "domain": "https://docs.readthedocs.io", "path": "/en/latest/server-side-search.html", "highlights": { "title": ["<span>Server</span> <span>Side</span> <span>Search</span>"] }, "blocks": [ { "type": "section", "id": "server-side-search", "title": "Server Side Search", "content": "Read the Docs provides full-text search across all of the pages of all projects, this is powered by Elasticsearch.", "highlights": { "title": ["<span>Server</span> <span>Side</span> <span>Search</span>"], "content": ["You can <span>search</span> all projects at https://readthedocs.org/<span>search</span>/"] } }, { "type": "domain", "role": "http:get", "name": "/_/api/v2/search/", "id": "get--_-api-v2-search-", "content": "Retrieve search results for docs", "highlights": { "name": [""], "content": ["Retrieve <span>search</span> results for docs"] } } ] } ] }v3 的视图实现在 readthedocs/search/api/v3/views.py:SearchAPI要求q参数必填(_validate_query_params),匿名与登录用户分别使用SearchAnonRateThrottle/SearchUserRateThrottle,限流为100 次/分钟(RATE_LIMIT = "100/minute")。响应顶层的projects与query字段由_add_extra_fields在序列化后补充:projects列出最终参与搜索的项目与版本,query为去除参数后的实际查询词(readthedocs/search/api/v3/views.py)。
此外,v3 响应会通过_add_cache_tags为所有参与搜索的项目打上Cache-Tag(项目 slug、{project}/{version}与rtd-search标签),使 CDN 能在文档更新或索引重建(rtd-search)后精确失效缓存;为避免超出 CDN/nginx 头部大小限制,标签总量被限制在 2000 字符内(第 124-157 行)。
项目/版本在响应中成为对象:v3 的PageSearchSerializer将project序列化为{slug, alias}、version序列化为{slug},并移除project_alias字段(readthedocs/search/api/v3/serializers.py)。
2. 从 API v2 迁移
v2 用独立查询参数指定项目与版本,v3 改为在查询词内声明。迁移映射关系如下:
| v2 参数 | v3 写法 |
|---|---|
project: docs、version: latest、q: test | q: project:docs/latest test |
v3 响应与 v2 非常相似,主要变化:
project由字符串变为对象;version由字符串变为对象;- 不再有
project_alias字段(已并入project对象)。
另一个关键行为差异:在 v3 中,搜索父项目不会自动包含子项目结果,需要显式传入subprojects参数。
3. 认证与授权
如果项目使用私有版本(Private versions),用户只能搜索自己有权限的项目。认证授权基于当前会话或任何有效的分享方式(sharing)(可通过/api/v3/projects/<project_slug>/sharing/以编程方式创建与撤销分享,见 API v3 文档)。
要使用当前用户会话,必须从文档所服务的域名调用 API,即<you-docs-domain>/_/api/v3/search/。例如项目https://docs.readthedocs-hosted.com/对应的搜索端点为https://docs.readthedocs-hosted.com/_/api/v3/search/。在开源版实现中,权限检查由SearchExecutor._has_permission与_get_project_version(内部版本管理器 +public()过滤 + 隐藏版本控制)共同完成(readthedocs/search/api/v3/executor.py),商业版(.com)会覆写权限逻辑以接入其认证后端。
4. API v2(已废弃)
⚠️ 请优先使用 API v3,迁移说明见上文。v2 端点仍可使用但已废弃。
端点:GET /api/v2/search/,返回某项目(含其子项目)的搜索结果。
请求参数:q(查询词)、project(项目 slug)、version(版本 slug)、page、page_size(默认 50)。
响应字段:与 v3 基本一致,但project、version、project_alias均为字符串;project_alias在结果为子项目时表示其别名。
示例请求(bash):
curl "https://app.readthedocs.org/api/v2/search/?project=docs&version=latest&q=server%20side%20search"示例请求(Python):
import requests URL = 'https://app.readthedocs.org/api/v2/search/' params = { 'q': 'server side search', 'project': 'docs', 'version': 'latest', } response = requests.get(URL, params=params) print(response.json())示例响应:
{ "count": 41, "next": "https://app.readthedocs.org/api/v2/search/?page=2&project=read-the-docs&q=server+side+search&version=latest", "previous": null, "results": [ { "type": "page", "project": "docs", "project_alias": null, "version": "latest", "title": "Server Side Search", "domain": "https://docs.readthedocs.io", "path": "/en/latest/server-side-search.html", "highlights": { "title": ["<span>Server</span> <span>Side</span> <span>Search</span>"] }, "blocks": [ { "type": "section", "id": "server-side-search", "title": "Server Side Search", "content": "Read the Docs provides full-text search across all of the pages of all projects, this is powered by Elasticsearch.", "highlights": { "title": ["<span>Server</span> <span>Side</span> <span>Search</span>"], "content": ["You can <span>search</span> all projects at https://readthedocs.org/<span>search</span>/"] } } ] } ] }v2 视图要求q、project、version三个参数全部必填(readthedocs/search/api/v2/views.py),权限校验使用IsAuthorizedToViewVersion,并自动把子项目并入搜索范围。
关于商业版(Read the Docs for Business):若使用
app.readthedocs.com,上述所有示例 URL 中的https://app.readthedocs.org/需替换为https://app.readthedocs.com/;使用私有版本时还需检查上文"认证与授权"小节。
五、边输入边搜索(Search as you type)
搜索弹窗支持边输入边搜索(as-you-type),用户在输入过程中即可看到候选结果,快速定位目标内容;同时会保存最近搜索记录,便于日后回看。在文档页面按/(正斜杠)即可唤起搜索弹窗并开始输入。
六、配置搜索
搜索选项可在项目Dashboard中配置:
- 进入 Dashboard;
- 点击项目名称;
- 进入
Settings; - 在左侧栏选择
Search。
在该页面可以切换:
- Enable search modal(启用搜索弹窗):控制文档中是否展示 Read the Docs 搜索弹窗;
- Show subprojects filter in search modal(在搜索弹窗中显示子项目过滤器):控制读者能否按子项目过滤搜索结果。
对应的数据库字段(Project.addons)定义在 readthedocs/projects/models.py,默认均为开启。项目级的search_indexing_enabled(第 587 行附近)则控制该项目是否参与搜索索引:PageDocument.get_queryset会过滤掉未开启索引、被标记 ignore、delisted(除名)或 spam 的项目与页面(readthedocs/search/documents.py)。
除此之外,还可以通过.readthedocs.yaml的search段做更细粒度的内容级控制(排名与忽略),完整 schema 与示例见配置文件 v2 参考。
七、主内容(Main Content)如何被识别
服务端搜索只索引 HTML 页面的主内容区域,忽略页头、页脚、导航等与文档内容无关的元素,从而保证结果聚焦,并避免导航菜单等重复元素污染相关性。主内容节点的识别顺序如下(优先级从高到低):
- 带
role="main"属性的元素(ARIA 角色,被众多静态站点生成器与主题使用); <main>HTML5 标签;- 首个
<h1>标签的父节点(假定所有小节是兄弟节点、共用一个父容器); <body>标签(兜底)。
这一逻辑在解析器中与文档完全对应(readthedocs/search/parsers.py 的_get_main_node),实现顺序为css_first("[role=main]")→css_first("main")→ 首个h1的容器父节点 →body。细节上还有一个巧妙的处理:若h1的父标签是header,会先返回header标签作为标题容器(_get_header_container),再取其父节点,以兼容主题中"标题被包在 header 里"的结构。
完整说明(含 HTML 结构示例与检测优化建议)见主内容检测参考。概括要点:
- 为提升识别准确率(同时改善无障碍体验),建议在主题主内容容器上加
role="main"、使用<main>标签、并确保主内容区有至少一个<h1>标题; - 若自动检测失败,可在项目
Settings→Addons→Advanced中填写CSS main content selector(例如div#main或.my-content),留空则使用自动检测。注意该自定义选择器目前只影响 Visual Diff 与 Link Previews(视觉差异、链接预览),不影响搜索索引; - 避免使用
body这类过于宽泛、或匹配多个节点的选择器。
八、索引与排序原理(源码视角)
1. 索引文档结构
页面索引文档PageDocument(readthedocs/search/documents.py)包含:
- 元数据:
project、version、doctype(文档类型)、path、full_path、rank(用户自定义排名); - 可搜索内容:
title、嵌套的sections(每节含id、title、content); - 文本字段采用
simple分析器(按非字母字符切分,如python.submodule会被切为[python, submodule]),sections.content启用with_positions_offsets词向量以加速高亮; prepare_rank会校验排名是否在 -10~10 之间,超出则归零;- 查询集过滤掉未开启索引、ignore、delisted、spam 的内容。
2. 查询构造与字段加权
PageSearch(readthedocs/search/faceted_search.py)对两类字段分别构造查询:
- 外层字段
_outer_fields = ["title^1.5"]; - 嵌套小节字段
_section_fields = ["sections.title^2", "sections.content"](通过Nested查询并限制inner_hits大小为 3,即每页最多返回 3 个命中小节)。
之后用Bool(should=...)组合,再用FunctionScore脚本把rank映射为分数权重(见上文"自定义排名"小节),最终让相关性分数与用户排名共同决定结果顺序。
3. 搜索分析与数据记录
每次 API 搜索都会被异步记录(tasks.record_search_query_batch.delay),这些数据正是搜索分析报表的来源,使项目维护者可以了解用户的搜索意图、并据此优化文档结构或排名配置。
九、使用建议与最佳实践
- 优先使用 API v3:新集成请直接使用
/api/v3/search/,将项目、版本约束写进q参数(project:slug/version、subprojects:slug、user:@me),并善用响应中的projects与query字段做前端展示。 - 集成时务必自行转义:除
highlights外的响应内容未经 HTML 转义,渲染前需自行转义以防 XSS。 - 合理利用排名而非删除:想"弱化"旧内容时优先降低
search.ranking,而非删除页面;想彻底隐藏页面再用search.ignore。 - 注意搜索范围差异:v3 中父项目搜索默认不含子项目;子项目搜索需用
subprojects:参数并留意"版本缺失时回退默认版本"的规则。 - 主内容检测遵循 ARIA 约定:为你的主题添加
role="main"或<main>,既能提升搜索索引的准确性,也能改善站点可访问性。
通过上述特性、语法、API 与配置的组合,无论是普通项目维护者还是需要深度集成搜索能力的开发者,都能在 Read the Docs 上构建出精确、可控、可分析的全站搜索体验。
- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
相关推荐
Read the Docs 服务端搜索查询语法完整指南:参数过滤、转义规则与 Elasticsearch 高级检索
Read the Docs 服务端搜索查询语法完整指南:参数过滤、转义规则与 Elasticsearch 高级检索 服务端搜索(Server Side Sear
后端文档Read the Docs 服务端搜索深度解析:Elasticsearch 索引、重建与查询的完整机制
Read the Docs 服务端搜索深度解析:Elasticsearch 索引、重建与查询的完整机制 Read the Docs 使用 Elasticsear
后端文档ToolJet Table 组件服务端搜索(Server Side Search)完整指南:从 SQL 查询到事件链路
ToolJet Table 组件服务端搜索(Server Side Search)完整指南:从 SQL 查询到事件链路 本篇指南讲解如何在 ToolJet 的
低代码后端前端AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考