全文搜索
Docsbook 搜索是一个针对您页面的 Postgres 全文索引,以页眉中的搜索按钮和侧边栏中的搜索框形式提供。每次查询都不产生任何费用——不会调用模型——而那些未返回任何结果的查询,是您的文档产生的两种最有价值的信号之一。
你将获得#
- 页眉、侧边栏或两者中的搜索框。 两者可以同时运行;通常一个就够了。
- 带有高亮摘要的结果 — 摘要是从匹配项周围的页面文本中提取的上下文窗口,而不是页面开头的 200 个字符。
- 指向确切标题的深层链接。 如果匹配项位于某个章节内,链接会携带该章节的实际锚点,因此读者会直接跳转到段落,而不是长页面的顶部。
- 优先显示已翻译的页面。 如果读者使用的是已翻译的语言区域,则已翻译的条目优先显示;未翻译的页面仍会出现,因此即使网站只完成了一部分翻译,也能进行完整搜索。
- 记录每次无结果的搜索,作为一个
search.no_resultswebhook 和一份失败搜索报告。
索引是如何构建的?#
在渲染时写入,而不是推送时写入。 Docsbook 渲染页面后,页面便会进入索引——在响应发送之后,因此索引过程不会延迟页面响应。翻译页面在其翻译内容被缓存时,也会以相同方式建立索引。无需手动重建,也没有重新建立索引按钮。
需要了解这一点:自发布以来从未被打开过的页面尚未进入索引。 因此,一个全新的网站在其页面至少被访问过一次之前,搜索结果会很少。在完全没有任何记录之前,搜索框会退回到从页面列表中匹配文件名,因此不会完全没有结果。
一条记录包含以下内容:
| 字段 | 内容 |
|---|---|
| 标题 | Frontmatter title,否则使用页面的 H1,再否则使用文件名 |
| 正文 | 页面的纯文本:会移除 frontmatter、标题标记、图像、链接语法、围栏代码块和行内 Markdown 标点 |
| 章节 | 每个 h2/h3 对应一条记录,其中包含标题渲染后的锚点及其文本,并移除 <pre> 块 |
| 语言 | 原始页面为空,每个翻译页面使用相应的区域代码 |
搜索针对生成的 tsvector 执行,其中标题的权重为 A,正文的权重为 B——PostgreSQL 的权重标签用于实现“[让文档不同部分的]词语在排名函数中具有不同权重”(PostgreSQL:其他文本搜索功能)。该列使用 GIN 建立索引。
查询如何得到回答?#
- 停用词会被剥除。
websearch_to_tsquery“将未加引号的文本词项与&(AND)运算符组合”(PostgreSQL:控制文本搜索),因此完整句子会要求页面也包含每个填充词。系统会先移除一个包含 45 个英语停用词的列表,但只针对引号外的内容——所以"exact phrase"、OR和-word仍然有效。 - 英语查询会进行词干提取。 对于英语和未翻译的内容,查询与文档会在
english配置下进行匹配,该配置使用 Snowball 词干提取器,“将单词的常见变体形式还原为基础形式或词干拼写”(PostgreSQL:词典)。没有这一处理时,包含“served”的页面无法匹配包含“serve”的查询。 - 其他语言使用已存储的索引。 非英语区域设置会匹配已存储的
simple向量,该向量“通过将输入词元转换为小写来运行”,且不会进行词干提取。将英语词干提取规则应用于非拉丁文字会产生无意义的结果,因此会有意跳过这一处理。 - 英语路径会针对长度对排名进行标准化。 对于英语查询,
ts_rank使用标准化标志 1 运行,该标志“将排名除以 1 加上文档长度的对数”。没有这一处理时,一份 76 KB 的变更日志可能只是顺带提及每个查询词,却会排在真正讨论该问题的短页面之前。非英语路径调用不带标准化标志的ts_rank,因此在这些区域设置下,长页面不会因长度而受到惩罚——请参阅“限制”。 - 每个页面只返回一行。 原文和译文会合并为单个结果,优先使用读者的语言,然后按排名排序。标题命中会排在仅正文命中之前。
- 摘要片段由服务器生成。
ts_headline会返回匹配项周围包含 5–18 个词的一个片段;客户端会重新高亮这些词。
输入拼写错误会发生什么?#
没有匹配项。Docsbook 搜索不支持模糊匹配、三元语法相似度或编辑距离回退。词干提取可以涵盖词形变化——serve 可以找到 served——但无法处理拼写错误:documnetation 找不到任何结果。
这是有意为之的取舍,而不是疏忽,同时也配套提供了补偿机制:每个零结果查询都会报告给你。报告会进行合并,因此一次搜索只产生一个信号,而不是每次击键都产生一个信号:
- 浏览器会在读者停止输入 1.5 秒 后再报告未找到结果;如果读者在搜索过程中关闭对话框,则会立即发送报告——在实际工作区中的测量显示,读者输入一个单词时,字符之间会暂停 0.9–1.3 秒;在此机制存在之前,这会为一个单词产生八条报告。
- 服务器会独立抑制来自同一读者、在短时间窗口内且严格扩展了上一个查询前缀的未命中结果,因为端点是公开的,不能假设任何客户端都进行了防抖处理。
因此,documnetation 出现在你的失败搜索报告中并不是搜索的错误——这是搜索在告诉你,某位读者找不到该页面,而这正是你需要采取行动的信息。反复出现的拼写错误最好在内容中修复,使用读者实际输入的术语来命名。
搜索框放置位置#
| 放置位置 | 最适合 | 权衡 |
|---|---|---|
| 页眉按钮 | 首次访问的访客,他们会先查看顶部栏 | 会与页眉链接争夺空间 |
| 侧边栏框 | 已经通过树状结构导航的读者 | 侧边栏折叠时,在窄屏上会被隐藏 |
在 Float Widget → 设计 → 页眉 → 搜索按钮中启用页眉按钮。在 Float Widget → 设计 → 左侧边栏 → 在侧边栏中搜索中启用侧边栏框。两者在所有套餐中均免费。
为什么这是正确的方法(证据)#
| 规则 | 有效原因 | 来源 |
|---|---|---|
| 即使已有语义搜索,也要保留词法索引 | 在超过 18 个检索数据集上,"BM25 is a robust baseline" 在零样本设置中表现稳健,而稠密检索器 "often underperform"——你的语料库对于任何嵌入模型来说都属于域外数据,而精确术语正是技术读者会输入的内容 | Thakur 等,2021 年——BEIR |
| 在查询到达 Postgres 之前去除停用词 | websearch_to_tsquery 会对未加引号的词使用 AND,因此只要页面中缺少一个填充词,整个匹配就会失败 |
PostgreSQL:控制文本搜索 |
| 对英语进行词干提取,不要对其他语言进行词干提取 | simple 配置只会进行小写转换;Snowball 词干提取器按语言提供,并会将不同变体归约为词干 |
PostgreSQL:字典 |
| 按文档长度规范化排名(英语路径) | 标志 1 会“将排名除以 1 加文档长度的对数”,因此,一篇仅偶然提及该主题的长文不会超过一篇专门讨论该主题的短页面 | PostgreSQL:控制文本搜索 |
| 提高标题相对于正文的权重 | 权重标签可以让排名以不同方式处理文档不同部分中的词语 | PostgreSQL:其他文本搜索功能 |
同一个索引也是 AI 聊天 背后的两个检索器之一——在那里它也不是较低级的备用方案。
限制#
- 不容忍拼写错误。 见上文。如果拼写错误会影响你的受众,请将该术语添加到页面中。
- 代码块无法搜索。 索引前会移除围栏代码,
<pre>代码块会从章节文本中移除。搜索仅出现在代码示例中的函数名的读者将无法找到它。需要注意的是:旧版文档声称代码块会被编入索引并“排名低于正文”;而索引器会直接移除代码块。 - 覆盖范围取决于流量,而不是你的代码仓库。 页面首次渲染时会进入索引。已发布但从未打开过的页面,在有人打开之前不会出现在搜索结果中。
- 我们不发布延迟数据。 英文查询会在查询时计算其
tsvector,而不是读取已存储的simple索引,这意味着随着语料库增长,英文路径每次查询需要执行更多工作。我们尚未发布基准测试,在具备基准数据之前也不会引用任何数据。 - 长度归一化仅适用于英语。 英文查询路径会将
ts_rank归一化标志传递为 1;其他语言环境使用的路径调用ts_rank时完全不传递标志,这意味着不会进行长度归一化。因此,在非英语网站上,一个非常长的页面可能会超过一个与查询更精准相关的短页面。两个路径统一之前,这一点仍有待确认。 - 搜索按项目进行。 索引限定在单个工作区内;不支持跨项目搜索。
- 失败搜索信号经过合并,并非精确。 输入链中的第一次未命中会被发送到实时 webhook,因此发送的查询可能只是读者最终确定查询的较短前缀。历史报告会还原最终确定的查询。
相关#
- AI 聊天 — 使用此索引作为其检索器之一的助手。
- 回答质量 — 词法检索和向量检索的合并方式。
- 分析:读者搜索了什么 — 未返回任何结果的查询。
- 页面反馈 — 用于表明页面缺失或命名不当的另一种信号。