二级索引

在 Redis 中构建二级索引

Redis 不完全是键值存储,因为值可以是复杂的数据结构。但是它有一个外部键值外壳:在 API 级别,数据由键名寻址。可以公平地说,Redis 本身只提供主键访问。但是由于 Redis 是一个数据结构服务器,因此其功能可用于索引,以创建不同类型的二级索引,包括复合(多列)索引。

本文档说明如何使用以下数据结构在 Redis 中创建索引

  • 有序集合用于根据 ID 或其他数字字段创建二级索引。
  • 具有词法范围的有序集合,用于创建更高级的二级索引、复合索引和图遍历索引。
  • 集合用于创建随机索引。
  • 列表用于创建简单的可迭代索引和最后 N 个项目索引。

在 Redis 中实现和维护索引是一个高级主题,因此大多数需要对数据执行复杂查询的用户应该了解是否使用关系型存储更适合他们。但是,通常情况下,尤其是在缓存场景中,需要将索引数据存储到 Redis 中以加快常见查询的速度,这些查询需要某种形式的索引才能执行。

使用有序集合创建简单的数值索引

您可以使用 Redis 创建的最简单的二级索引是使用有序集合数据类型,这是一种数据结构,表示由浮点数(每个元素的分数)排序的一组元素。元素按分数从最小到最大排序。

由于分数是双精度浮点数,因此您可以使用普通有序集合构建的索引仅限于索引字段是给定范围内的数字的情况。

构建这些索引的两个命令是 ZADDZRANGE,它们带有 BYSCORE 参数,分别用于添加项目和检索指定范围内的项目。

例如,可以通过将元素添加到有序集合来索引一组人员名称及其年龄。元素将是人员的姓名,分数将是年龄。

ZADD myindex 25 Manuel
ZADD myindex 18 Anna
ZADD myindex 35 Jon
ZADD myindex 67 Helen

要检索所有年龄在 20 到 40 之间的人员,可以使用以下命令

ZRANGE myindex 20 40 BYSCORE
1) "Manuel"
2) "Jon"

通过使用 ZRANGEWITHSCORES 选项,也可以获取与返回元素相关联的分数。

可以使用 ZCOUNT 命令检索给定范围内的元素数量,而无需实际获取元素,这一点也很有用,尤其是考虑到操作在对数时间内执行,无论范围大小如何。

范围可以是包含的或排除的,请参阅 ZRANGE 命令文档以了解更多信息。

注意:使用带有 BYSCOREREV 参数的 ZRANGE,可以按相反顺序查询范围,这在数据以给定方向(升序或降序)索引但我们要以相反方向检索信息时通常很有用。

使用对象 ID 作为关联值

在上面的示例中,我们将姓名与年龄相关联。但是,通常我们可能想要索引存储在其他地方的对象的某个字段。与其直接使用有序集合值来存储与索引字段关联的数据,不如只存储对象的 ID。

例如,我可能拥有表示用户的 Redis 哈希。每个用户都由一个单独的键表示,可以直接通过 ID 访问

HMSET user:1 id 1 username antirez ctime 1444809424 age 38
HMSET user:2 id 2 username maria ctime 1444808132 age 42
HMSET user:3 id 3 username jballard ctime 1443246218 age 33

如果我想创建索引以按年龄查询用户,我可以这样做

ZADD user.age.index 38 1
ZADD user.age.index 42 2
ZADD user.age.index 33 3

这次,与有序集合中的分数关联的值是对象的 ID。因此,一旦我使用带有 BYSCORE 参数的 ZRANGE 查询索引,我还需要使用 HGETALL 或类似命令来检索所需的信息。显而易见的优势是,只要我们不更改索引字段,对象就可以在不影响索引的情况下发生更改。

在接下来的示例中,我们几乎总是使用 ID 作为与索引关联的值,因为这通常是更合理的設計,只有少数例外。

更新简单的有序集合索引

通常我们索引随时间变化的事物。在上面的示例中,用户的年龄每年都会变化。在这种情况下,使用出生日期作为索引而不是年龄本身将更有意义,但还有其他情况,我们只是希望某个字段不时发生变化,并且索引反映这种变化。

