😓领导无语:根据关键词搜索文章你居然用like去实现?
前言
可乐他们团队最近在做一个文章社区平台,由于人手不够,前后端都是由前端同学来写。后端使用 nest 来实现。
某天,可乐接到了一个根据关键词搜索文章的需求,用户输入关键词,去模糊匹配文章表的以下字段:
title:标题content:内容introduction:简介
可乐心想:这还不简单,一个 like 模糊查询的事情。
往期文章
初步模糊搜索实现
十分钟不到,可乐就完成了后端代码的编写:
async searchArticle(params: SearchArticleDto) {
const { keyword, pageNo, pageSize } = params;
const paginationService = new PaginationService<ArticleEntity>(
this.articleRepository,
);
const res = await paginationService.paginate({
page: pageNo,
pageSize,
options: {
where: [
{ content: Like(`%${keyword}%`) },
{ title: Like(`%${keyword}%`) },
{ introduction: Like(`%${keyword}%`) },
],
select: ['id', 'categoryId', 'introduction', 'title', 'creatorName'],
},
});
return res;
}
假设我们输入的参数是:
{
"keyword": "nest",
"pageNo": 1,
"pageSize": 10
}
则实际执行的sql语句是:
SELECT id,title,categoryId,creatorName,introduction from articles WHERE content LIKE '%nest%' OR title LIKE '%nest%' OR introduction LIKE '%nest%' LIMIT 10 OFFSET 0
然后,他就跟领导说自己做完了,内心OS:上次点赞你说我做了五天做的慢,这次我十分钟做完,你没话说了吧。
领导一看可乐写的代码,无语说道:
首先,我承认使用 like 是最简便的方式,也能完成这个需求。但是,这样实现的弊端十分明显:
- 你这样的
like是没办法走到索引的,我们也不会给那几个字段加索引,到时候数据量越来越大,这样的实现方式效率会很低下 - 这样的实现方式是大小写敏感的,对于搜索来说,如果用户想搜索
react,假设但是库里存的东西都是React,那就搜不到了。 - 这样也没有分词逻辑,比如输入是:
我喜欢使用MeiliSearch进行全文搜索,那么一些搜索引擎可能就会把它分成“我”、“喜欢”、“使用”、“MeiliSearch”、“进行”、“全文搜索”等词语去进行搜索。
看样子你是没使用过搜索引擎是吧,市面上有很多优秀的搜索引擎,例如 Elasticsearch(ES) ,它可以作为一个很强大的搜索引擎。但是对于我们这种小团队来说,它不太适合。
首先,它需要占用大量的内存、 CPU 资源;其次,他的上手难度也比较高;再者,对于一些分词功能,需要我们额外进行调试。
你去了解一下 MeiliSearch 吧,我们这个搜索功能就用它来实现。它非常快速且占用资源较少,部署也很简单,配置也很容易上手。
聊完之后,可乐开始去搜寻一些 MeiliSearch 的资料,并着手准备部署与使用它。
MeiliSearch安装
我们可以在 Docker 上安装 MeiliSearch,按照以下步骤进行操作即可:
-
拉取 MeiliSearch 镜像:首先,使用以下命令从
Docker Hub拉取MeiliSearch的官方镜像。docker pull getmeili/meilisearch -
创建并运行 MeiliSearch 容器:然后,使用以下命令在
Docker中创建并运行MeiliSearch容器。docker run -d --rm \ -p 7700:7700 \ -e MEILI_MASTER_KEY=my_custom_master_key \ getmeili/meilisearch在这个命令中:
docker run: 这个命令用于在Docker 中运行一个容器。-d: 这个选项告诉Docker在后台运行容器。--rm: 这个选项告诉Docker在容器停止运行后自动删除容器。-p 7700:7700: 这个选项将容器的端口映射到主机上。MeiliSearch服务器运行在容器内的端口7700上,主机可以通过localhost:7700来访问MeiliSearch。-e MEILI_MASTER_KEY=my_custom_master_key: 这个选项设置了MeiliSearch的master key,master key相当于MeiliSearch的密码
- 访问 MeiliSearch 控制台:完成上述步骤后,通过访问
http://localhost:7700来访问MeiliSearch的Web控制台。

MeiliSearch初体验
下面来介绍一下 MeiliSearch 中的一些核心概念:
- 索引(Index): 索引是
MeiliSearch中用于组织和存储数据的逻辑单元。每个索引都是独立的数据集合,它包含了一组文档,这些文档可以被搜索、过滤、排序等操作。在MeiliSearch中,索引是搜索的基本单位。 - 文档(Document): 文档是
MeiliSearch中存储的实际数据对象。每个文档都是一个包含了一定结构的数据记录,它可以是JSON格式的数据,包含了多个字段(Field),每个字段都有一个字段名和对应的值。例如,在一个名为"books"的索引中,每个文档可能代表一本书,其中包含了书名、作者、出版日期等字段信息。
也就是我们需要建立一个 articles 索引,然后往 articles 索引中加入以下格式的文档数据:
{
id:1,
title:'title',
content:'content',
introduction:'introduction'
}

