土豆不好吃

轻量级全文搜索引擎 Meilisearch使用体验

文章目录[显示]

自从人人影视分享站被攻击之后,我便一直在想如何提升搜索性能。

问题分析

人人影视分享站的搜索功能主要包括这三大模块:

  1. 搜索yyets数据库的cnnameennamealiasname三个字段,也就是之前爬下来的老的数据库的内容
  2. 搜索评论信息,同时找到评论所在页面
  3. 如果以上都无结果,那么去搜索其他网站

三大模块的优化方案

但是以上方案无论怎么折腾,都逃不过搜索时要全表检索,只要涉及到全表检索那肯定会重IO重CPU。

那不请求数据库,加redis做缓存呢?好是好,但是命中率会很低,因为每个人搜索关键词可能差很多,而且缓存多久合适?很难搞的问题

那有没有更好的办法呢,那种搜索很快的数据库?当然有的了,比如知名的Elastic Search

全文搜索引擎

ElasticSearch可谓是最致命知名的全文搜索引擎了,几年前在做实习生的时候简单地接触过。那时候在我的眼中,ES似乎和MongoDB没什么区别。

为了能够让人人影视分享站的搜索功能更上一个台阶,我需要选择一个全文搜索引擎。

ES确实是非常知名的全文搜索引擎,但是同时也非常致命:需要非常的配置,尤其是内存,官方建议似乎最少8G了,我真的很穷😭

我对于这个全文搜索的要求如下:

  1. 轻量级,对配置要求比较低
  2. 开发活跃,有相应的Python SDK
  3. 支持CJK
  4. 最好再支持docker,真的不想自己写Dockerfile,太累了
  5. 使用简单,像MongoDB那样随便插入,不需要定义schema,心智负担小一点吧

网上搜索了一圈,发现了这么几个比较不错的产品

  1. sonic 官方宣称轻量级的ES替代品,支持几十种语言。坏消息是Python library没一个能用的,全挂了。官方维护的nodejs的倒还可以,不愧是亲儿子啊
  2. Tantivy 官方说也支持十几种语言,并且可以通过插件支持CJK,还有一个tantivy-py,rust的bingding,虽然没啥自动补全了,安装时说不准还得来个rust,但是也能用。缺点就是,这个tantivy-py,不支持第三方tokenizer,那和不支持CJK有什么区别嘛……
  3. Typesense 也是非常有名的全文搜索引擎,不过对CJK的支持不太完善,并且需要定义schema

最终我找到了今天的主角,Meilisearch,美丽搜索。支持CJK,可以容忍错别字,有官方的各种语言的SDK,自带一个简单的检索页面,使用rust构建,同样轻量级

Meilisearch消费数据

对于全文检索来说最重要的无非是两件事情,存储数据和检索。对Meilisearch来说这很简单:

import Meilisearch

client = Meilisearch.Client("http://127.0.0.1:7700", "masterKey")
index = client.index("demo")
documents = [
    {
        "id": 123,
        "title": "后端的接口大概在半年前就写好了"
    },
    {
        "id": 456,
        "title": "我最近才学了一点点 React做好了邮件验证的功能"
    }
]
index.add_documents(documents)

可一次性提交很多数据,Meilisearch会异步处理。如果想要知道处理结果可以看tasks结果,如:

task = index.add_documents(documents)
time.sleep(1)
print(index.get_task(task.task_uid))

Meilisearch的ID

Meilisearch要求数据包含ID,如果你提交的数据是这样的

documents = [
    {
        "title": "后端的接口大概在半年前就写好了",
        "titleId": "123a",
        "content": "好的",
        "contentId": "yagwah2"
    },
    {
        "title": "我最近才学了一点点 React做好了邮件验证的功能",
        "titleId": "456b",
        "content": "不好",
        "contentId": "9777"
    },
]

你会发现在网页上看不到这些索引,这是因为Meilisearch要求数据中包含唯一ID,这也是以后更新索引的依据。给每一条数据加上一个唯一的ID就好了。或者:

如果你的数据中不包括id字段,那么Meilisearch将会尝试自动从其他类似ID的字段中推导,比如你的数据是:

{
    "username": "abcdefg",
    "date": "2021-06-13 22:32:48",
    "comment": "很好看",
    "commentID": "60c61710004833fc8cd4b240",
    "origin": "comment",
    "hasAvatar": None,
    "resourceName": "急诊室的故事",
}