使用 ZADD 命令可以使更新简单索引成为一项非常简单的操作,因为重新添加具有不同分数和相同值的元素只会更新分数并将元素移动到正确的位置,因此,如果用户 antirez 年满 39 岁,为了更新表示用户的哈希中的数据以及索引,我们需要执行以下两个命令

HSET user:1 age 39
ZADD user.age.index 39 1

操作可以包装在 MULTI/EXEC 事务中,以确保两个字段都被更新或都不更新。

将多维数据转换为线性数据

使用有序集合创建的索引只能索引单个数值。由于这个原因,您可能认为使用这种索引不可能索引具有多个维度的某些东西,但实际上并非总是如此。如果您能够以线性方式有效地表示多维的东西,那么通常可以使用简单的有序集合来进行索引。

例如,Redis 地理索引 API 使用有序集合通过纬度和经度来索引位置,使用一种称为 Geo hash 的技术。有序集合分数表示经度和纬度的交替位,这样我们就可以将有序集合的线性分数映射到地球表面上的许多小方格。通过执行 8+1 风格的中心加邻域搜索,可以按半径检索元素。

分数的限制

有序集合元素分数是双精度浮点数。这意味着它们可以使用不同的误差来表示不同的十进制或整数值,因为它们在内部使用指数表示法。但是,对于索引目的而言,有趣的是分数始终能够在没有任何误差的情况下表示 -9007199254740992 和 9007199254740992 之间的数字,即 -/+ 2^53

在表示更大的数字时,您需要一种能够以任何精度索引数字的不同形式的索引,称为词典索引。

词典索引

Redis 有序集合有一个有趣的属性。当元素以相同的分数添加时,它们按词典顺序排序,使用 memcmp() 函数将字符串作为二进制数据进行比较。

对于不了解 C 语言或 memcmp 函数的人来说,这意味着具有相同分数的元素是通过比较其字节的原始值逐字节进行排序的。如果第一个字节相同,则检查第二个字节,依此类推。如果两个字符串的公共前缀相同,则较长的字符串被认为是两者中较大的,因此 "foobar" 大于 "foo"。

有一些命令,如 ZRANGEZLEXCOUNT,能够按词典顺序查询和计数范围,假设它们与所有元素都具有相同分数的有序集合一起使用。

此 Redis 功能基本上等同于 b-tree 数据结构,该结构通常用于在传统数据库中实现索引。正如您可能猜到的,由于这个原因,可以使用此 Redis 数据结构来实现非常奇特的索引。

在我们深入研究词典索引的使用之前,让我们检查一下有序集合在这种特殊操作模式下的行为。由于我们需要添加具有相同分数的元素,因此我们始终使用 0 的特殊分数。

ZADD myindex 0 baaa
ZADD myindex 0 abbb
ZADD myindex 0 aaaa
ZADD myindex 0 bbbb

从有序集合中获取所有元素立即显示它们按词典顺序排序。

ZRANGE myindex 0 -1
1) "aaaa"
2) "abbb"
3) "baaa"
4) "bbbb"

现在,我们可以使用带有 BYLEX 参数的 ZRANGE 来执行范围查询。

ZRANGE myindex [a (b BYLEX
1) "aaaa"
2) "abbb"

