😓领导无语:根据关键词搜索文章你居然用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