二级索引

在 Redis 中构建二级索引

Redis 并非严格意义上的键值存储,因为值可以是复杂的数据结构。然而,它具有外部的键值外壳:在 API 层面,数据通过键名寻址。可以说,原生状态下,Redis 只提供主键访问。然而,由于 Redis 是一个数据结构服务器,其能力可以用于索引,以便创建不同类型的二级索引,包括复合(多列)索引。

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

  • Hash 和 JSON 文档,使用多种字段类型;与 Redis 查询引擎结合使用。
  • 有序集合,用于按 ID 或其他数值字段创建二级索引。
  • 带有字典范围的有序集合,用于创建更高级的二级索引、复合索引和图遍历索引。
  • 集合,用于创建随机索引。
  • 列表,用于创建简单的可迭代索引和最近 N 项索引。
  • 带有标签的时间序列。

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

Hash 和 JSON 索引

Redis 查询引擎提供了使用各种字段类型对 Hash 和 JSON 键进行索引和查询的能力

  • 文本
  • 标签
  • 数字
  • 地理
  • 矢量
  • 地理形状

使用 FT.CREATE 命令对 Hash 或 JSON 键进行索引后,可以使用 FT.SEARCHFT.AGGREGATE 命令查询所有使用索引中定义前缀的键。

有关创建 Hash 和 JSON 索引的更多信息,请参阅以下页面。

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

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

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

构建此类索引的两个命令是 ZADD 和带有 BYSCORE 参数的 ZRANGE,分别用于添加项和检索指定范围内的项。

例如,可以通过向有序集合添加元素,按年龄对一组人名进行索引。元素将是人名,得分将是年龄。

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"

使用 ZRANGE 命令的 WITHSCORES 选项,还可以获取与返回元素关联的得分。

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

范围可以是包含的或不包含的,有关更多信息,请参阅 ZRANGE 命令文档。

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

使用对象 ID 作为关联值

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

例如,我可能有表示用户的 Redis Hash。每个用户由一个单独的键表示,可以直接通过 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 岁,为了更新表示用户的 Hash 数据以及索引,我们需要执行以下两个命令

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

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

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

使用有序集合创建的索引只能索引单个数值。因此,您可能认为无法使用此类索引对具有多个维度的事物进行索引,但实际上这并非总是如此。如果您能够以线性方式有效地表示多维事物,那么通常可以使用简单的有序集合进行索引。

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

得分的限制

有序集合元素的得分是双精度浮点数。这意味着它们可以表示具有不同误差的不同小数或整数值,因为它们内部使用指数表示法。然而,对于索引目的而言,有趣的是得分总是能够在 -9007199254740992 和 9007199254740992 之间的数字范围内无误差地表示,即 -/+ 2^53

表示更大数字时,您需要一种不同的索引形式,能够以任意精度对数字进行索引,这称为字典索引。

时间序列索引

使用 TS.CREATE 命令创建新的时间序列时,可以为其关联一个或多个 LABELS。每个标签都是一个名称-值对,其中名称和值都是文本。标签作为二级索引,允许您使用各种时间序列命令对时间序列键组执行查询。

有关创建带有标签的时间序列的示例,请参阅时间序列快速入门指南

TS.MGETTS.MRANGETS.MREVRANGE 命令基于指定的标签或使用标签相关的过滤表达式对多个时间序列进行操作。TS.QUERYINDEX 命令返回所有与给定标签相关的过滤表达式匹配的时间序列键。

字典索引

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

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

有一些命令,例如 ZRANGEZLEXCOUNT,能够在所有元素具有相同得分的有序集合中按字典顺序查询和计数范围。

这个 Redis 特性基本上相当于传统的数据库中常用来实现索引的 b-tree 数据结构。正如您所料,正因如此,可以使用此 Redis 数据结构来实现相当精巧的索引。

在我们深入使用字典索引之前,先看看有序集合在这种特殊操作模式下的行为。由于我们需要添加得分相同的元素,我们将始终使用特殊的零得分。

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 数据。

更新字典索引

字典索引中索引的值可能非常复杂,并且难以或缓慢地从我们存储的有关对象的信息中重建。因此,一种简化索引处理的方法(以增加内存使用为代价)是,在表示索引的有序集合旁边,还使用一个 Hash 来映射对象 ID 与当前索引值。

例如,当我们索引时,我们还添加到 Hash 中

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

这并非总是必需,但它简化了更新索引的操作。为了移除我们为对象 ID 90 索引的旧信息,无论对象当前字段的值如何,我们只需通过对象 ID 检索 Hash 值,并在有序集合视图中对其执行 ZREM 操作。

使用 Hexastore 表示和查询图

复合索引的一个很棒之处在于它们很方便用来表示图,使用一种称为 Hexastore 的数据结构。

Hexastore 提供了一种表示对象之间关系的方法,关系由主语谓语宾语构成。对象之间的一个简单关系可以是

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"

通过组合不同的查询,我可以提出一些巧妙的问题。例如:我的所有朋友中,哪些喜欢啤酒,住在巴塞罗那,并且 matteocollina 也认为是朋友? 为了获取这些信息,我首先进行 spo 查询来找出所有我认识的朋友。然后对于获得的每个结果,我执行 spo 查询来检查他们是否喜欢啤酒,移除找不到这种关系的那些。我再次执行以按城市过滤。最后,我执行 ops 查询来找出在我获得的列表中,哪些被 matteocollina 认为是朋友。

务必查看 Matteo Collina 关于 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,而是替换最后四位数字,从而获得以下范围

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) 的搜索框。

因此,我们要做的是检查我们的搜索框较小的那个维度,并检查最接近此数字的二次幂。我们的搜索框范围是 50,100 到 100,300,所以它的宽度是 50,高度是 200。我们取两者中较小的那个,即 50,并检查最接近的二次幂,即 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 数据结构,如 Sets 或 Lists,可以用于构建其他类型的索引。它们非常常用,但也许我们并不总是意识到它们实际上是一种索引形式。

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

类似地,lists 可以用于以固定顺序索引项目。我可以将所有项目添加到 Redis list 中,并使用相同的键名作为源和目标,通过 RPOPLPUSH 命令轮换该 list。当我想以相同的顺序永久地一遍又一遍地处理一组给定项目时,这很有用。想想一个需要定期刷新本地副本的 RSS feed 系统。

Redis 中常用的另一种流行索引是 capped list,其中项目使用 LPUSH 命令添加并使用 LTRIM 命令修剪,以便创建一个只包含最新 N 个遇到项目并以它们被看到的相同顺序排列的视图。

索引不一致性

保持索引更新可能具有挑战性,在数月或数年内,可能会因为软件 bug、网络分区或其他事件而引入不一致性。

可以使用不同的策略。如果索引数据存储在 Redis 外部,读修复 (read repair) 可能是一个解决方案,其中数据在被请求时以懒惰方式进行修复。当我们索引存储在 Redis 自身中的数据时,可以使用 SCAN 系列命令来验证、更新或从头开始增量重建索引。

评价此页面
返回顶部 ↑