序言

各位观众老爷们好,很荣幸能邀请到我司虚拟化平台组的研发同学 Vici 做一些在产品中使用 redisearch 的心得分享(加戏)。

“搜索”是很多产品中无法绕开的一个功能模块,smartx 的虚拟化管理平台也提供了对主机,存储等对象的查询功能。近期的研发工作中,我们引入了 redisearch 来优化查询效果。

希望通过阅读本文,能让研发同学更加深入的了解 redisearch 模块一种高效经济的文本检索方案。

全文大约 7000 字,阅读需要 10-15分钟。

0. 为什么是 Redisearch

在引入全文检索之前,我们使用 mongoDB 内置的 正则匹配搜索文本。为了提升性能(正则搜索大部分情况下无法使用索引。)和引入语义搜索功能 (如搜索虚拟机 描述文档的内容), 正则匹配的方式已经远远不够了。我们优先考虑了 mongodb 自带的 text 索引和 search 关键字,考虑到当前客户环境的mongodb 版本较低(2.6),升级会引入额外成本。同时 text索引会给客户环境带来额外的内存压力。所以我们希望能找到一种独立的全文检索方案。

业界常用 elasticsearch 和 lucene 方案都对 JVM 的运行时内存有着最低限额,笔者在选型测试时优先测试了 elasticearch ,其运行时大小推荐 2G 以上的内存空间,并且需要额外的磁盘空间做持久化存储。

相对照的,我们还测试了 redisearch 的内存占用情况和中文分词功能的可用性。

如下结构的测试数据 比较贴近我们的业务搜索场景。


redis检索数据 redis 全文检索功能_mongo客户端


相同数据量的情况下(4-30W),redis 可以比 Elasticearch 节省 约75%的内存占用。

而且,ES 的一些对嵌套文档索引和查询的高级功能也不是需求的业务特性

中文的分词组件(friso)可以满足基本需求,模块化的设计也给我们后续的优化开发提供了余地。

1. Redis Modules

Redis Modules 是 redis 4.0 引入的一种扩展机制,用户可以通过实现 redis module 提供的 C api 接口为 redis 服务添加定制化功能。 redisLab 也希望籍此来规范 redis 社区的 ecosystem 实现。

redis module 本身的版本独立于redis,并且以编译成动态加载库 .so 文件的方式 release, 不同版本的 redis 可以 load 同一版本 module.so 文件。

redis 提供了两种加载方式。可以通过 在 conf 文件中 加入 loadmodule /path/to/mymodule.so ,也可以在 redis-cli 中 使用命令 MODULE LOAD /path/to/panda.so 动态加载,MODULE UNLOAD 卸载。

2. redisearch

Redis 社区两位核心开发@dvirsky和@mnunberg 在 2016 年启动了 redisearch 项目,旨在为 redis 提供全文索引相关的搜索功能(没错, 很像elasticsearch )。 截至2019年1月, redisearch 的最新发布版本是 v1.4.2 。

特性

  • 基于文档的全文索引。
  • 高性能增量索引。
  • 支持文档评分,文档字段(field) 权重机制。
  • 支持布尔复杂查询。
  • 支持自动补全 (未测试)。
  • 基于 snowball 的词干分析,多语言支持。使用 friso 支持中文分词。
  • utf-8 字符集支持。
  • redis 数据持久化支持。
  • 自定义评分机制。

相比 elasticsearch,redisearch 在这些“主营业务”上其实没有什么优势,不过麻雀虽小却也五脏俱全。内存存储,轻量,文档的实时 index & search 特性才是我们选择它的原因。

有些遗憾的是,redisearch 分布式支持功能只在其企业版中可用。

在rediserch 的 的文档说明中,说明了其主要功能的实现都没有使用 redis的原生数据结构。

以全文检索使用到的倒排索引为例,对于常见的一些vm数据:


