介绍
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。
- 获取 Admin Key 和 Search Key;
1
| curl -X GET 'http://localhost:7700/keys' -H 'Authorization: Bearer {MASTER_KEY}'
|
- 获取文档索引信息;
1
| curl -X GET 'http://localhost:7700/indexes' -H 'Authorization: Bearer {ADMIN_KEY}
|
- 添加或替换文档索引;
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 2
| 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 对我的自定义组件进行封装。
- 声明搜索实例
1 2 3 4 5 6
| import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
export const searchClient = instantMeiliSearch( '{YOUR_SERVER}', '{YOUR_SEARCH_KEY}' );
|
- 封装自定义的搜索框
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;
|
- 封装自定义的 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;
|
- 挂载自定义组件
我们需要将所有自定义组件都使用 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 或者文档需要定期更新,自动化起来还是很有必要的,主要思路为:
- 写一个脚本,当文档或者 Blog 有更新时,重新生成一份文档索引文件;
- 请求 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); }
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