请注意,在范围查询中,我们在标识范围的 minmax 元素之前添加了特殊字符 [(。这些前缀是必需的,它们指定范围的元素是包含的还是排除的。因此,范围 [a (b 表示给我所有在 a 包含和 b 排除之间按词典顺序排列的元素,即所有以 a 开头的元素。

还有两个特殊字符表示无穷负字符串和无穷正字符串,分别是 -+

ZRANGE myindex [b + BYLEX
1) "baaa"
2) "bbbb"

基本上就是这样。让我们看看如何使用这些功能来构建索引。

第一个示例:完成

索引的一个有趣的应用是完成。完成是指您开始在搜索引擎中键入查询时发生的事情:用户界面会预测您可能键入的内容,提供以相同字符开头的常见查询。

完成的幼稚方法是将用户发送的每个查询都添加到索引中。例如,如果用户搜索 banana,我们会这样做

ZADD myindex 0 banana

依此类推,对于每次遇到的搜索查询都是如此。然后,当我们要完成用户输入时,我们使用带有 BYLEX 参数的 ZRANGE 执行范围查询。想象一下用户在搜索表单中键入 "bit",并且我们要提供以 "bit" 开头的可能的搜索关键字。我们向 Redis 发送如下命令

ZRANGE myindex "[bit" "[bit\xff" BYLEX

基本上,我们使用用户当前键入的字符串作为起点创建一个范围,并使用与该字符串相同的字符串加上一个设置为 255 的尾部字节(在示例中为 \xff)作为范围的终点。这样,我们可以获取所有以用户键入的字符串开头的字符串。

请注意,我们不希望返回太多项目,因此可以使用 LIMIT 选项来减少结果数量。

将频率添加到组合中

以上方法有点幼稚,因为所有用户搜索在这个方面都是一样的。在真实系统中,我们希望根据频率完成字符串:与很少键入的搜索字符串相比,非常流行的搜索将被提议的可能性更高。

为了实现依赖频率的东西,同时自动适应未来的输入,通过清除不再流行的搜索,我们可以使用非常简单的流式算法

首先,我们修改索引,以便不只存储搜索词,还存储与搜索词关联的频率。因此,与其只添加 banana,不如添加 banana:1,其中 1 是频率。

ZADD myindex 0 banana:1

我们还需要逻辑来递增索引,如果搜索词已存在于索引中,因此我们将实际执行类似于以下的操作

ZRANGE myindex "[banana:" + BYLEX LIMIT 0 1
1) "banana:1"

这将返回 banana 的单个条目(如果存在)。然后,我们可以递增关联的频率并发送以下两个命令

ZREM myindex 0 banana:1
ZADD myindex 0 banana:2

请注意,由于可能存在并发更新,因此以上三个命令应通过 Lua 脚本 发送,以便 Lua 脚本原子地获取旧计数并重新添加具有递增分数的项目。

因此,结果将是,每次用户搜索 banana 时,我们的条目都会更新。

还有更多:我们的目标是只保留搜索频率很高的项目。因此,我们需要某种形式的清除。当我们实际查询索引以完成用户输入时,我们可能会看到类似于以下内容

ZRANGE myindex "[banana:" + BYLEX LIMIT 0 10
1) "banana:123"
2) "banaooo:1"
3) "banned user:49"
4) "banning:89"

显然,没有人搜索 "banaooo",例如,但查询执行了一次,因此我们最终将它呈现给用户。

这就是我们能做的事情。在返回的项目中,我们随机选取一个,将其分数减一,并以新的分数重新添加。但是,如果分数达到 0,我们只需从列表中删除该项目。您可以使用更高级的系统,但想法是,从长远来看,索引将包含热门搜索,如果热门搜索随着时间推移而发生变化,它将自动适应。

对该算法的改进是根据列表中条目的权重来选择条目:分数越高,选择条目以减少其分数或将其驱逐的可能性越低。

规范化字符串的大小写和重音符

在完成示例中,我们始终使用小写字符串。但是现实比这要复杂得多:语言有首字母大写的名称、重音符等等。

处理这些问题的一种简单方法是实际规范化用户搜索的字符串。无论用户搜索“Banana”、“BANANA”还是“Ba'nana”,我们都可以将其始终转换为“banana”。

但是,有时我们可能希望向用户展示他们输入的原始项目,即使我们规范化了用于索引的字符串。为了做到这一点,我们需要做的就是更改索引的格式,以便不再仅仅存储 term:frequency,而是存储 normalized:frequency:original,如下例所示:

ZADD myindex 0 banana:273:Banana

基本上,我们添加了另一个字段,我们只将其提取出来用于可视化。范围将始终使用规范化的字符串进行计算。这是一种常见的技巧,它有多种应用。

