介绍
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。
# 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 服务上传我们用来搜索的文档索引,官方提供了一个示例,你可以直接下载它来进行测试。
它的格式主要为:
[
{
"id": 0,
"title": "...",
...
},
{
"id": 1,
"title": "...",
...
}
...
]
上传文档索引到 Meilisearch
这里介绍一些用到的 API,可以阅读官方文档查看所有的 API。
- 获取 Admin Key 和 Search Key;
curl -X GET 'http://localhost:7700/keys' -H 'Authorization: Bearer {MASTER_KEY}'
- 获取文档索引信息;
curl -X GET 'http://localhost:7700/indexes' -H 'Authorization: Bearer {ADMIN_KEY}
- 添加或替换文档索引;
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 用来标识索引中的每个文档,确保不可能在同一索引中出现两个完全相同的文档。
- 删除文档索引;
curl -X DELETE 'http://localhost:7700/indexes/{INDEX_NAME}/documents' \
-H 'Authorization: Bearer {ADMIN_KEY}'
前端集成
官方提供了 js、React、Vue 的例子,使用了 react-instantsearch-dom
和 instant-meilisearch
两个库。
由于我在落地到实际项目中时发现 react-instantsearch-dom
这个库不支持 TS 项目,它没有声明对应的类型,于是我选择了另外一种解决方案,改用 react-instantsearch-hooks-web
直接使用 Hooks 对我的自定义组件进行封装。
-
声明搜索实例
其中 SEARCH_KEY 可以直接使用 Meilisearch 中自动生成的 search key。可以安全的公开,如果不放心的话,也可以放到环境变量中去。
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
export const searchClient = instantMeiliSearch(
'{YOUR_SERVER}',
'{YOUR_SEARCH_KEY}'
);
-
封装自定义的搜索框
下面的
<Input />
是 chakra 的组件。需要注意的是我们需要把refine
方法传递到onChange
方法中去。
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;
- 封装自定义的 Hits
Hits
组件就是我们搜索框显示的结果组件,可以自己修改该组件样式。
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;
- 挂载自定义组件
我们需要将所有自定义组件都使用 InstantSearch
来包裹。下面例子中的 indexName
为上传文档索引时的名字,searchClient
为我们刚刚封装好的搜索实例。
import { InstantSearch } from 'react-instantsearch-hooks-web';
...
<InstantSearch indexName="{INDEX_NAME}" searchClient={searchClient}>
<CustomSearchBox />
<CustomHits />
</InstantSearch>
自动化更新 Meilisearch 文档索引
如果你的 Blog 或者文档需要定期更新,自动化起来还是很有必要的,主要思路为:
- 写一个脚本,当文档或者 Blog 有更新时,重新生成一份文档索引文件;
- 请求 MeiliSearch 的 API 来更新数据源即可;
注意因为 Meilisearch 的 Admin key 不能暴露,所以我们需要设置环境变量来储存。
下面以 GitHub Action 举例说明:
Node 脚本
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:
...
- 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