尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Read the Docs 新版搜索 API 设计解析:key:value 语法、多项目搜索与实现落地

Read the Docs 新版搜索 API 设计解析:key:value 语法、多项目搜索与实现落地 后端文档【免费下载链接】readthedocs.orgThe source code that powers readthedocs.org项目地址https://gitcode.com/gh_mirrors/re/readthedocs.org点击查看免费下载本篇技术文章基于 Read the Docsreadthedocs.org官方仓库中的设计文档 new-search-api.rst 展开系统讲解其新版V3服务器端搜索 API 的设计目标、key:value查询语法、参数语义project/subprojects/user、缓存与 CORS 策略以及响应结构。读完本文你将理解这套 API 从语法解析SearchQueryParser到执行搜索SearchExecutor的完整实现链路并能在自己的项目或二次开发中正确调用与扩展api/v3/search/端点。一、设计目标为什么需要一套新的搜索 API旧版搜索接口V2将搜索选项存储在数据库相关配置中且一次请求只能围绕单个项目展开。新版 API 的设计文档明确列出了三个核心目标在 API 层面配置搜索而不是把选项放在数据库里——搜索范围通过查询参数动态表达无需后端配置支持一次搜索多个项目/版本——这是新 API 最本质的能力升级例如一条请求同时搜索docs项目的stable版本和dev项目的latest版本让仪表盘Dashboard搜索与 API 使用同一套语法统一搜索体验。从源码结构看这套设计已被完整落地路由注册在 readthedocs/urls.py 中path(api/v2/search/, include(readthedocs.search.api.v2.urls)), path(api/v3/search/, include(readthedocs.search.api.v3.urls)),即 V3 搜索端点为GET /api/v3/search/。视图定义在 readthedocs/search/api/v3/views.py 的SearchAPI类中并且存在一个代理proxied版本ProxiedSearchAPI注册于 readthedocs/api/v3/proxied_urls.py供 .com 商业域名复用认证后端——这与文档中“Dashboard 分 .org / .com 两种搜索行为”的规划相吻合。二、查询语法key:value与转义机制新版 API 的搜索参数通过q查询参数传入采用key:value语法设计文档注明灵感来自 GitHub 等服务的搜索语法。语法规则如下参数值当前不包含空格因此不支持引号包裹值key:value不被支持为避免把普通查询词误解析为参数提供了转义字符例如project\:docs不会被解释为参数而是作为搜索词project:docs参与全文匹配只有当查询恰好包含“合法参数前缀”时才需要转义未知参数如foo:bar会被当作纯文本无需转义所有不匹配合法参数的 token 会被拼接起来组成最终的搜索词。这一语法由 SearchQueryParser 实现合法参数白名单与文档一一对应class SearchQueryParser: Simplified and minimal parser for name:value expressions. allowed_arguments { project: list, subprojects: list, user: str, }解析流程源码 docstring 原样描述分三步按空白字符切分字符串每个 token 按name:value形式做 tokenization只有name在allowed_arguments中才成为ArgumentToken否则是TextToken所有文本 token 拼接后执行_unescape将\:还原为:得到最终搜索词。parse()方法还体现了类型语义project和subprojects是list类型可重复出现user是str类型——重复出现时后者覆盖前者。测试用例 test_queryparser.py 完整覆盖了这些行为例如def test_multiple_project_arguments(self): parser SearchQueryParser(project:foo query project:bar) parser.parse() self.assertEqual(parser.arguments[project], [foo, bar]) self.assertEqual(parser.query, query) def test_escaped_argument(self): parser SearchQueryParser(rproject\:foo project:bar query) parser.parse() self.assertEqual(parser.arguments[project], [bar]) self.assertEqual(parser.query, project:foo query)三、参数详解project指定要搜索的项目与版本指定从哪个项目及其版本返回结果不包含子项目子项目需用下文subprojects参数版本可省略省略时默认使用该项目的默认版本default version可以出现一个或多个project参数但至少需要提供一个参数user参数单独也视为提供了参数权限容错设计如果用户对某个版本没有权限或版本不存在就静默地跳过该项目/版本不使整个搜索失败。设计文档明确解释了动机这样用户可以对所有用户共用一个搜索端点无需关心每个用户的权限差异也不必在项目或版本被删除后更新端点配置。在 SearchExecutor 中这一语义体现为生成器_get_projects_to_search()遍历parser.arguments[project]逐个解析(project, version)只有当版本存在且self._has_permission(...)通过时才yieldfor value in self.parser.arguments[project]: project, version self._get_project_and_version(value) if version and self._has_permission(self.request, version): yield project, version关于分隔符的选型设计文档记录了一个有趣的取舍/最终被采用如project:docs/latest但文档指出“它可以是任何不会出现在项目或版本 slug 中的字符”。:曾被考虑project:docs:latest但因为:已经用于分隔 key 与 value可读性差而被放弃。源码侧对应_split_project_and_version用term.split(/, maxsplit1)完成切分未提供版本时返回None作为 version。此外projects属性通过islice将显式指定的项目数量限制在max_projects100以内并用dict去重确保每个项目只用一个版本多版本搜索是未来的功能见文末。subprojects包含子项目含父项目设计文档对“如何包含子项目”给出了三个候选方案最终选择了 inclusive 版本候选方案行为结论include-subprojects:true语义不清晰不知道从哪些项目展开子项目只能对所有项目生效放弃subprojects:project/versioninclusive明确指定从哪个项目展开子项目并可匹配版本结果同时包含父项目选中subprojects:project/versionexclusive同上但不含父项目若要含父项目需写成project:project/latest subprojects:project/latest重复且难用放弃选择第二个方案的原因文档给出两点其一这正是当前“在带子项目的项目内搜索”的既有行为用户心智一致其二避免了用户在想包含父项目时必须重复写一遍project:参数。版本省略规则与project参数相同默认使用父项目的默认版本。实现上_get_subprojects遍历父项目的subprojects关系先尝试按给定version_slug匹配子项目版本匹配不到则回退到子项目自身的默认版本同样受权限过滤。源码还做了两处查询优化将父项目的relationship预置到子项目的_superprojects、将organization预置到_organizations以避免后续属性访问时的额外数据库查询。user限定用户可访问的项目用于包含“该用户有访问权限的项目”的结果目前唯一支持的取值是me指当前请求用户别名。实现位于SearchExecutor._get_projects_from_user()def _get_projects_from_user(self): for project in Project.objects.for_user(userself.request.user): version self._get_project_version( projectproject, version_slugproject.default_version, include_hiddenFalse, ) if version and self._has_permission(self.request, version): yield project, version即取当前用户可访问的所有项目对每个项目取其默认版本且排除隐藏版本再经权限校验后纳入搜索集合。四、执行链路从 q 参数到 Elasticsearch 查询完整的调用链为SearchAPI.get() # readthedocs/search/api/v3/views.py └─ SearchExecutor(request, query) # readthedocs/search/api/v3/executor.py ├─ SearchQueryParser.parse() # 解析出 arguments 与最终 query ├─ projects 属性 # 生成 [(project, version), ...] └─ search() → PageSearch(...) # readthedocs/search/faceted_search.py几个关键细节无参数即拒绝arguments_requiredTrue默认时如果查询中没有任何合法参数search()直接返回None视图返回空结果——这就是设计文档中“searchinvalid, at least one project is required”这一例子的落地查询构造策略PageSearch继承自 RTDFacetedSearch对文本查询使用SimpleQueryString支持 ES 简单查询语法并同时以and/or两种default_operator构建Bool(should...)查询因为满足and的文档理应获得更高分数项目过滤则用Bool(should[Bool(must[Term(project...), Term(version...)]), ...])精确限定“项目版本”组合见_get_projects_query高级/模糊查询切换should_use_advanced_query 通过第一个项目的DEFAULT_TO_FUZZY_SEARCHfeature flag 决定走SimpleQueryString还是模糊匹配路径。注意源码中留有 TODO文档“Future features”里设想的“指定搜索类型multi match / simple query string / fuzzy”参数尚未成为 API 参数目前是按项目特性隐式决定的排序加权PageSearch通过FunctionScore附加脚本评分把用户可设置的页面 rank[-10, 10]线性映射到 [0.01, 2] 的权重区间且刻意设计了边界值0.8 / 1.3使精确匹配的分节标题结果仍优先于整页标题结果详见_get_script_score的注释推导限流搜索 API 面向匿名用户和“搜索即输入”场景SearchAPI单独设置了 100 次/分钟的匿名与认证用户限流views.py 中的RATE_LIMIT 100/minute。五、响应结构项目/版本对象化与顶层元信息设计文档规定响应大体沿用旧格式但增加“最终搜索用到了哪些项目、版本、以及最终查询词”这类元信息同时version、project、project_alias三个字段从字符串变为对象。这一决策的论证是即使直接复用旧响应破坏性变更也只是“属性变成对象”而这些对象暂时不新增字段并且直接复用现有序列化器也没有问题。实际实现 PageSearchSerializer 正是如此继承 V2 序列化器project输出{slug, alias}对象、version输出{slug}对象并移除顶层project_alias字段并入project.aliasclass PageSearchSerializer(PageSearchSerializerBase): project serializers.SerializerMethodField() version serializers.SerializerMethodField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields.pop(project_alias)顶层的projects与query字段由_add_extra_fields注入。下面是设计文档给出的响应示例结构与当前实现一致blocks为页面内容片段highlights为span标签包裹的匹配高亮{ count: 1, next: null, previous: null, projects: [ { slug: docs, versions: [ { slug: latest } ] } ], query: The final query used in the search, results: [ { type: page, project: { slug: docs, alias: null }, version: { slug: latest }, title: Main Features, path: /en/latest/features.html, domain: https://docs.readthedocs.io, highlights: { title: [] }, blocks: [ { type: section, id: full-text-search, title: Full-Text Search, content: We provide search across all the projects that we host. ..., highlights: { title: [Full-spanText/span Search], content: [] } }, { type: domain, role: http:post, name: /api/v3/projects/, id: post--api-v3-projects-, content: Import a project under authenticated user. ..., highlights: { name: [], content: [] } } ] } ] }从源码结构看blocks中type: section的条目来自PageSearch的Nested嵌套查询pathsectionsinner_hits.size限制为 3而type: domain则来自文档解析阶段对 Sphinx 域对象如 HTTP 路由的结构化抽取——这与 PageDocument 的 sections 嵌套字段设计相呼应。六、缓存、CORS 与搜索分析缓存按“最终搜索涉及的所有项目”打 tag设计文档的考虑是一个请求可能关联多个项目因此缓存 tag 需要覆盖全部项目形式为project1, project1:version, project2, project2:version。实现见_add_cache_tags对每个(project, version)生成三类 tag——项目 slug、project:version版本 tag、以及rtd-search索引 tag文档重建时 purge 索引缓存用。源码补充了一个文档未提及的工程约束Cache-Tag 响应头受 Cloudflare16KB与 nginx4KB限制实测上限约 2.5K因此实现中把注入的 tag 总量限制在2000 字符超出后记录日志并停止添加更多 tag。CORS新版 API 暂不支持跨站请求由于一个请求可能关联多个项目中间件无法在单个请求级别轻易判断是否应开启 CORS。设计文档的结论是新 API 暂时不允许跨站请求。未来要支持需要重构 CORS 代码让每个视图自行决定例如“仅当最终搜索涉及的所有版本都是公开版本时允许跨站”或“始终允许跨站、但跨站请求只返回公开版本的结果”。这与SearchAPI面向.org公开搜索的定位一致。分析为每个参与搜索的项目记录同一查询设计文档规定“对最终搜索用到的每个项目都记录同一条查询”。实现位于_record_query通过tasks.record_search_query_batch.delay(...)异步批量记录参数为全部(project_slug, version_slug)元组、最终查询词去参数后的小写文本、总结果数与时间戳。源码中的 NOTE 也如实指出局限total_results是所有项目合计的结果数单个项目本身可能 0 条命中却仍记录总数。七、实战示例设计文档给出的四个代表性示例key:value语法 全文词混排及其含义查询含义project:docs project:dev/latest test在docs项目默认版本、dev项目 latest 版本中搜索testa project:docs/stable search term在docs项目 stable 版本中搜索a search termproject:docs project\:project/version在docs项目默认版本中搜索字面文本project:project/version转义示例search无效至少需要一个参数无project/subprojects/user实际请求形如GET /api/v3/search/?qproject:docs%20stable注意 URL 编码配合Authorization: Token token可访问私有版本——权限逻辑正是上文SearchExecutor._has_permission的入口.com 代理版本通过认证后端覆盖该方法。八、Dashboard 搜索的同步改造新版 API 的第三个目标是把同一套语法带到 readthedocs.org / readthedocs.com 的仪表盘搜索。仪表盘搜索分两类项目级搜索Project scoped只搜索当前项目的文件与版本——新语法在这里不生效本来就只有一个项目文档建议的替代方案是从项目级搜索链接跳转到全局搜索并在查询中预填project:{project.slug}全局搜索Global搜索 .org 上所有项目的文件/版本.com 上则只搜索用户有权限的项目此外全局搜索还支持按名称/描述搜索项目本身。针对全局搜索的规划项目搜索保持现状不套用新语法在那里没有意义文件搜索直接复用 API 语法.org 默认搜全部项目.com 默认搜用户有权限的项目文档还讨论了允许user:stsewd之类的按用户过滤初期可只支持me方便用户搜索自己名下所有项目Facets分面初期只支持projectsfacet。由于新语法要求“先指定项目才能搜版本”无法跨所有项目搜所有latest版本原有 facets 语义会有所变化默认展示projectfacet用户过滤到具体项目后再展示versionfacet若用户一次搜索多个项目文档坦承“事情会变得复杂”比如点击版本 facet 是否要改所有项目的版本并给出了务实的退路——如果太难解释/实现就先只支持projectfacet。从源码结构看当前PageSearch的 facets 恰好就是{project: TermsFacet(fieldproject)}faceted_search.py与“初期只支持 projects facet”的规划一致。向后兼容设计文档提出应尽力让全局搜索的旧 URL 继续可用备选方案是忽略旧语法或将旧语法转换后 301 到新语法并 redirect例如?qtestprojectdocsversionlatest转换为?qtest project:docs/latest。九、未来特性Roadmap设计文档末尾列出的后续规划可作为跟进该项目搜索能力演进的清单搜索同一项目的多个版本当前 API 响应结构已预留projects中每个项目带versions数组便捷搜索项目全部版本语法形如project:docs/*或project:docs/all允许显式指定搜索类型Multi match查询原样匹配Simple query string允许使用 ES 查询语法Fuzzy search带 fuzziness 的 multi match对照当前 should_use_advanced_query 中的 TODO 注释可以确认该项尚未暴露为 API 参数目前依赖项目 feature flag 隐式决定org过滤器按组织搜索其名下所有项目返回各项目默认版本的结果。十、小结Read the Docs 新版搜索 API 的核心设计思想可以概括为三句话查询即配置搜索范围完全由q参数中的key:valuetoken 表达不落库、多项目一次搜索project可重复、subprojects展开子项目、user:me纳入个人项目且无权限目标静默跳过、响应自描述顶层projects/query字段让客户端知道结果到底来自哪些项目和版本。语法解析queryparser.py、执行与权限过滤executor.py、视图层缓存 tag 与查询记录views.py、以及 Elasticsearch 查询构造faceted_search.py共同构成了一个边界清晰、可被 .org/.com 双域名复用的搜索层。对于希望理解“搜索语法如何变成索引过滤”这一完整链路的读者test_api.py 与 test_queryparser.py 提供了逐用例的行为验证入口。赞分享后端文档【免费下载链接】readthedocs.orgThe source code that powers readthedocs.org项目地址https://gitcode.com/gh_mirrors/re/readthedocs.org点击查看免费下载相关推荐Read the Docs 文档内搜索 UI 设计Search-as-you-type 的设计思路、后端选型与落地现状Read the Docs 文档内搜索 UI 设计Search as you type 的设计思路、后端选型与落地现状 本文基于 Read the Docs后端文档如何用Nix构建mermaid-asciiflake.nix可复现构建指南如何用Nix构建mermaid asciiflake.nix可复现构建指南 mermaid ascii 是一款能将 Mermaid 图表直接渲染为终端 ASCCLI开发工具Spacedrive 搜索界面SRCH-000设计实现指南从全局搜索栏到 FTS5 与语义搜索的接口落地Spacedrive 搜索界面SRCH 000设计实现指南从全局搜索栏到 FTS5 与语义搜索的接口落地 导读 本篇技术指南围绕 Spacedrive 仓桌面应用移动开发后端存储数据同步上一篇OrdinaryRoad Live Chat Client 开源项目教程下一篇【亲测免费】 探索未来设计的门户Chili3D——云端3D建模的新星创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表