在索引中添加辅助信息

当以直接方式使用排序集时,每个对象有两个不同的属性:分数,我们用它作为索引,以及一个关联的值。当使用词典索引时,分数始终设置为 0 并且基本上根本不用。我们只剩下一个字符串,即元素本身。

就像我们在之前的完成示例中所做的那样,我们仍然可以使用分隔符来存储关联数据。例如,我们使用冒号来添加频率和原始单词以供完成。

一般来说,我们可以将任何类型的关联值添加到我们的索引键中。为了使用词典索引来实现一个简单的键值存储,我们只需将条目存储为 key:value

ZADD myindex 0 mykey:myvalue

并在以下位置搜索密钥:

ZRANGE myindex [mykey: + BYLEX LIMIT 0 1
1) "mykey:myvalue"

然后,我们提取冒号后面的部分来检索值。但是,在这种情况下,要解决的问题是冲突。冒号字符本身可能就是密钥的一部分,因此必须选择它以防止与我们添加的密钥发生冲突。

由于 Redis 中的词典范围是二进制安全的,因此您可以使用任何字节或任何字节序列。但是,如果您收到不可信的用户输入,最好使用某种形式的转义来保证分隔符永远不会成为密钥的一部分。

例如,如果您使用两个空字节作为分隔符 "\0\0",您可能希望始终将空字节转义为字符串中的两个字节序列。

数字填充

词典索引可能看起来只有在手头的问题是索引字符串时才有效。实际上,使用这种类型的索引来执行任意精度数字的索引非常简单。

在 ASCII 字符集中,数字按从 0 到 9 的顺序出现,因此,如果我们在数字前面用前导零填充,则结果是将它们作为字符串比较将按其数值对它们进行排序。

ZADD myindex 0 00324823481:foo
ZADD myindex 0 12838349234:bar
ZADD myindex 0 00000000111:zap

ZRANGE myindex 0 -1
1) "00000000111:zap"
2) "00324823481:foo"
3) "12838349234:bar"

我们有效地创建了一个使用数值字段的索引,该字段可以像我们想要的那样大。这对于任何精度的浮点数也有效,确保我们使用前导零填充数字部分,使用尾随零填充小数部分,如下面的数字列表所示:

    01000000000000.11000000000000
    01000000000000.02200000000000
    00000002121241.34893482930000
    00999999999999.00000000000000

使用二进制形式的数字

以十进制形式存储数字可能会占用太多内存。另一种方法是直接以二进制形式存储数字,例如 128 位整数。但是,要使这工作,您需要以 *大端格式* 存储数字,以便最高有效字节存储在最低有效字节之前。这样,当 Redis 使用 memcmp() 比较字符串时,它将有效地按数字值对数字进行排序。

请记住,以二进制格式存储的数据在调试时可观察性较差,解析和导出也更难。因此,这绝对是一个权衡。

组合索引

到目前为止,我们探索了索引单个字段的方法。但是,我们都知道 SQL 存储可以创建使用多个字段的索引。例如,我可能在一个非常大的商店中按房间号和价格对产品进行索引。

我需要运行查询来检索给定房间中具有给定价格范围的所有产品。我可以按以下方式对每个产品进行索引

ZADD myindex 0 0056:0028.44:90
ZADD myindex 0 0034:0011.00:832

这里,字段是 room:price:product_id。为了简单起见,我在示例中只使用了四位数字填充。辅助数据(产品 ID)不需要任何填充。

使用这样的索引,要获取 56 号房间中价格在 10 到 30 美元之间的所有产品非常容易。我们只需运行以下命令

