我之前使用的 Fuwari 主题,默认是内置了 PageFind 来作为文章搜索工具的。 尽管 PageFind 并不是一个很差的解决方案,但是作为一个纯客户端搜索,还是带来了一些小问题的:
- 中文的分词做得相对比较差
- 依赖于一个索引文件,而索引文件会随着文章大小增大而增大。 在这种情况下,我决定使用一款带有服务端的搜素服务来解决问题。
选择搜索服务#
目前主流的搜索服务中,有牛逼但是非常重量级的 ElasticSearch,也有 Algolia 这这种付费 SaaS 选手。
而开源、省资源且可以自己建立的情况下,则只剩下了 TypeSense 和 MeiliSearch 这两位。
TypeSense相对而言对复杂搜索做的比较好,但是对于博客的搜素,TypeSense 更高的硬件要求则显得有些多此一举。
相对于 TypeSense,MeiliSearch尽管缺失了很多高级功能,比如 StartWith
这些,但是其有特殊的分词优化,对于中文博客搜索还是绰绰有余的。
因为,我选择使用 MeiliSearch 来进行搜索服务的实现。
创建 MeiliSearch 服务#
安装 MeiliSearch#
MeiliSearch 不支持多节点,自然也不需要像 ElasticSearch 那样复杂的安装,一个简单的 DockerCompose 足够拉起来整个项目。
这里唯一需要注意的是,设置一个比较复杂的管理员秘钥,越长越好那种,之后所有的操作都是围绕这个秘钥进行的,这个秘钥也需要保管好。
之后所有的请求,均需要在请求头中添加 Authorization
字段,内容则需要以Bareer <YOUR_KEY>
这样的形式书写。
创建索引#
创建 MeiliSearch 索引的唯一方法就是通过 API。按照官方的文档,使用下面的方法创建即可:
这里一共有两个字段需要设置,一个是uid
,这个字段就是索引的名字。另一个字段是primaryKey
,是文档的唯一标识符。
而与 ElasticSearch 等服务不同的是,MeiliSearch 的搜索是需要额外手动提供文档 ID 的。这一点在之后部署的时候需要注意。
创建搜索专用 Key#
由于 MasterKey 有着极高的权限。我们有必要为前台搜索提供专门的 Key 并限制具体访问的 API。 MeiliSearch 可以通过一下的 API 来创建 Key:
这里同样有两个字段需要注意:
actions
:这个字段主要限制 key 的权限范围,具体可用参数可以 参考这里。indexes
:这个参数则主要限制可以使用的索引名称,里面是有索引 UID 组成的数组。这里我们仅仅设置我们之前设置的索引 ID 即可。
按照官方提供的例子,下面的字段key
就是我们创建好的 key
录入文档#
由于 Astro 是 SSG 框架,所以索引的录入只能放在编译阶段。 目前其实有两种思路来录入文档,一种是直接读取 Markdown 文件,另外一种是读取编译好的页面。
根据我的实验,建议在页面编译完成后再录入文档,其中主要原因在于:
- Astro 的页面地址在编译前可能并不是确定的。
- 部分文档类型如 MDX 可能会引入很多没用的东西。
按照逻辑,我们应该可以使用下面的代码来读取并生成页面的元数据信息:
上边是本站正在使用的部分,简单讲解下就是以下的思路:
- 读取生成目录下的所有文章的 HTML。
- 读取页面中的元数据部分。(部分主题,如这里,可能已经将元数据做成了 json 格式)
- 去除文章中多余的 HTML 代码部分。
- 生成 ID,整理并返回页面的元数据。
由于截止目前,MeiliSearch 还不支持嵌套值的搜索,所以建议将页面的可搜索字段拉平。
在获取到文章后,则可以将所有的文章提交到 MeiliSearch 服务了。
前端实现搜索功能#
与 ElasticSearch 不同,MeiliSearch 支持将API直接暴露在前端,那么,自然就可以直接与 MeiliSearch 的搜索接口通讯。
可以使用下面的代码与自己的 MeiliSearch 对接:
如果你想在客户端(浏览器)获取到编译时的部分环境变量,可以在变量前加入 PUBLIC_
这个前缀来解决。
至于与主题的集成,则由于每个人的主题不同,有着不同的集成方法。
如本站,则是简单的替换了原主题使用的 PageFind方法实现的。
再次就不一而足了。
关于安全性#
集成 MeiliSearch 的操作其实本身并不复杂。但是其周边生态其实有不少要考虑的。
比如如果使用第三方 CI 或 Serverless 部署,可能需要专门为其提供秘钥和访问方法来限制普通用户的访问。
对于这种情况,可以使用 Cloudflare Access 的 Service Auth 来避免公网直接访问非搜索接口的问题。
而对于搜索接口本身,同样建议反向代理指定 API 并增加速率限制等方式防止被刷。