自定义 Blog 搜索 - 集成 Meilisearch

介绍

MeiliSearch 是一个基于 Rust 写的搜索引擎,并且它也是开源的,既然是用 Rust 写的,那它的稳定性我一定就不用多说了。Meilisearch 包含了你能想到的所有搜索的功能,选择它的一个原因是因为我只想用它的 API 和搜索的能力,但是我想完全自定义一整套 UI 和交互等。说白了,就是我想完全自定义,灵活搭配,但是我不想写一个 Search API。

这里得提一嘴,当然了如果只是个人使用的话,白嫖 Algolia 的免费版也是可以的,但是免费版每个月会有 10,000 次的请求限制,超过之后就要收费了。再有就是 Algolia 是闭源的,纯国内使用有可能会存在部分服务不稳定的问题。总之呢,解决方案有多种,选择一种你喜欢的就可以。

下面,记录一下我是怎么集成 MeiliSearch 服务做一个自定义搜索框功能的。因为官方文档还是比较全的,本文会便总结性的来记录一些注意事项、和自定义时踩的一些坑。

前端集成 MeiliSearch

部署 MeiliSearch

根据官网的部署指南,你可以非常轻松的将 MeiliSearch 部署到本地。官方文档也提供了各种部署的方式、Docker、K8s、AWS 等等应有尽有。

部署的时候需要注意:如果我们是部署生产模式,需要我们设置一个 Master Key,用来获取 Search key 和 Admin key。

1
2
3
4
5
6
# Mac

# Update brew and install Meilisearch
brew update && brew install meilisearch

meilisearch --master-key={MASTER_KEY} --env production
  • Search key:只能用来查询,可以直接暴露到配置文件中;
  • Admin key:可以用来做任何事情,但是我们在生产使用时,不应该用它来做查询的操作,不应该暴露在任何一个地方。
  • env:env 参数不传递时默认为 development,可以在对应端口看到 MeiliSearch 的页面,当设置为 production 时就是一个纯后端服务了。

注意 production 模式启动时必须设置 MasterK ey

准备文档索引

我们需要为 MeiliSearch 服务上传我们用来搜索的文档索引,官方提供了一个示例,你可以直接下载它来进行测试。

它的格式主要为:

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"id": 0,
"title": "...",
...
},
{
"id": 1,
"title": "...",
...
}
...
]

上传文档索引到 Meilisearch

这里介绍一些用到的 API,可以阅读官方文档查看所有的 API。

  1. 获取 Admin Key 和 Search Key;
1
curl -X GET 'http://localhost:7700/keys' -H 'Authorization: Bearer {MASTER_KEY}'
  1. 获取文档索引信息;
1
curl -X GET 'http://localhost:7700/indexes' -H 'Authorization: Bearer {ADMIN_KEY}
  1. 添加或替换文档索引;
1
2
3
4
5
curl \
-X POST 'http://localhost:7700/indexes/{INDEX_NAME}/documents?primaryKey=id' \
-H 'Authorization: Bearer {ADMIN_KeY}' \
-H 'Content-Type: application/json' \
--data-binary {YOUR_DOC_DATA}

参数 primaryKey 用来标识索引中的每个文档,确保不可能在同一索引中出现两个完全相同的文档。

  1. 删除文档索引;
1
2
curl -X DELETE 'http://localhost:7700/indexes/{INDEX_NAME}/documents' \
-H 'Authorization: Bearer {ADMIN_KEY}'

前端集成

官方提供了 js、React、Vue 的例子,使用了 react-instantsearch-dominstant-meilisearch 两个库。

由于我在落地到实际项目中时发现 react-instantsearch-dom 这个库不支持 TS 项目,它没有声明对应的类型,于是我选择了另外一种解决方案,改用 react-instantsearch-hooks-web 直接使用 Hooks 对我的自定义组件进行封装。

  1. 声明搜索实例
1
2
3
4
5
6
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';

