资讯动态

VitePress 本地搜索自定义分词器(tokenize)实战:让 hash-probe 与连字符关键词保持完整匹配

发布时间:2026/9/21 15:30:46 来源:尧图企业网站定制
VitePress 本地搜索自定义分词器tokenize实战让 #hash-probe 与连字符关键词保持完整匹配【免费下载链接】vitepressVite Vue powered static site generator.项目地址: https://gitcode.com/gh_mirrors/vi/vitepress本地搜索Local Search是 VitePress 默认主题内置的纯前端全文检索方案其索引构建与查询解析都委托给 MiniSearch 完成。本文以仓库内 e2e 本地搜索测试夹具 为切入点讲解如何通过自定义tokenize函数控制分词粒度使#hash-probe这类带井号的标签、以及hyphen-linked-words这类连字符复合词在索引与查询两端保持为一个完整 token从而避免默认分词器把关键词拆碎后导致的漏匹配问题。读完本文你将掌握本地搜索的启用方式、tokenize 的正则设计原理、配套的内容排除机制以及索引构建与客户端查询的完整调用链。一、测试夹具两行文档背后的完整测试场景位于tests/e2e/local-search/index.md 的页面正文只有两行# Local search included The custom tokenizer keeps #hash-probe and hyphen-linked-words whole.它并非一篇说明书而是 e2e 测试套件的夹具页面fixture标题Local search included与正文中的#hash-probe、hyphen-linked-words都是被测试代码精确断言的关键词。围绕它同一目录下还有三个配合页面excluded.md标题为Local search config excluded仅用于验证通过配置排除的页面不会出现在结果中frontmatter-excluded.md通过 frontmatter 中的search: false从索引中排除frontmatter-title.md使用$frontmatter.title表达式验证搜索结果标题能正确解析 frontmatter 插值。而真正考这些夹具的是 local-search.test.ts它用 Playwright 驱动真实浏览器验证了 7 项行为索引加载进度、内容排除、frontmatter 标题解析、查询持久化、自定义 tokenize 到达客户端、与导航栏一致的桌面断点、macOS Ctrl 快捷键导航。本文以其中的 tokenize 主题为主线展开。二、启用本地搜索测试环境的真实配置本地搜索在 VitePress 中是开箱即用的可选功能只需在.vitepress/config.ts中将themeConfig.search.provider设为localimport { defineConfig } from vitepress export default defineConfig({ themeConfig: { search: { provider: local } } })仓库 e2e 测试环境的真实配置位于tests/e2e/.vitepress/config.ts它在上述基础之上加装了自定义 tokenize 与自定义渲染钩子search: { provider: local, options: { miniSearch: { options: { tokenize: (text) text.split(/[\n\r\p{Z}\p{Terminal_Punctuation}]/u) } }, async _render(src, env, md) { const html await md.renderAsync(src, env) if (env.frontmatter?.search false) return if (env.relativePath.startsWith(local-search/excluded)) return return html } } }其中miniSearch.options会直接透传给 MiniSearch 实例官方文档 default-theme-search.md 中也有完整示例_render则是索引阶段自定义 Markdown 渲染的钩子。两处配置共同决定了哪些内容进索引、以什么粒度分词。三、核心原理自定义 tokenize 让特殊字符保持完整3.1 为什么默认分词器不够用MiniSearch 的默认分词器会按词边界把输入拆成若干 token。以#hash-probe为例默认分词会把查询词降解为hash和probe两个碎片而以自定义 tokenizer 构建的索引里#hash-probe是一个整体 token两端不一致结果就是查询命中失败。测试 local-search.test.ts 的注释明确记录了这一点#hash-probesurvives as one token only under the custom tokenizer — MiniSearchs default one would degrade the query tohash/probeand miss the index built with the custom tokenizer3.2 逐字拆解自定义分词正则测试配置中的分词器是一个单行正则切分tokenize: (text) text.split(/[\n\r\p{Z}\p{Terminal_Punctuation}]/u)它把文本按换行符 Unicode 空白分隔符 Unicode 终端标点切分\n、\r显式匹配换行\p{Z}Unicode 属性空白分隔符覆盖空格、制表符、全角空格等所有空白类字符\p{Terminal_Punctuation}Unicode 属性终端标点如句号、问号、感叹号、逗号、分号等用于终止句子的标点标志u启用 Unicode 模式使\p{...}属性转义生效。关键在于#井号与-连字符都不属于上述切分集合因此#hash-probe整体保留为一个 tokenhyphen-linked-words同样保持完整。这正是本主题的核心机制——分词器的职责从尽量拆碎变为只按句法级分隔符切分保留用户关心的复合词形态。3.3 测试如何验证 tokenize 行为测试用例custom tokenize function reaches the clientlocal-search.test.ts做了两段对称断言// #hash-probe 在自定义 tokenizer 下整体存活 → 恰好命中 1 条 const input await searchFor(#hash-probe) await waitForSearchResults({ text: Local search included, count: 1 }) // 完整 token 的任意片段linked-words 只是 hyphen-linked-words 的一部分 // 反过来必须一个都匹配不到 await input.fill(linked-words) await page.waitForSelector(.no-results)第一段证明整体 token 可被整体查询命中第二段证明token 的局部片段不能跨 token 匹配。这两个方向共同刻画了自定义分词器在查询端的完整行为边界也说明 tokenize 不仅作用于索引构建还作用于查询词预处理。四、tokenize 如何贯穿索引构建与客户端查询两端4.1 服务端索引构建插件本地搜索的索引在构建/开发阶段由 localSearchPlugin.ts 生成流程为render()L59-L81读取.md原文若提供了options._render则调用之否则走默认实现——渲染 HTML 后检查env.frontmatter?.search false来决定是否返回空串splitPageIntoSections()L282-L304用标题正则把渲染后的 HTML 按h1h6切分为多个 section每个 section 记录自己的锚点、标题层级链titles和去标签后的正文textgetIndexByLocale()L85-L96按 locale 维护独立的MiniSearch实例实例化时通过...options.miniSearch?.options展开传入自定义 tokenizegetDocId()L127-L136把文件路径转换为站点相对 ID如/guide/page.html#section这一 ID 会出现在原始索引与searchOptions.filter/boostDocument中。开发模式下插件还会通过hotUpdate监听.md变更并增量重建对应页面索引L263-L275配合server.moduleGraph.onFileChange与 HMR 推送保证搜索索引实时更新。4.2 客户端搜索框组件加载与查询索引 JSON 由 VPLocalSearchBox.vue 以MiniSearch.loadJSON方式加载L78-L100同样会合并theme.value.search.options?.miniSearch?.optionsMiniSearch.loadJSONResult(json, { fields: [title, titles, text], storeFields: [title, titles], searchOptions: { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2, titles: 1 }, ...(theme.value.search?.provider local theme.value.search.options?.miniSearch?.searchOptions) }, ...(theme.value.search?.provider local theme.value.search.options?.miniSearch?.options) })从源码结构可以推断出两点关键约束两端配置必须一致构建侧用miniSearch.options建索引客户端用同一份配置解析查询任何一侧漏配自定义 tokenize 都会造成索引是整体的、查询是碎片化的这类错位匹配默认搜索行为有明确定义未覆盖时默认启用fuzzy: 0.2模糊匹配、prefix: true前缀匹配与boost: { title: 4, text: 2, titles: 1 }标题加权这些默认值同样记载于官方文档 default-theme-search.md。4.3 查询持久化与输入替换组件通过useSessionStorage(vitepress:local-search-filter, )把查询词存进 sessionStorageL102-L111关闭再打开弹窗会恢复并预选中上次的查询。测试用例typing replaces the persisted querylocal-search.test.ts专门验证此时直接键入新词必须整体替换预选内容而非追加否则会拼出Frontmatter Title Resolvedlorem之类的脏查询。这正是持久化 预选设计带来的一个容易被忽略的交互细节。五、配套的内容控制frontmatter 排除与自定义 _render要让excluded.md、frontmatter-excluded.md不出现在结果中测试环境同时演示了两种官方支持的排除手段方式一frontmatter 开关。在页面 frontmatter 写入--- search: false ---方式二自定义_render。官方文档 default-theme-search.md 的Excluding pages from search小节指出一旦提供自定义_render框架就不再替你处理search: false必须自己判断。测试配置正是这样实现的async _render(src, env, md) { const html await md.renderAsync(src, env) if (env.frontmatter?.search false) return if (env.relativePath.startsWith(local-search/excluded)) return return html }官方文档还特别提醒env对象在md.renderAsync完成前并未完全填充因此对frontmatter等可选属性的判断必须放在await md.renderAsync(src, env)之后。测试用例exclude content from search resultslocal-search.test.ts搜索local时精确断言结果列表只有 1 条Local search included而Local search config excluded与Local search frontmatter excluded均为 0 条从端到端验证了两条排除路径同时生效。六、索引加载进度与可访问性细节测试套件还覆盖了索引加载的体验与无障碍语义。用例shows progress while loading search indexlocal-search.test.ts通过 Playwright 路由拦截/localSearchIndexroot请求并人为延迟 800ms随后断言加载指示器.search-loading进入active状态该元素rolestatus、aria-labelLoading search results结果列表.results在加载期间aria-busytrue加载完成后恢复false。该用例仅以runIf(!process.env.VITE_TEST_BUILD)条件在 dev 模式下运行原因是生产构建下索引随构建产物一起就绪来不及稳定观测加载态。这从侧面说明本地搜索的索引在 dev 下由插件异步扫描构建并随 HMR 更新在 build 下则是构建期产物两种模式的数据到达时序不同。七、交互一致性断点与快捷键最后两个测试用例验证本地搜索与默认主题其余部分的交互一致性断点一致uses the same desktop breakpoint as the nav barL116-L136在视口 767px 时断言汉堡菜单可见、.search-actions.before可见移动端布局切到 768px 后二者均隐藏桌面端布局证明搜索框的布局断点与导航栏同为 768px快捷键导航navigate results with macOS Ctrl shortcutsL138-L157在模拟 macOS 环境下用Ctrln/Ctrlp上下移动高亮项并断言aria-activedescendant在localsearch-item-0与localsearch-item-1之间正确切换验证了键盘可达性。八、小结从 index.md 中一句custom tokenizer keeps #hash-probe and hyphen-linked-words whole可以梳理出 VitePress 本地搜索的一套完整实践启用只需search.provider: local进阶能力全部集中在options内自定义 tokenize通过 Unicode 属性正则\p{Z}\p{Terminal_Punctuation}控制切分集合从而保留井号标签与连字符复合词的完整性且必须保证 构建插件 与 客户端组件 两端使用同一配置内容排除有 frontmattersearch: false与自定义_render两条路径前者在默认渲染中生效后者需自行判断索引构建按标题切分 section、按 locale 隔离实例、dev 下随 HMR 增量更新客户端则以fuzzy: 0.2、prefix: true、标题加权等默认行为执行查询并辅以查询持久化、加载进度语义与键盘导航等细节。如果你的文档站包含大量#tag、foo-bar这类对默认分词器不友好的关键词不妨参考本测试环境的配置为本地搜索定制一份属于自己的 tokenize 函数。【免费下载链接】vitepressVite Vue powered static site generator.项目地址: https://gitcode.com/gh_mirrors/vi/vitepress创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价