{


进行分词后的倒排索引简单结构类似:

结构如下:


{text: "我", ids: ["1a2c3d343e",]}, 
{text: "vm", ids: ["1a2c3d343e","1a2c3d343d","1a2c3d343f",]},
{text: "全文", ids:  ["1a2c3d343d",]},
{text: "检索", ids:  ["1a2c3d343d",]},
{text: "测试", ids: ["1a2c3d343e","1a2c3d343f",]}


在 redis 的 hashmap 基础上就可以很容易实现倒排索引的结构。redisearch 倒排索引除了实现了基础功能外,还引入了内存管理等优化功能。如果有兴趣可以阅读源码中的 src/inverted_index.c

快速上手

Redislab 社区推荐测试时使用 docker 快速启动验证环境。


docker run -p 6666:6379 redislabs/redisearch:latest


这里我们同时需要 一个 redis-cli 来进行功能验证:

linux or Darwin:


cd /tmp
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make
cp src/redis-cli /usr/local/bin/
chmod 755 /usr/local/bin/redis-cli


进入交互环境


redis-cli


或者硬核一点, 使用 linux 自带的 nc 命令 与 redisearch 交互:


nc -v 127.0.0.1 6379


检查 modules 是否成功加载


127.0.0.1:6666> MODULE list
1) 1) "name"
   2) "ft"
   3) "ver"
   4) (integer) 10405


如果返回数组中存在 "ft" , 则表明 redisearch 已经成功加载。

创建索引 index

Redisearch 的索引概念 与elasticsearch 的 index 类似,表示某一类文档资源单元。

这里我们定义了一个 SMARTX_VM


127.0.0.1:6666> FT.CREATE SMARTX_VM SCHEMA name TEXT WEIGHT 5.0 desc TEXT


查看索引信息


127.0.0.1:6666> FT.INFO SMARTX_VM


FT.INFO可以查询 index 的metadata. 包括 索引名,字段信息 fields info,已索引文档数, 内存用量等。

向索引添加文档

向之前创建的索引 SMARTX_VM 存储文档:


{
    "id": vm-2019030211110001,
    "name": "测试虚拟机-01",
    "desc": "我在北京故宫也能吃炸鸡"
}
127.0.0.1:6666> FT.ADD SMARTX_VM vm-2019030211110001 1.0 LANGUAGE "chinese" FIELDS
name "测试虚拟机-01" desc "我在北京故宫也能吃炸鸡"


LANGUAGE "chinese" 参数 表示 使用 中文分词器 处理文本。默认为英文.

文档检索

这里直接搜索 “故宫炸鸡” 是检索不到的,因为没有指定合理的文本分词器


127.0.0.1:6666> FT.SEARCH SMARTX_VM "故宫炸鸡"
1) (integer) 0


检索时指定语言类型


127.0.0.1:6666> FT.SEARCH SMARTX_VM "故宫炸鸡" LANGUAGE "chinese"
1) (integer) 1
2) "vm-2019030211110001"
3) 1) "name"
   2) "xe6xb5x8bxe8xafx95xe8x99x9axe6x8bx9fxe6x9cxba-01"
   3) "desc"
...


可以看到已经返回了我们想要的结果。

进阶

存储结构

redisearch的 索引文档存储复用了 redis 的 HASH table 类型。

通过redis 的 HGETALL ,HGET 命令可以查询文档内容。 比如查询上文示例中添加的文档


127.0.0.1:6666> HGETALL vm-2019030211110001


需要注意的是 redisarch 模块并没有做文档的资源隔离,当存在多个索引,不同索引的文档 id 出现重复时,相同 id 的文档将会合并。合理使用这一特性 可以节省内存空间。如果业务层需要严格的资源隔离,则可以使用 redis 的 多个 db,或者为 不同索引的 id 添加 prefix。

接入姿势

在 smartx 虚拟化管理平台,我们将 redisearch 置于 我们的 mongodb 业务数据库之后, 并且实现了一个 mongo 操作日志监听工具 goose,将 mongodb集群中业务方需要检索的文档字段实时同步到 redisearch 中。

