diff --git a/SEARCH_OPTIMIZATION_PLAN.md b/SEARCH_OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..5cfd71f --- /dev/null +++ b/SEARCH_OPTIMIZATION_PLAN.md @@ -0,0 +1,328 @@ +# GoClub 搜索功能调研与优化方案 + +更新日期:2026-08-03 + +## 1. 文档目标 + +本文记录 GoClub 当前站内搜索的索引范围、性能实测、主要问题和后续优化路线,为后续搜索实现改造提供决策和验收依据。 + +本次只整理方案,不直接改动搜索实现。后续实施时建议按“先修现有交互,再验证 Pagefind”的顺序拆分提交,避免一次性替换搜索引擎带来较大的回归范围。 + +当前检索链路如下: + +```mermaid +flowchart LR + A[用户首次聚焦搜索框] --> B[下载完整搜索 JSON] + B --> C[浏览器解析 JSON] + C --> D[创建 Fuse.js 索引] + D --> E[每次 keyup 执行全文检索] + E --> F[完整排序后截取前 10 条] + F --> G[只展示标题和父级栏目] +``` + +## 2. 当前搜索实现 + +GoClub 当前使用 Hugo 在构建阶段生成一个完整的 JSON 索引,浏览器首次聚焦搜索框时下载索引,再使用 Fuse.js 7.1.0 在前端执行全文模糊检索。 + +相关文件: + +- `themes/hugo-book/assets/search-data.json`:生成搜索数据。 +- `themes/hugo-book/assets/search.js`:下载索引、创建 Fuse 实例并渲染结果。 +- `themes/hugo-book/layouts/_partials/docs/search.html`:搜索框和结果容器。 +- `i18n/zh-cn.yaml`:中文搜索配置。 +- `themes/hugo-book/static/fuse.min.js`:Fuse.js 7.1.0 浏览器包。 + +当前索引规则: + +1. 收录 Hugo 中类型为 `page` 或 `section` 的页面。 +2. 排除 `bookSearchExclude: true` 的页面。 +3. 排除正文为空的页面。 +4. 每条记录只保存链接、标题、直接父级栏目和页面纯文本正文。 +5. 搜索只对标题和正文加权,标题权重为 0.7,正文权重为 0.3。 +6. 完整计算命中结果后,只展示前 10 条。 + +## 3. 当前可检索范围 + +目前能够检索: + +- 文章标题; +- Markdown 渲染后的正文纯文本; +- 标题和正文中的中英文技术术语; +- 能够进入 Hugo `.Plain` 内容的代码文本。 + +目前不能或不能可靠检索: + +- 标签、关键词和分类等独立元数据; +- 完整面包屑路径; +- 标题层级和文章内标题锚点; +- 作者、摘要等独立加权字段; +- 命中关键词附近的正文片段; +- 图片中的文字; +- `static/files/epub-books/` 下的 2 个 EPUB 文件正文; +- 设置了 `bookSearchExclude: true` 的 3 个交互学习页面; +- 外部链接指向的网站正文。 + +仓库中现有 403 个 Markdown 文件,但目前没有文章设置 `tags` 字段,只有 1 篇文章设置了 `description`。因此扩展元数据搜索时,还需要同步建立内容元数据规范。 + +## 4. 性能实测 + +测试环境为当前开发电脑和当前网络,数据用于定位本项目瓶颈,不代表所有访问者的固定耗时。 + +### 4.1 索引和网络 + +| 指标 | 实测结果 | +| --- | ---: | +| 索引记录 | 402 条 | +| 本地原始 JSON | 3,751,141 字节 | +| 线上 gzip 响应 | 1,506,529 字节 | +| 线上 Brotli 响应 | 1,395,997 字节 | +| 首字节时间 | 约 1.0~2.0 秒 | +| gzip 完整下载 | 13.1~15.1 秒 | +| Brotli 单次下载 | 17.6 秒,受当时网络波动影响 | +| 当前下载速度 | 约 80~115 KB/s | + +线上索引响应当前使用 `Cache-Control: max-age=600`,Cloudflare 返回 `cf-cache-status: DYNAMIC`。索引 URL 已包含内容哈希,因此可以使用更长时间的不可变缓存。 + +### 4.2 浏览器端计算 + +| 环节或查询词 | 中位耗时 | 命中数 | +| --- | ---: | ---: | +| JSON 解析 | 2.0 ms | - | +| Fuse 索引创建 | 2.5 ms | - | +| `MySQL` | 102.3 ms | 56 | +| `MVCC` | 57.4 ms | 8 | +| `Kubernetes` | 129.9 ms | 117 | +| `分布式锁` | 57.9 ms | 12 | +| `云原生` | 57.3 ms | 50 | + +结论:当前冷启动的首要瓶颈是下载整个索引。索引加载完成后,桌面端单次查询约为 57~130 ms;低性能移动设备预计会更慢。 + +## 5. 已确认的问题 + +### 5.1 首次搜索等待时间长 + +搜索索引直到输入框第一次获得焦点才开始下载。在当前网络环境下,用户点击输入框后可能等待十几秒才能看到结果。 + +### 5.2 输入过程中重复执行全文搜索 + +当前监听 `keyup`,没有防抖。用户每输入一个字符都会扫描全文索引,容易造成主线程卡顿和重复计算。 + +### 5.3 索引加载期间存在调用风险 + +索引下载完成前,`window.bookSearchIndex` 尚未创建。如果用户已经开始输入,搜索函数可能调用未初始化的对象并产生控制台错误。 + +### 5.4 只显示 10 条,但仍计算全部结果 + +当前先调用 `search()` 计算全部命中,再使用 `.slice(0, 10)` 截断。结果很多时仍会承担完整搜索和排序开销。 + +### 5.5 中文分词配置未生效 + +`i18n/zh-cn.yaml` 配置了 `encode` 和 `tokenize`,但项目内置的 Fuse.js 7.1.0 不包含这两个配置的实现,也不包含新版的 `useTokenSearch`。当前中文检索主要依赖字符级模糊匹配,不是真正的中文分词检索。 + +### 5.6 结果信息不足 + +结果只展示文章标题和直接父级栏目,没有以下信息: + +- 命中总数; +- 命中关键词高亮; +- 正文上下文摘要; +- 加载、空结果和加载失败状态; +- 分类筛选和继续加载; +- 跳转到文章内命中位置的锚点。 + +## 6. 搜索范围扩展建议 + +### 6.1 推荐的索引记录结构 + +在继续使用 Fuse.js 的阶段,建议先统一搜索记录的数据结构: + +```json +{ + "href": "/docs/example/", + "title": "文章标题", + "description": "人工摘要", + "content": "正文纯文本", + "headings": ["一级标题", "二级标题"], + "keywords": ["Go", "MySQL"], + "category": "技术博客", + "breadcrumb": ["技术博客", "数据库"], + "codeSymbols": ["database/sql", "QueryContext"] +} +``` + +建议权重由高到低依次为:标题、关键词、标题层级、摘要、代码符号、正文。栏目和面包屑主要用于筛选与结果说明,不宜拥有过高的相关性权重。 + +### 6.2 优先增加结构化字段 + +建议为搜索记录增加以下字段,并设置独立权重: + +- `description`:人工摘要; +- `keywords` 或 `tags`:技术关键词; +- `headings`:文章标题层级; +- `breadcrumb`:完整栏目路径; +- `category`:顶级栏目; +- `codeSymbols`:函数名、类型名和重要命令。 + +同时建立 Front Matter 规范,要求新增文章至少包含标题、摘要、栏目和关键词。否则只有索引字段,没有稳定的数据来源。 + +### 6.3 交互页面 + +3 个交互学习页面不建议直接收录整段 HTML 和脚本。更稳妥的做法是为每个页面生成一条精简搜索记录,包括页面名称、功能简介、技术关键词和入口链接。 + +### 6.4 EPUB 和图片 + +- EPUB 可以在构建阶段提取章节文字,每章生成独立搜索记录,但需要先确认内容授权和期望的搜索粒度。 +- 图片不建议默认执行全量 OCR。优先索引图片标题、说明文字和所在文章上下文,只对确有检索价值的图片增加 OCR。 + +### 6.5 外部资源 + +不建议由浏览器临时抓取外部页面。若需要搜索外部资源正文,应在构建阶段使用白名单抓取并保存摘要,同时处理版权、更新频率、超时和失效链接问题。 + +## 7. 分阶段实施方案 + +### 7.1 方案对比 + +| 方案 | 首次下载 | 中文检索 | 摘要与高亮 | 运维成本 | 扩展能力 | 建议 | +| --- | --- | --- | --- | --- | --- | --- | +| 继续使用当前 Fuse.js | 完整下载约 1.4 MB | 字符级模糊匹配 | 需要自行实现 | 低 | 较低 | 不建议维持原状 | +| 优化并升级 Fuse.js | 仍需完整下载 | 可增加中文分词 | 需要自行实现 | 低 | 中等 | 适合短期修复 | +| Pagefind | 按查询加载分块 | extended 版本支持中文分词 | 原生支持 | 低 | 高 | 推荐中期方案 | +| Meilisearch/Typesense 等服务 | 按 API 返回结果 | 取决于配置 | 通常支持 | 中到高 | 很高 | 当前阶段不需要 | + +### 7.2 阶段一:低风险优化现有 Fuse.js + +目标是在不更换搜索引擎的情况下改善等待反馈、重复计算和结果可读性。 + +建议改动: + +1. 将监听事件改为 `input`,增加 200~300 ms 防抖。 +2. 索引未初始化时不执行搜索,并展示“正在加载搜索索引”。 +3. 增加加载失败和重试逻辑。 +4. 使用 Fuse 的结果数量限制参数,避免在渲染阶段才截断。 +5. 展示命中总数、正文摘要、关键词高亮和空结果状态。 +6. 增加顶级栏目筛选和“加载更多”。 +7. 为索引文件配置 `public, max-age=31536000, immutable`。 +8. 在非省流量模式下使用空闲预加载,减少用户首次聚焦后的等待。 +9. 若继续使用 Fuse.js,评估升级到支持 token search 的版本,并使用 `Intl.Segmenter('zh-CN')`;升级后必须回归中文排序和模糊匹配结果。 + +阶段一可以改善交互,但无法根治首次访问必须下载约 1.4 MB 索引的问题。 + +### 7.3 阶段二:使用 Pagefind 进行静态分块索引验证 + +Pagefind 更符合 GoClub 的 Hugo 静态站点形态: + +- 在 Hugo 构建完成后扫描生成的 HTML; +- 保持纯静态部署,不增加搜索服务器; +- 按查询按需下载分块索引; +- 支持中文分词; +- 原生提供摘要、高亮、分页、元数据和筛选能力; +- 可以通过 `data-pagefind-*` 属性精确控制正文、导航和重复区域是否进入索引。 + +建议先建立验证分支,不立即删除 Fuse.js。验证内容包括: + +1. Hugo 生产构建后运行 Pagefind extended。 +2. 只标记主文章区域为可索引区域,排除菜单、页脚和重复导航。 +3. 保留当前页面视觉风格,使用 Pagefind API 自定义结果界面。 +4. 对中英文、缩写、代码符号和错别字建立固定测试词表。 +5. 比较索引总体积、首次查询下载量、查询耗时和结果相关性。 +6. 验证通过后再移除旧 Fuse.js 索引与脚本。 + +### 7.4 阶段三:仅在需求增长后考虑服务端搜索 + +只有出现以下需求时,才建议评估 Meilisearch、Typesense、Algolia 或其他托管搜索服务: + +- 内容需要实时更新而不能等待静态构建; +- 需要跨多个独立站点统一检索; +- 需要复杂权限控制; +- 需要搜索分析、个性化排序或语义检索; +- 内容规模显著超过纯浏览器静态搜索的合理范围。 + +当前 GoClub 没有必要为了搜索新增长期运行的后端服务。 + +## 8. 推荐决策 + +推荐采用“两步走”: + +1. 先完成 Fuse.js 的加载状态、防抖、结果展示和缓存优化,快速改善当前体验。 +2. 同时制作 Pagefind 验证版本,用同一组查询词对比后决定是否替换。 + +不建议直接向当前单体 JSON 持续加入 EPUB 全文、OCR 文本和外部网页正文。这样虽然扩大了范围,但会线性增加下载体积和浏览器端全文扫描成本。 + +### 8.1 实施优先级 + +| 优先级 | 工作项 | 目的 | +| --- | --- | --- | +| P0 | 索引未初始化保护、加载状态、失败重试 | 消除报错和不可解释的等待 | +| P0 | 输入防抖 | 避免每个按键触发全文扫描 | +| P1 | 强缓存策略 | 改善重复访问和跨页面访问 | +| P1 | 命中数量、摘要、高亮、空结果 | 提升结果可理解性 | +| P1 | 固定搜索回归词表 | 防止调整排序后出现静默退化 | +| P2 | description、keywords、headings 等元数据 | 扩大可检索范围并改善排序 | +| P2 | Pagefind 验证分支 | 验证分块索引收益和中文相关性 | +| P3 | EPUB、OCR、外部资源索引 | 在确有需求和授权后再扩展 | + +## 9. 验收标准 + +### 阶段一验收 + +- 索引加载期间输入不会报错。 +- 连续输入只在停止输入后触发实际查询。 +- 搜索结果有加载、失败、空结果和命中数量反馈。 +- 每条结果至少包含标题、栏目、命中摘要和高亮。 +- 桌面端和移动端均不会因结果列表造成布局溢出。 +- 原有中英文固定测试词均能返回合理结果。 + +### Pagefind 验证验收 + +- 首次查询网络传输量明显低于当前约 1.4 MB。 +- 中文、英文缩写和代码术语的结果相关性不低于现有实现。 +- 支持文章内标题或段落级定位。 +- GitHub Pages 部署不需要新增常驻服务。 +- Hugo 生产构建和部署流程可重复执行。 + +## 10. 回归测试词表 + +建议将以下查询作为固定回归用例,并为每个查询保存期望出现的前几条结果: + +| 类型 | 示例 | 主要验证点 | +| --- | --- | --- | +| 中文术语 | `分布式锁`、`云原生` | 中文分词和正文命中 | +| 英文术语 | `MySQL`、`Kubernetes`、`MVCC` | 大小写、缩写和排序 | +| 中英混合 | `Go GC`、`MySQL 索引` | 多词查询和跨语言分词 | +| 代码符号 | `QueryContext`、`database/sql` | 标点与代码字段 | +| 标题精确匹配 | 完整文章标题 | 标题权重应明显高于正文 | +| 错别字或拼写误差 | `Kubernets` | 模糊匹配能力 | +| 无结果 | `不存在的测试词xyz` | 空状态和无错误反馈 | + +回归测试至少覆盖桌面端、移动端、首次冷加载、缓存命中、慢速网络和加载失败六种场景。 + +## 11. 上线与回滚策略 + +### 11.1 Fuse.js 优化 + +每个交互改动应保持小提交。上线前记录索引体积、固定查询耗时和前 10 条结果;如果相关性明显下降,可单独回滚权重或分词配置,不影响内容页面。 + +### 11.2 Pagefind 切换 + +Pagefind 验证期间保留旧 Fuse.js 文件和开关。部署配置增加可切换参数,在新索引构建失败或线上结果异常时恢复旧搜索入口。确认连续多次生产构建成功后,再删除旧索引生成逻辑。 + +### 11.3 监测指标 + +建议至少记录: + +- 首次搜索索引下载字节数; +- 首次结果出现时间; +- 缓存命中后的查询耗时; +- 无结果比例; +- 搜索结果点击率; +- 搜索脚本异常数量。 + +在没有隐私声明和用户同意前,不记录完整的个人查询内容;如需分析搜索词,应先进行匿名化和聚合处理。 + +## 12. 参考资料 + +- Pagefind:https://pagefind.app/ +- Pagefind Hugo 构建接入:https://pagefind.app/docs/running-pagefind/ +- Pagefind 中文与多语言支持:https://pagefind.app/docs/multilingual/ +- Pagefind 索引范围控制:https://pagefind.app/docs/indexing/ +- Fuse.js 性能说明:https://www.fusejs.io/performance.html diff --git a/assets/_custom.scss b/assets/_custom.scss index 2cfcaea..a160aa2 100644 --- a/assets/_custom.scss +++ b/assets/_custom.scss @@ -1928,10 +1928,12 @@ body.sidebar-collapsed .book-brand-mark { .section-card-summary { margin-top: 0.75rem; + min-width: 0; color: var(--body-font-color); opacity: 0.74; line-height: 1.72; flex-grow: 1; + overflow-wrap: anywhere; } .section-card-meta { diff --git a/layouts/baseof.html b/layouts/baseof.html index 1f6db4d..4ef7598 100644 --- a/layouts/baseof.html +++ b/layouts/baseof.html @@ -41,10 +41,10 @@ {{ end }}