export const searchClient = instantMeiliSearch(
'{YOUR_SERVER}',
'{YOUR_SEARCH_KEY}'
);
  1. 封装自定义的搜索框
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { useSearchBox } from 'react-instantsearch-hooks-web';

const CustomSearchBox: React.FC = () => {
const { refine } = useSearchBox();

return (
<form noValidate action="" role="search">
<Input
autoFocus
onChange={(event) => refine(event.currentTarget.value)}
type="search"
placeholder="Search the articles"
/>
</form>
);
};

export default CustomSearchBox;
  1. 封装自定义的 Hits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { useHits, Highlight } from 'react-instantsearch-hooks-web';

const CustomHits: React.FC = () => {
const { hits } = useHits();
return (
<div>
{hits.map((item) => (
<div key={item.__queryID}>
<Highlight attribute="title" hit={item} />
</div>
))}
</div>
);
};

export default CustomHits;
  1. 挂载自定义组件

我们需要将所有自定义组件都使用 InstantSearch 来包裹。下面例子中的 indexName 为上传文档索引时的名字,searchClient 为我们刚刚封装好的搜索实例。

1
2
3
4
5
6
7
import { InstantSearch } from 'react-instantsearch-hooks-web';

...
<InstantSearch indexName="{INDEX_NAME}" searchClient={searchClient}>
<CustomSearchBox />
<CustomHits />
</InstantSearch>

自动化更新 MeiliSearch 文档索引

如果你的 Blog 或者文档需要定期更新,自动化起来还是很有必要的,主要思路为:

  1. 写一个脚本,当文档或者 Blog 有更新时,重新生成一份文档索引文件;
  2. 请求 MeiliSearch 的 API 来更新数据源即可;

注意因为 MeiliSearch 的 Admin key 不能暴露,所以我们需要设置环境变量来储存。

下面以 GitHub Action 举例说明:

Node 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import axiox, { AxiosError, AxiosResponse } from 'axios';

const ENDPOINT = process.env.NEXT_PUBLIC_SEARCH_ENDPOINT;
const ADMIN_KEY = process.env.NEXT_PUBLIC_SEARCH_ADMIN_KEY;

axiox.defaults.timeout = 5000;

(async () => {
if (!ENDPOINT || !ADMIN_KEY) {
console.log('Missing env variables');
return process.exit(1);
}

// 生成文档索引数据 posts
const posts = []

writeFile('posts.json', JSON.stringify(posts, null, 2)).then(() => {
console.log('posts length: ', posts.length);
// 更新文档索引
axiox
.post(`${ENDPOINT}/indexes/{YOUR_INDEX_NAME}/documents?primaryKey=id`, posts, {
headers: {
Authorization: `Bearer ${ADMIN_KEY}`,
'Content-Type': 'application/json',
},
})
.then((res: AxiosResponse) => {
const { status } = res;
if (status === 202) {
console.log('update success');
}
})
.catch((err: AxiosError) => {
console.log('update failed, the error info: ', err);
});
});
})();

GitHub Action:

1
2
3
4
5
6
7
8
...
- name: Update Meilisearch Data
env:
SEARCH_ENDPOINT: ${{ secrets.SEARCH_ENDPOINT }}
SEARCH_ADMIN_KEY: ${{ secrets.SEARCH_ADMIN_KEY }}
run: |
NEXT_PUBLIC_SEARCH_ENDPOINT=$SEARCH_ENDPOINT NEXT_PUBLIC_SEARCH_ADMIN_KEY=$SEARCH_ADMIN_KEY node auto-update.js
...

总结

平常的前端开发中,搜索功能还是比较常见的。如果纯自己手写会比较麻烦一些,Meilisearch 提供了一个解决方案,我们只需关心 UI 样式,无需关心搜索服务本身的请求调用,体验非常不错。甚至做一个全局搜索也不是问题了~

参考资料

Master key and API keys
Front-end integration
react-instantsearch-hooks-web