likes
comments
collection
share

😓领导无语:根据关键词搜索文章你居然用like去实现?

作者站长头像
站长
· 阅读数 44

前言

可乐他们团队最近在做一个文章社区平台,由于人手不够,前后端都是由前端同学来写。后端使用 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 是最简便的方式,也能完成这个需求。但是,这样实现的弊端十分明显:

  1. 你这样的 like 是没办法走到索引的,我们也不会给那几个字段加索引,到时候数据量越来越大,这样的实现方式效率会很低下
  2. 这样的实现方式是大小写敏感的,对于搜索来说,如果用户想搜索 react ,假设但是库里存的东西都是 React ,那就搜不到了。
  3. 这样也没有分词逻辑,比如输入是:我喜欢使用MeiliSearch进行全文搜索,那么一些搜索引擎可能就会把它分成“我”、“喜欢”、“使用”、“MeiliSearch”、“进行”、“全文搜索”等词语去进行搜索。

看样子你是没使用过搜索引擎是吧,市面上有很多优秀的搜索引擎,例如 Elasticsearch(ES) ,它可以作为一个很强大的搜索引擎。但是对于我们这种小团队来说,它不太适合。

首先,它需要占用大量的内存、 CPU 资源;其次,他的上手难度也比较高;再者,对于一些分词功能,需要我们额外进行调试。

你去了解一下 MeiliSearch 吧,我们这个搜索功能就用它来实现。它非常快速且占用资源较少,部署也很简单,配置也很容易上手。

聊完之后,可乐开始去搜寻一些 MeiliSearch 的资料,并着手准备部署与使用它。

MeiliSearch安装

我们可以在 Docker 上安装 MeiliSearch,按照以下步骤进行操作即可:

  1. 拉取 MeiliSearch 镜像:首先,使用以下命令从 Docker Hub 拉取 MeiliSearch 的官方镜像。

    docker pull getmeili/meilisearch
    
  2. 创建并运行 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: 这个选项设置了 MeiliSearchmaster keymaster key 相当于 MeiliSearch 的密码
  1. 访问 MeiliSearch 控制台:完成上述步骤后,通过访问 http://localhost:7700来访问 MeiliSearchWeb 控制台。

😓领导无语:根据关键词搜索文章你居然用like去实现?

MeiliSearch初体验

下面来介绍一下 MeiliSearch 中的一些核心概念:

  1. 索引(Index): 索引是 MeiliSearch 中用于组织和存储数据的逻辑单元。每个索引都是独立的数据集合,它包含了一组文档,这些文档可以被搜索、过滤、排序等操作。在 MeiliSearch 中,索引是搜索的基本单位。
  2. 文档(Document): 文档是 MeiliSearch 中存储的实际数据对象。每个文档都是一个包含了一定结构的数据记录,它可以是 JSON 格式的数据,包含了多个字段(Field),每个字段都有一个字段名和对应的值。例如,在一个名为 "books" 的索引中,每个文档可能代表一本书,其中包含了书名、作者、出版日期等字段信息。

也就是我们需要建立一个 articles 索引,然后往 articles 索引中加入以下格式的文档数据:

{
    id:1,
    title:'title',
    content:'content',
    introduction:'introduction'
}

😓领导无语:根据关键词搜索文章你居然用like去实现?

上述请求使用 post 访问 indexes 路由,创建一个名为 articles 的索引。

建好索引之后,我们不希望 id 字段可以被搜索到,可以使用如下方式来修改可以被搜索的字段:

😓领导无语:根据关键词搜索文章你居然用like去实现?

访问 /indexes/articles/settings ,参数是:

{
    "searchableAttributes": [
        "title",
        "content",
        "introduction"
    ]
}

表示 titlecontentintroduction 这三个字段才可以被搜索。

然后我们把一条文章推送到 articles 索引中来试一下。

😓领导无语:根据关键词搜索文章你居然用like去实现?

使用 post 访问 /indexes/:index/documents 路由,把一条测试数据推送到 MeiliSearch 中:

[
    {
        "id": "1",
        "title": "测试标题",
        "content": "测试内容",
        "introduction": "测试描述"
    }
]

可以在 MeiliSearch的webui 中看到我们已经推送的数据

😓领导无语:根据关键词搜索文章你居然用like去实现?

同样可以通过请求访问

😓领导无语:根据关键词搜索文章你居然用like去实现?

解释一下参数:

  • 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 中,然而调用的时候报错了。

😓领导无语:根据关键词搜索文章你居然用like去实现?

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

😓领导无语:根据关键词搜索文章你居然用like去实现?

可能是我 Docker 安装的版本与这个 jssdk 版本的 api 不是特别匹配,但是它也提供了一些方式注入请求头。实例化的时候可以按照下面的操作:

this.client = new MeiliSearch({
  host,
  apiKey,
  requestConfig: {
    headers: {
      'X-MEILI-API-KEY': apiKey,
      'Content-Type': 'application/json',
    },
  },
});

然后就可以推送成功了

😓领导无语:根据关键词搜索文章你居然用like去实现?

全量数据推送完毕后,我们还需要在文章发布的时候,把文章数据推送到 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,
    };
  }

解释一下上面的代码:

  • attributesToRetrieveMeiliSearch 需要返回的字段
  • hits :当前搜索的条数
  • nbHits:总条数
  • 根据前端传过来的分页信息以及关键词,调用 MeiliSearch 去做搜索,搜索完成后,拼接成分页接口的样子返回给前端

😓领导无语:根据关键词搜索文章你居然用like去实现?

关键词高亮

调用 search 的时候可以加上一个 attributesToHighlight 字段,它可以帮我们在结果中标红关键词,比如说我想要标红标题和简介,那么我可以设置

attributesToHighlight: ['title', 'introduction']

结果中可以看到,相应的区域已经被 em 标签包裹,这个使用前端就可以使用它去做不同的样式展示。

😓领导无语:根据关键词搜索文章你居然用like去实现?

最后

以上就是本文的全部内容,介绍了 Nest+MeiliSearch 实现全文检索。如果你觉得有意思的话,点点关注点点赞吧~

转载自:https://juejin.cn/post/7366826090836262951
评论
请登录