ZRANGE myindex [0056:0010.00 [0056:0030.00 BYLEX

以上被称为组合索引。它的有效性取决于字段的顺序以及我想运行的查询。例如,上面的索引不能有效地用于获取所有具有特定价格范围的产品,无论房间号如何。但是,我可以使用主键来运行与价格无关的查询,例如 *给我所有在 44 号房间中的产品*。

组合索引非常强大,并且在传统存储中用于优化复杂的查询。在 Redis 中,它们可能既可用于实现传统数据存储中存储的某些内容的超快内存内 Redis 索引,也可用于直接索引 Redis 数据。

更新词典索引

词典索引中索引的值可能变得非常复杂,并且难以或缓慢地从我们关于对象的存储中重建。因此,简化索引处理的一种方法(以使用更多内存为代价)是,除了代表索引的排序集之外,还使用一个哈希将对象 ID 映射到当前索引值。

因此,例如,当我们索引时,我们还添加到哈希中

MULTI
ZADD myindex 0 0056:0028.44:90
HSET index.content 90 0056:0028.44:90
EXEC

这并非始终需要,但简化了更新索引的操作。为了删除我们为对象 ID 90 索引的旧信息(无论对象的 *当前* 字段值如何),我们只需通过对象 ID 检索哈希值,并使用 ZREM 在排序集视图中删除它。

使用六元组表示和查询图

组合索引的一件很酷的事情是,它们对于使用称为 六元组 的数据结构来表示图非常有用。

六元组提供了一种表示对象之间关系的方法,这种关系由 *主体*、*谓词* 和 *宾语* 组成。对象之间的一个简单关系可能是

antirez is-friend-of matteocollina

为了表示这种关系,我可以将以下元素存储在我的词典索引中

ZADD myindex 0 spo:antirez:is-friend-of:matteocollina

请注意,我在我的项目前面添加了字符串 spo。这意味着该项目表示主体、谓词、宾语关系。

我可以为相同的关系添加 5 个条目,但顺序不同

ZADD myindex 0 sop:antirez:matteocollina:is-friend-of
ZADD myindex 0 ops:matteocollina:is-friend-of:antirez
ZADD myindex 0 osp:matteocollina:antirez:is-friend-of
ZADD myindex 0 pso:is-friend-of:antirez:matteocollina
ZADD myindex 0 pos:is-friend-of:matteocollina:antirez

现在事情开始变得有趣,我可以以多种不同的方式查询图。例如,所有 antirez *是朋友的* 人是谁?

ZRANGE myindex "[spo:antirez:is-friend-of:" "[spo:antirez:is-friend-of:\xff" BYLEX
1) "spo:antirez:is-friend-of:matteocollina"
2) "spo:antirez:is-friend-of:wonderwoman"
3) "spo:antirez:is-friend-of:spiderman"

或者,antirezmatteocollina 有哪些关系,其中第一个是主体,第二个是宾语?

ZRANGE myindex "[sop:antirez:matteocollina:" "[sop:antirez:matteocollina:\xff" BYLEX
1) "sop:antirez:matteocollina:is-friend-of"
2) "sop:antirez:matteocollina:was-at-conference-with"
3) "sop:antirez:matteocollina:talked-with"

通过组合不同的查询,我可以提出复杂的问题。例如:*所有喜欢啤酒、住在巴塞罗那,并且马特奥·科利纳也认为是朋友的人是谁?* 要获取此信息,我从一个 spo 查询开始,以找到所有与我为友的人。然后,对于我得到的每个结果,我执行一个 spo 查询来检查他们是否喜欢啤酒,删除找不到这种关系的人。我再次这样做以按城市进行筛选。最后,我执行一个 ops 查询,以找到在我获得的列表中,谁被马特奥·科利纳认为是朋友。

请务必查看 马特奥·科利纳关于 Levelgraph 的幻灯片,以更好地理解这些想法。

多维索引

一种更复杂的索引类型是一种索引,它允许您执行查询,其中同时查询两个或多个变量以查找特定范围。例如,我可能有一个表示人员年龄和薪资的数据集,并且我想检索所有年龄在 50 到 55 岁之间且薪资在 70000 到 85000 之间的人。

此查询可以使用多列索引执行,但这需要我们选择第一个变量,然后扫描第二个变量,这意味着我们可能做的工作比实际需要更多。可以使用不同的数据结构来执行涉及多个变量的这些类型的查询。例如,有时会使用多维树(如 *k-d 树* 或 *r 树*)。在这里,我们将描述一种不同的方法来将数据索引到多个维度,使用一种表示技巧,使我们能够使用 Redis 词典范围以非常有效的方式执行查询。