用户的检索查询请求会首先从redisearch 获取到匹配文档的id,再从mongoDB 中查询。流程图如下:


redis检索数据 redis 全文检索功能_mongo客户端_02


在mongodb的副本集(replicaSet) 部署中,各个副本节点与主节点使用 oplog 机制同步, oplog 会记录mongo客户端的操作历史,副本节点可以监听主节点的oolog 并重放就可以完成 同步数据的效果。

oplog的 prototype:


type OpLog struct {
   Id                // mongo doc 的 objectId
   Operation         // 操作数: 增,改,删                
   Namespace         // db.collection              
   Timestamp         // 操作时间戳
   Doc               // 文档内容        
}


github 社区的开源同步工具 mongo-connector,就是通过监听 oplog的方式从mongo中做持久化数据同步,然后再根据disparcher 接口将数据分发到 相应的数据组件: elasticsearch,mongo,etc...

Mongo-connector 虽然提供了完备的mongo同步框架,但同步到 redisearch 相关的 dispatcher 仍然需要自行实现。同时其数据 filter 模块也不支持 field 粒度的查询。

出于上述考虑,为了能够更加精细化的做数据筛选同步和兼容产品后端服务,笔者实现了一个可以定制筛选条件的 oplog 同步中间件: goose(鹅)。

产品中的其他服务可以向 goose 注册筛选条件(以toml文件的方式):


# sync VOLUME => "ELF_VOLUME"
[[sync-rule]]
db = "resources"
collection = "resource"
index = "VOLUME"   # also an unique id of rule
id = "_id"
filter = { super_type = "KVM_VOLUME_SUPER", status = "created"}
fields = ["name", "description"]


形如上述配置,goose会从 mongo 的 resouce 中筛选一些满足 supertype = "KVMVOLUME_SUPER" & status = "created" 的文档,将文档中的 name 和 decription 同步到 redisearch 的 VOLUME 集合中,完成从监听 --> 同步 --> 建立全文索引的过程。

相关数据的处理和流向关系用下图表示:


redis检索数据 redis 全文检索功能_redis_03


高可用

由于 redisearch 的开源版本不支持redis的集群模式部署, 该查询服务的高可用部分交由goose 和产品集群的 服务注册和代理服务合作。


redis检索数据 redis 全文检索功能_mongo客户端_04


我们在每个 mongo节点都部署了针对单节点的 数据同步和redisearch服务 并暴露本地6666端口, 如果goose服务健康, goose 会把当前节点注册到集群的 反向代理和负载均衡 服务中。其他服务可以访问集群任意节点的11900端口进行 全文检索查询服务。

开发

在实际开发中,笔者通读了 redisearch python 和 golang 的客户端实现,python-cli 作为社区推荐的客户端实现,目前覆盖封装了了1.4 版本之前的大部分功能, 可以满足开发使用要求。但是 golang 版本比较滞后,很多关键命令(如 ALTER, SUG* 等)没有封装,连接池复用等常用客户端特性均未实现,如有golang 使用需求,建议使用 redigo 项目自行实现。

license

RedisLab 在 2018 年 7月 将其大部分项目的 license 更换到了 Apache License with Commons Clause。

在 2019年 2月份,又替换为 Redis Source Available License Agreement。

为避免商业纠纷,我们也在第一时间联系了Redis社区,了解 license 被修改后相关产品的使用边界。

根据反馈结果,可以确认 redis 依然可以作为一种基础设施内存数据库存在于产品中。但是将redis作为产品的卖点,或封装后直接售卖 是不被允许的。

写在最后

Redisearch 是一个高效,功能完备的内存存储的高性能全文检索组件, 十分适合应用在数据量适中, 内存和存储空间有限的环境。借助数据同步手段,我们可以很方便的将redisearch 结合到现有的数据存储中, 进而向产品提供 全文检索, 自动补全等服务优化功能。