我最近一直在使用RediSearch模块——它是 Redis 生态系统中更引人入胜的发展之一,值得单独写一个系列。
如果你曾使用 Redis 作为主数据存储构建应用程序,你很可能体验过原生数据类型的欣喜与困惑。当你理解了这些数据类型,你会意识到你的大部分数据都可以整洁地适配其中一种。然而,许多常见的应用程序模式既需要索引(“哪个键具有 x 值?”)又需要搜索(“哪个键包含 某个文本字符串?”)。虽然这些问题可以通过创造性地利用原生数据类型来回答,但代码可能会很复杂,并且存在速度和/或空间效率的权衡。RediSearch 模块以较少的权衡填补了这些空白。在本系列第一部分,我们将对该模块进行初步探索,作为温和的入门介绍。
模块是 Redis 服务器的附加组件。在最基本的层面上,它们实现了新的命令,但也可以实现新的数据类型。模块使用系统编程语言编写;C/C++、Rust 和 Golang 都曾被使用,但其他语言也是可能的。由于它们是用编译语言编写的,因此可以实现极高的性能。
模块与 Redis 脚本(Lua)不同之处在于,它们是系统中的一流命令,可以直接与存储接口,从而能够创建自己的数据类型。它们与内置命令唯一的区别是,模块命令通过一个前缀(通常是两个字母)和一个点进行命名空间划分(例如:XX.SOMECOMMAND)。
模块可以在运行时通过 MODULE LOAD 命令加载,也可以在 redis.conf 文件中使用 loadmodule 加载,或者通过命令行参数“loadmodule”加载。我个人偏好通过 conf 文件加载,因为它能确保模块始终可用且配置具有可移植性。
我曾问过自己 RediSearch 不是什么——但我将尝试直接回答。RediSearch 是一个模块,提供三个主要功能:
RediSearch 既利用自身的数据类型,也利用 Redis 内置的数据类型。因此,它更像是一个既使用 Redis 又与 Redis 共存的解决方案。现在这听起来可能有点令人困惑,但请跟着我的思路。
让我们评估一下上述的每个功能。首先,考虑全文搜索。使用 RediSearch,你可以索引尚未处理过的文本。假设你有一百万条客户评论,并且想找到所有提到“rendering”(渲染)的评论。在 RediSearch 出现之前,你当然可以将这些评论存储在 Redis 中(比如存储在哈希中),但在这些评论中查找特定词汇充其量是一件很困难的事。即使你设法构建了自己的词汇到评论的索引(这需要在应用程序层面将每条评论分割成词),匹配也需要完全精确——“render”、“rendering”和“rendered”将无法相互匹配。而通过使用 RediSearch 存储数据,你无需在应用程序层面做任何特殊处理,就能找到所有相关评论,并且 RediSearch 会自动将“rendered”与“rendering”进行匹配,因为它能智能地处理索引和查询。
显然,如果能做到上述功能,那么在没有语言处理智能的情况下也能做到——当你开始思考这一点时,你会意识到 RediSearch 可以用作通用的二级索引。但它也可以超越文本匹配——RediSearch 可以对单个项目(称为“文档”)进行数值和地理位置索引。每个文档可以有多个字段——每个字段都带有各自的属性。
最后,RediSearch 还独立提供了一个建议引擎,可以驱动类似自动补全的服务。这允许你使用已知有效的值,为用户提供“提示”。它基于前缀模型,所以如果用户开始输入“Hamb”,建议引擎会提供,比如,“Hamburger”、“Hambone”和“Hamburg”。值得注意的是,这些建议并未直接与搜索结果集成,因此需要你的应用程序负责从这个建议存储中添加或删除它们。
作为动手实践练习,让我们安装该模块
$ git clone https://github.com/RedisLabsModules/RediSearch.git
$ cd RediSearch
$ make all
$ cd src
$ redis-cli
> MODULE LOAD ./redisearch.so
(或者在你的 redis.conf 文件中安装并重启 redis-server)
模块加载完成后,请在 redis-cli 中运行此命令以验证模块是否正在运行
> module list
1) 1) "name"
2) "ft"
3) "ver"
4) (integer) 2000
在该命令的结果中,你应该会看到每个已安装模块的条目(很可能只有一个)。其中一个条目的 name 字段应显示为“ft”(表示 full text,全文)。这就是 RediSearch 的标识符和命令前缀。你的版本号可能与我的不同,因为这个模块进展很快。
现在模块已启动并运行,最好使用干净的数据库进行这些练习(flushdb 或一个干净的数据库/实例)。首先,让我们创建一个索引并添加一个项目
> FT.CREATE shakespeare SCHEMA line TEXT SORTABLE play TEXT NOSTEM speech NUMERIC SORTABLE speaker TEXT NOSTEM entry TEXT location GEO
这看起来可能有点复杂,特别是如果你习惯于只有一两个参数的命令。让我们来分解一下
FT.CREATE shakespeare
这只是命令和“键”(稍后会详细介绍)
SCHEMA
这表明接下来的参数将是关于搜索索引中的字段。
line TEXT SORTABLE
这里我们创建一个名为 line 的字段,它将保存文本值,并且稍后可以进行排序。
play TEXT NOSTEM
这是名为“play”的字段,用于文本值,但它不会进行词干提取(例如,rendering 将不会匹配 render)
speech NUMERIC SORTABLE
我们正在创建一个名为“speech”的字段,它是数字类型且可排序。
speaker TEXT NOSTEM
就像 play 字段一样,speaker 字段也将保存文本,但只会进行精确的逐词匹配。
entry TEXT
这个字段(entry)保存文本值,这些值会进行处理以实现精确匹配或词干匹配。
location GEO
location 字段保存地理坐标。
看——虽然在一行中内容很多,但实际上并不复杂。
现在,让我们向索引添加一个文档
> FT.ADD shakespeare 57956 1 FIELDS text_entry "Out, damned spot! out, I say!--One: two: why," line "5.1.31" play macbeth speech 15 speaker "LADY MACBETH" location -3.9264,57.5243
比较这两个命令,你可能会注意到 FT.CREATE 和 FT.ADD 命令遵循了类似的模式。让我们更深入地查看这个命令
FT.ADD shakespeare 57956 1
我们正在向索引(shakespeare)添加一个 ID 为 57956 的文档。请注意,在此命令中,文档 ID 是一个数字(只是我使用的数据集的特性),但它可以是任何有效的 Redis 键。本节的最后一个参数是权重——我们将在本系列的稍后部分深入探讨这一点,但现在你只需要知道它可以在 0 到 1 之间,并且 1 是一个不错的默认值。
FIELDS …
“FIELDS” 表示我们将以 [字段名] [值] 的重复模式指定文档的字段。请注意,当值是单个单词或数字时,无需使用引号,但如果使用空格或其他特殊字符,请将值用引号括起来。另一个特殊之处是位置字段,它包含一组坐标(经度,纬度)
回顾一下,我们创建了一个以“shakespeare”为键的索引(通过 FT.CREATE 命令)。让我们做一个快速实验
> TYPE shakespeare
none
奇怪,对吧?这就是我们开始偏离正常的 Redis 行为的地方,你将开始看到 RediSearch 如何成为一个既使用 Redis 又与 Redis 集成的解决方案。
如果你在非生产数据库上运行此操作,为了调试目的,让我们执行 KEYS *
> KEYS *
1) "ft:shakespeare/1"
2) "ft:shakespeare/31"
3) "idx:shakespeare"
4) "ft:shakespeare/5"
5) "ft:shakespeare/macbeth"
6) "ft:shakespeare/lady"
7) "nm:shakespeare/speech"
8) "geo:shakespeare/location"
9) "57956"
运行两个命令产生了 9 个键。我想重点介绍其中几个键,以帮助理解这里实际发生了什么
> TYPE idx:shakespeare
ft_index0
这里我们可以看到 RediSearch 创建了一个拥有其自身数据类型(ft_index0)的键。我们无法直接对这个键进行太多操作,但重要的是知道它存在以及它是如何创建的。
现在,让我们看看键 57956
> TYPE 57956
hash
一个哈希!我们可以操作它——让我们直接看看这个键
> HGETALL 57956
1) "text_entry"
2) "Out, damned spot! out, I say!--One: two: why,"
3) "line"
4) "5.1.31"
5) "play"
6) "macbeth"
7) "speech"
8) "15"
9) "speaker"
10) "LADY MACBETH"
11) "location"
12) "-3.9264,57.5243"
这应该看起来很熟悉,因为它来自 FT.ADD 命令中的数据,而键就是你的文档 ID。虽然了解它是如何存储的很很重要,但不要直接使用 HASH 命令操作这个键。
> TYPE nm:shakespeare/speech
numericdx
有趣的是——我们数据集中的 speech 字段是一个数字索引,其类型是“numericdx”。同样,由于这是 RediSearch 的原生数据类型,我们无法使用任何“普通”的 Redis 命令来操作它。
> TYPE geo:shakespeare/location
zset
这里的键给了你一个提示——虽然 TYPE 命令返回它是一个 ZSET,但 Redis 的地理位置哈希集合就是存储为 ZSET 的,并且在查询类型时也会报告为 ZSET。话虽如此,让我们看几个 GEO 命令
> GEOHASH geo:shakespeare/location 1
1) "gfjpnxuzk40"
> GEOPOS geo:shakespeare/location 1
1) 1) "-3.92640262842178345"
2) "57.52429905544970268"
太棒了!RediSearch 已将坐标存储在一个标准的 GEO 集合中。但是,就像上面的哈希一样,不要直接使用 ZSET 或 GEO 命令修改这些值。
最后,让我们看看另一个键
> TYPE ft:shakespeare/lady
ft_invidx
敏锐的读者可能会注意到,“lady”这个词只在一个全文字段(speaker)中被索引。存储在 ft_invidx 键中的数据是文本索引。
既然我们对 RediSearch 如何存储数据有了一些了解,我们可以开始向数据库加载更多实质性信息并探索查询,但这将留待几周后发布的《掌握 RediSearch》第二部分。