假设我们在空间中有点,它们代表我们的数据样本,其中 xy 是我们的坐标。这两个变量的最大值都是 400。

在下图中,蓝色框代表我们的查询。我们想要所有 x 在 50 到 100 之间且 y 在 100 到 300 之间的所有点。

Points in the space

为了表示使这些类型的查询快速执行的数据,我们首先使用 0 填充我们的数字。因此,例如,假设我们要将点 10,25 (x,y) 添加到我们的索引中。鉴于示例中的最大范围是 400,我们可以简单地填充到三位数字,从而得到

x = 010
y = 025

现在,我们要做的就是交错数字,从 x 中取出最左边的数字,从 y 中取出最左边的数字,依此类推,以创建一个单一数字

001205

这就是我们的索引,但是为了更轻松地重建原始表示(如果我们想要,以空间为代价),我们也可以添加原始值作为附加列

001205:10:25

现在,让我们来讨论这种表示以及它在范围查询环境中的原因。例如,让我们以蓝色框的中心为例,它位于 x=75y=200 处。我们可以像前面一样对这个数字进行编码,通过交错数字,得到

027050

如果我们分别用 00 和 99 替换最后两位数字,会发生什么?我们得到一个词典上连续的范围

027000 to 027099

这映射到一个正方形,代表所有 x 变量在 70 到 79 之间,y 变量在 200 到 209 之间的所有值。为了识别这个特定区域,我们可以写入该间隔中的随机点。

Small area

因此,上述词典查询使我们能够轻松地查询特定正方形中的点。但是,正方形可能对我们要搜索的框来说太小了,因此需要太多的查询。因此,我们可以做同样的事情,但不是用 00 和 99 替换最后两位数字,而是用 00 和 99 替换最后四位数字,得到以下范围

020000 029999

这一次,范围表示所有 x 在 0 到 99 之间,y 在 200 到 299 之间的所有点。在该间隔中绘制随机点会向我们显示这个更大的区域。

Large area

现在我们的区域对于我们的查询来说太大了,而且我们的搜索框还没有完全包含在内。我们需要更细粒度的划分,但我们可以通过将数字表示成二进制形式来轻松地获得它。这次,当我们替换数字而不是得到十倍大的正方形时,我们得到的是仅仅两倍大的正方形。

假设我们每个变量只需要 9 个比特(为了表示最大值为 400 的数字),那么我们的二进制数字将是

x = 75  -> 001001011
y = 200 -> 011001000

因此,通过交错数字,我们在索引中的表示将是

000111000011001010:75:200

让我们看看当我们将交错表示中的最后 2、4、6、8 ... 位替换成 0 和 1 时,我们的范围是什么

2 bits: x between 74 and 75, y between 200 and 201 (range=2)
4 bits: x between 72 and 75, y between 200 and 203 (range=4)
6 bits: x between 72 and 79, y between 200 and 207 (range=8)
8 bits: x between 64 and 79, y between 192 and 207 (range=16)

等等。现在我们有了更好的粒度!如你所见,从索引中替换 N 个比特会给我们边长为 2^(N/2) 的搜索框。

因此,我们所做的是检查搜索框较小的维度,并检查这个数字的最近的 2 的幂。我们的搜索框是 50,100 到 100,300,因此它的宽度为 50,高度为 200。我们取两者中较小的那个,即 50,并检查最近的 2 的幂,即 64。64 是 2^6,因此我们将使用通过替换交错表示中的最后 12 位得到的索引(这样我们最终只替换了每个变量的 6 位)。

但是单个正方形可能不能覆盖我们所有的搜索,因此我们可能需要更多。我们所做的是从搜索框的左下角开始,即 50,100,通过将每个数字的最后 6 位替换为 0 来找到第一个范围。然后我们对右上角做同样的事情。