Meilisearch就会把commentID当作ID去使用。但是如果非常不巧你的数据中包含多个ID结尾的字段,如

{
    "username": "abcdefg",
    "date": "2021-06-13 22:32:48",
    "comment": "很好看",
    "commentID": "60c61710004833fc8cd4b240",
    "origin": "comment",
    "hasAvatar": None,
    "resourceID": 10323,
    "resourceName": "急诊室的故事",
}

那么就会推导失败,此时要么自己添加好id,要么指定主键名

index.add_documents(documents, primary_key="commentID")

Meilisearch检索数据

非常简单高效,唯一的缺点是没有提供confidence score之类的东西,有的时候搜出来的东西实在是离谱🤡

result = index.search("棒")

不用考虑简体繁体,有非常多的参数可以选择,具体可以看文档

集成Meilisearch

把Meilisearch集成到人人影视分享站,要考虑的问题如下:

  1. 数据持久化,我的所有应用都是docker跑的,这样就一定要考虑数据持久化的事情
  2. 消费数据,要保证Meilisearch的数据和MongoDB同步更新,并且在每次重启容器时也不会有丢失
  3. 保证返回一样的数据结构,避免前端改来改去

数据持久化

Meilisearch的数据并不重要,因此不需要映射宿主机的目录到容器之中,只需要docker volume就可以了。

docker volume create meili

然后

docker run -v meili:/meili_data getmeili/meilisearch:v1.0.2

docker-compose的话,也基本一样

version: '3.1'

services:
  meili:
    image: getmeili/meilisearch:v1.0.2
    volumes:
      - meilisearch:/meili_data

volumes:
  meilisearch:

第一次消费数据

由于Meilisearch是异步处理数据的,也就意味着提交一大堆数据之后,我们并不会阻塞。

因此问题就变成了如何更优雅的从MongoDB中查询到我们想要的数据,然后一股脑的塞过去。

此时就要用到 MongoDB的aggregate了,非常强大的功能,一个查询就可以全部搞定,不用拿Python再遍历然后二次查

我的第一次消费数据也非常简单粗暴,程序启动时丢个线程去跑就好了,反正也没多少,第一次几秒钟就结束

threading.Thread(target=engine.run_import).start()

同步数据

MongoDB有一个功能叫做 Change Stream,这是一种发布订阅的模式,简单的说就是当数据库发生变更,MongoDB会“推送”数据过去。

唯一的小缺点是,数据库药需要配置成replica set才可以。当然了,只有一个节点的replica set也没问题啦。

用起来很容易,先在配置文件中启用replica set,如果用的docker,那么command追加--replSet rs0即可,然后启动MongoDB,

配置replica set,比如我要配置两个节点的

rs.initiate({
    _id: "rs0",
    members: [{
        _id: 0,
        host: "localhost:27017"
    },
    {
        _id: 1,
        host: "mongo2:27017"
    }]
})

rs.status()可以查看配置结果

多个节点的话,等到数据同步好了,可选配置一下优先级

cfg = rs.conf()
cfg.members[0].priority = 0.5
cfg.members[1].priority = 0.5
cfg.members[2].priority = 1 # 最高
rs.reconfig(cfg)

配置成功之后,比如我想监听comment这个集合的变化

cursor = self.db.comment.watch()
for change in cursor:
    print(change)

无论是增删改都会在这个change中体现。我同样也很简单粗暴,拿到数据了add_documents 就好。

数据结构

由于在喂数据的时候就是根据前端的要的数据构造的,所以这里根本不用管,直接把Meilisearch返回的结果返回给前端就好了

性能对比

在开了省电模式的M1 Macbook单核心、2G内存的docker engine环境,用ab -c 100 -n 2000 的跑分,优化前:

优化后:

每秒请求次数从20左右提升到200多,直接10倍,这不比升级机器配置好多啦!并且IO、CPU、RAM占用也比MongoDB理想多了。

 

后续

我也不知道这个人是谁,还在孜孜不倦的爬呀爬😂 虽然并无任何影响,但是看着就很恶心人,有什么好的办法吗

 


文章版权归原作者所有丨本站默认采用CC-BY-NC-SA 4.0协议进行授权|
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/meilisearch.html
退出移动版