MDN 的自动完成搜索是如何工作的

2021-08-05 20:40:05

上个月,我和 Gregor Weber 向 MDN Web Docs 添加了一个自动完成搜索,它允许您通过键入部分文档标题快速直接跳转到您要查找的文档。这是关于如何实施的故事。如果你坚持到底,我将分享一个“复活节彩蛋”功能,一旦你学会了它,会让你在晚宴上看起来很酷。或者,也许您只是想比普通人更快地浏览 MDN。在最简单的形式中,输入字段有一个 onkeypress 事件侦听器,它过滤每个文档标题(每个区域设置)的完整列表。在撰写本文时,英语美国有 11,690 个不同的文档标题(及其 URL)。您可以通过打开 https://developer.mozilla.org/en-US/search-index.json 来查看预览。是的,它很大,但是将所有内容加载到内存中并不太大。毕竟,连同执行搜索的代码,它只会在用户表示想要输入内容时加载。而说到大小,因为文件是用Brotli压缩的,所以文件在网络上只有144KB。默认情况下,加载的唯一 JavaScript 代码是一个小 shim,用于监视搜索 <input> 字段的 onmouseover 和 onfocus。整个文档上还有一个事件侦听器,用于查找某个击键。在任何时候按下 / ,就像您使用鼠标光标将焦点放在 <input> 字段中一样。一旦焦点被触发,它做的第一件事就是下载两个 JavaScript 包,将 <input> 字段变成更高级的东西。在最简单的(伪)形式中,它是如何工作的: let started = false;function startAutocomplete() { if (started) { return false; } const script = document.createElement("script"); script.src = "https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/static/js/autocomplete.js"; document.head.appendChild(script);} 然后它加载 /static/js/autocomplete.js 这就是真正的魔法发生的地方。让我们深入挖掘伪代码: (async function() { const response = await fetch('/en-US/search-index.json'); const documents = await response.json(); const inputValue = document.querySelector ( 'input[type="search"]' ).value; const flex = FlexSearch.create(); documents.forEach(({ title }, i) => { flex.add(i, title); }); const indexResults = flex.search(inputValue); const foundDocuments = indexResults.map((index) => documents[index]); displayFoundDocuments(foundDocuments.slice(0, 10));})();正如您可能看到的,这是对其实际工作方式的过度简化,但现在还不是深入研究细节的时候。下一步是显示匹配项。我们使用 (TypeScript) React 来做到这一点,但下面的伪代码更容易理解:

function displayFoundResults(documents) { const container = document.createElement("ul");文档.forEach(({url, title}) => { const row = document.createElement("li"); const link = document.createElement("a"); link.href = url; link.textContent = title; row.appendChild(link); container.appendChild(row); }); document.querySelector('#search').appendChild(container);} 然后使用一些 CSS,我们将其显示为 <input> 字段下方的叠加层。例如,我们根据 inputValue 突出显示每个标题,并且当您上下导航时,各种击键事件处理程序负责突出显示相关行。我们只创建一次 FlexSearch 索引,并在每次新击键时重新使用它。因为用户在等待网络时可能会输入更多内容,所以它实际上是反应性的,因此在所有 JavaScript 和 JSON XHR 到达后执行实际搜索。在我们深入研究这个 FlexSearch 是什么之前,让我们先谈谈显示器的实际工作原理。为此,我们使用名为 downshift 的 React 库来处理所有交互、显示并确保显示的搜索结果可访问。 downshift 是一个成熟的库,它通过构建这样的小部件来应对无数挑战,尤其是使其可访问的方面。那么,这个 FlexSearch 库是什么?它是另一个第三方,确保搜索标题时考虑到自然语言。它将自己描述为“Web 上最快、内存最灵活、零依赖的全文搜索库”。这比尝试简单地在其他字符串的长列表中查找一个字符串要高效和准确得多。公平地说,如果用户键入 foreac,将 10,000 多个文档标题的列表减少到只有标题中包含 foreac 的列表并不难,然后我们决定首先显示哪个结果。我们实施的方式依赖于浏览量统计数据。我们为每个 MDN URL 记录哪个页面浏览量最多,作为确定“受欢迎程度”的一种形式。大多数人决定访问的文档很可能就是用户正在搜索的内容。我们生成 search-index.json 文件的构建过程知道每个 URL 的浏览量。我们实际上并不关心绝对数字,但我们关心的是相对差异。例如,我们知道 Array.prototype.forEach()(这是文档标题之一)是比 TypedArray.prototype.forEach() 更受欢迎的页面,因此我们利用它并相应地对 search-index.json 中的条目进行排序.现在,通过 FlexSearch 进行缩减,我们使用数组的“自然顺序”作为尝试向用户提供他们可能正在寻找的文档的技巧。这实际上与我们在完整站点搜索中用于 Elasticsearch 的技术相同。有关更多信息,请参见:MDN 的站点搜索是如何工作的。

实际上,这不是一个异想天开的复活节彩蛋,而是一种功能,因为此自动完成功能需要为我们的内容创建者工作。你看,当你处理 MDN 中的内容时,你启动了一个本地“预览服务器”,它是所有文档的完整副本,但都在本地运行,作为一个静态站点,位于 http://localhost:5000 下。在那里,您不想依赖服务器进行搜索。内容作者需要在文档之间快速移动,所以自动完成搜索完全在客户端完成的大部分原因就是因为这个。通常在 VSCode 和 Atom IDE 等工具中实现,您只需键入部分文件路径即可进行“模糊搜索”以查找和打开文件。例如,搜索 whmlemvo 应该找到文件 files/ web/ ht ml/ element/ vide o。您也可以使用 MDN 的自动完成搜索来做到这一点。您这样做的方法是键入 / 作为第一个输入字符。如果您知道它的 URL 但不想准确地拼出它,它可以非常快速地直接跳转到一个文档。事实上,还有另一种导航方式,那就是在浏览 MDN 时首先按 / 任意位置,这会激活自动完成搜索。然后你再次输入 / ,你就可以开始比赛了!所有这些的代码都在 Yari 存储库中,它是构建和预览所有 MDN 内容的项目。要找到确切的代码,请单击 client/src/search.tsx 源代码,您将找到用于延迟加载、搜索、预加载和显示自动完成搜索的所有代码。