使用两个简单的嵌套 for 循环,我们只递增有效位,就可以找到这两个范围之间的所有正方形。对于每个正方形,我们将两个数字转换为交错表示,并使用转换后的表示作为起始范围创建范围,并使用相同的表示但将最后 12 位设置为 1 作为结束范围。

对于找到的每个正方形,我们执行查询并获取内部的元素,移除那些在搜索框外部的元素。

将这些转化为代码很简单。这里有一个 Ruby 示例

def spacequery(x0,y0,x1,y1,exp)
    bits=exp*2
    x_start = x0/(2**exp)
    x_end = x1/(2**exp)
    y_start = y0/(2**exp)
    y_end = y1/(2**exp)
    (x_start..x_end).each{|x|
        (y_start..y_end).each{|y|
            x_range_start = x*(2**exp)
            x_range_end = x_range_start | ((2**exp)-1)
            y_range_start = y*(2**exp)
            y_range_end = y_range_start | ((2**exp)-1)
            puts "#{x},#{y} x from #{x_range_start} to #{x_range_end}, y from #{y_range_start} to #{y_range_end}"

            # Turn it into interleaved form for ZRANGE query.
            # We assume we need 9 bits for each integer, so the final
            # interleaved representation will be 18 bits.
            xbin = x_range_start.to_s(2).rjust(9,'0')
            ybin = y_range_start.to_s(2).rjust(9,'0')
            s = xbin.split("").zip(ybin.split("")).flatten.compact.join("")
            # Now that we have the start of the range, calculate the end
            # by replacing the specified number of bits from 0 to 1.
            e = s[0..-(bits+1)]+("1"*bits)
            puts "ZRANGE myindex [#{s} [#{e} BYLEX"
        }
    }
end

spacequery(50,100,100,300,6)

虽然这并不简单,但这是一个非常有用的索引策略,将来可能在 Redis 中以原生方式实现。目前,好处是复杂性可以轻松地封装在一个库中,该库可以用来执行索引和查询。这样的库的一个例子是 Redimension,这是一个用 Ruby 编写的概念验证库,它使用这里描述的技术在 Redis 中索引 N 维数据。

多维索引与负数或浮点数

表示负值的简单方法是使用无符号整数,并使用偏移量表示它们,以便在索引时,在将数字转换为索引表示之前,添加最小负整数的绝对值。

对于浮点数,最简单的办法可能是将它们转换为整数,方法是将整数乘以十的幂,该幂与要保留的小数点后的位数成正比。

非范围索引

到目前为止,我们检查了对范围或单个项目进行查询有用的索引。但是,Redis 的其他数据结构,如集合或列表,可以用来构建其他类型的索引。它们被非常普遍地使用,但也许我们并没有总是意识到它们实际上是一种索引形式。

例如,我可以将对象 ID 索引到一个集合数据类型中,以便使用 SRANDMEMBER 命令执行获取随机元素的操作来检索一组随机对象。集合也可以用来检查是否存在,当我只需要测试一个给定的项目是否存在或是否具有单个布尔属性时。

类似地,列表可以用来将项目按固定顺序索引。我可以将所有项目添加到一个 Redis 列表中,并使用 RPOPLPUSH 命令使用相同的键名作为源和目标来旋转列表。这在我想以相同的顺序一遍又一遍地处理给定的项目集时很有用。例如,一个 RSS 提要系统需要定期刷新本地副本。

另一个经常与 Redis 一起使用的流行索引是 **带上限的列表**,其中项目使用 LPUSH 添加,并使用 LTRIM 修剪,以创建一个只包含遇到的最新 N 个项目的视图,它们以相同的顺序显示。

索引不一致

保持索引更新可能很困难,在几个月或几年中,由于软件错误、网络分区或其他事件,可能会出现不一致。

可以使用不同的策略。如果索引数据在 Redis 之外,**读取修复** 可以是一个解决方案,其中数据在请求时以延迟的方式修复。当我们索引存储在 Redis 本身中的数据时,可以使用 SCAN 命令族来逐步验证、更新或从头开始重建索引。

RATE THIS PAGE
Back to top ↑