上述请求使用 post 访问 indexes 路由,创建一个名为 articles 的索引。
建好索引之后,我们不希望 id 字段可以被搜索到,可以使用如下方式来修改可以被搜索的字段:

访问 /indexes/articles/settings ,参数是:
{
"searchableAttributes": [
"title",
"content",
"introduction"
]
}
表示 title 、 content 、 introduction 这三个字段才可以被搜索。
然后我们把一条文章推送到 articles 索引中来试一下。

使用 post 访问 /indexes/:index/documents 路由,把一条测试数据推送到 MeiliSearch 中:
[
{
"id": "1",
"title": "测试标题",
"content": "测试内容",
"introduction": "测试描述"
}
]
可以在 MeiliSearch的webui 中看到我们已经推送的数据

同样可以通过请求访问

解释一下参数:
q:查询的内容attributesToRetrieve:需要返回的字段limit:每页条数offset:偏移量
数据推送
大概了解了 MeiliSearch 的用法之后,可以写一个测试的接口或者脚本,把数据库的数据同步到 MeiliSearch 中。
首先,我们先在项目里接入 MeiliSearch ,首先安装一下这个库,npm install meilisearch
然后封装一个 service 如下:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MeiliSearch, SearchParams } from 'meilisearch';
@Injectable()
export class MeiliSearchService {
private readonly client: MeiliSearch;
constructor() {
const configService = new ConfigService();
const host = configService.get<string>(
'MEILI_HOST',
'http://localhost:7700',
);
const apiKey = configService.get<string>('MEILI_MASTER_KEY', 'master_key');
this.client = new MeiliSearch({
host,
apiKey,
});
}
async search(indexName: string, query: string, options: SearchParams) {
return await this.client.index(indexName).search(query, options);
}
async addDocument(indexName: string, documents: Record<string, any>[]) {
return await this.client.index(indexName).addDocuments(documents);
}
}
这里主要根据配置实例化一个 MeiliSearch ,然后简单封装了一个查询和插入文档的方法。
然后我们写一个接口,把数据库的数据都同步到 MeiliSearch 中。
async pushAllArticles() {
const list = await this.articleRepository.find({
where: {
status: 1,
isDeleted: 0,
},
});
await this.meiliSearchService.addDocument(ARTICLE_INDEX, list);
}
这里把已发布 (status为1) 且未删除 (isDeleted为0) 的文章查出来,然后推到 MeiliSearch 中,然而调用的时候报错了。

看这个错误是他说我们少了一个请求头,这个请求头的值应该是 master key 或者其他 key ,我把 MeiliSearch 实例打出来看了一下,发现它把 master key 填充到了 Authorization 字段中。

可能是我 Docker 安装的版本与这个 js 的 sdk 版本的 api 不是特别匹配,但是它也提供了一些方式注入请求头。实例化的时候可以按照下面的操作:
this.client = new MeiliSearch({
host,
apiKey,
requestConfig: {
headers: {
'X-MEILI-API-KEY': apiKey,
'Content-Type': 'application/json',
},
},
});
然后就可以推送成功了

全量数据推送完毕后,我们还需要在文章发布的时候,把文章数据推送到 MeiliSearch 中。
async pushAllArticles() {
const list = await this.articleRepository.find({
where: {
status: 1,
},
});
await this.meiliSearchService.addDocument(ARTICLE_INDEX, list);
}
实现起来大同小异,这里就不再赘述。
搜索接口实现
在数据推送完毕后,我们就可以重写搜索功能了:
async searchArticle(params: SearchArticleDto) {
const { keyword, pageNo, pageSize } = params;
const res: any = await this.meiliSearchService.search(
ARTICLE_INDEX,
keyword,
{
attributesToRetrieve: ['id', 'title', 'introduction'],
limit: pageSize,
offset: (pageNo - 1) * pageSize,
},
);
const hits = res.hits;
const nbHits = res.nbHits;
return {
list: hits,
total: nbHits,
pageSize: pageSize,
currentPage: pageNo,
totalPage: Math.ceil(nbHits / pageSize),
isEnd: Math.ceil(nbHits / pageSize) === pageNo,
};
}
解释一下上面的代码:
attributesToRetrieve:MeiliSearch需要返回的字段hits:当前搜索的条数nbHits:总条数- 根据前端传过来的分页信息以及关键词,调用
MeiliSearch去做搜索,搜索完成后,拼接成分页接口的样子返回给前端

关键词高亮
调用 search 的时候可以加上一个 attributesToHighlight 字段,它可以帮我们在结果中标红关键词,比如说我想要标红标题和简介,那么我可以设置
attributesToHighlight: ['title', 'introduction']
结果中可以看到,相应的区域已经被 em 标签包裹,这个使用前端就可以使用它去做不同的样式展示。

最后
以上就是本文的全部内容,介绍了 Nest+MeiliSearch 实现全文检索。如果你觉得有意思的话,点点关注点点赞吧~
转载自:https://juejin.cn/post/7366826090836262951