二级索引
提示
来自deepseek解释
原文链接:https://redis.io/docs/latest/develop/clients/patterns/indexes/
Redis 并不完全是一个键值存储,因为值可以是复杂的数据结构。然而它有一个外部的键值外壳:在 API 层面,数据是通过键名来寻址的。可以说,Redis 原生只提供主键访问。但由于 Redis 是一个数据结构服务器,它的能力可用于索引,从而创建各种类型的二级索引,包括复合(多列)索引。
本文档解释了如何利用以下数据结构在 Redis 中创建索引:
- 哈希和 JSON 文档,使用多种字段类型;与 Redis Search 结合使用。
- 有序集合,用于按 ID 或其他数值字段创建二级索引。
- 带字典序范围的有序集合,用于创建更高级的二级索引、复合索引和图遍历索引。
- 集合,用于创建随机索引。
- 列表,用于创建简单的可迭代索引和最近 N 项索引。
- 带标签的时间序列。
使用 Redis 实现和维护索引是一个高级主题,因此大多数需要对数据执行复杂查询的用户应该考虑关系型存储是否更适合。但通常,尤其是在缓存场景中,明确需要将索引数据存储在 Redis 中,以加速那些需要某种形式索引才能执行的常见查询。
哈希与 JSON 索引
Redis Search 提供了使用多种字段类型索引和查询哈希键及 JSON 键的能力:
TEXTTAGNUMERICGEOVECTORGEOSHAPE
一旦使用 FT.CREATE 命令对哈希或 JSON 键建立了索引,所有使用索引中定义的前缀的键都可以通过 FT.SEARCH 和 FT.AGGREGATE 命令进行查询。
有关创建哈希和 JSON 索引的更多信息,请参见以下页面。
使用有序集合的简单数值索引
您可以使用 Redis 的有序集合数据类型创建最简单的二级索引,这种数据结构表示一个元素集合,每个元素通过一个浮点数 score(分值)进行排序。元素按分值从小到大排列。
由于分值是一个双精度浮点数,使用原生有序集合构建的索引仅限于索引字段为给定范围内数字的情况。
构建此类索引的两个命令是 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 命令文档。
注意:使用带有 BYSCORE 和 REV 参数的 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。
当表示更大的数字时,您需要一种不同形式的索引,能够以任意精度索引数字,称为字典序索引。
时间序列索引
当您使用 TS.CREATE 命令创建新的时间序列时,可以为其关联一个或多个 LABELS。每个标签都是一个名称-值对,名称和值都是文本。标签充当二级索引,允许您使用各种时间序列命令对时间序列键组执行查询。
有关使用标签创建时间序列的示例,请参见时间序列快速入门指南。
TS.MGET、TS.MRANGE 和 TS.MREVRANGE 命令基于指定的标签或使用标签相关的过滤表达式对多个时间序列进行操作。TS.QUERYINDEX 命令返回匹配给定标签相关过滤表达式的所有时间序列键。
字典序索引
Redis 有序集合有一个有趣的特性。当元素以相同的分值添加时,它们会按字典序排序,使用 memcmp() 函数将字符串作为二进制数据进行比较。
对于不了解 C 语言或 memcmp 函数的人来说,这意味着具有相同分值的元素通过逐字节比较其原始字节值来排序。如果第一个字节相同,则检查第二个字节,依此类推。如果两个字符串的公共前缀相同,则较长的字符串被认为是较大的,因此 "foobar" 大于 "foo"。
诸如 ZRANGE 和 ZLEXCOUNT 等命令能够以字典序方式查询和计数范围,前提是它们用于所有元素具有相同分值的有序集合。
这个 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"
注意,在范围查询中,我们在标识范围的 min 和 max 元素前加上了特殊字符 [ 和 (。这些前缀是强制性的,它们指定范围的元素是包含还是排除。因此,范围 [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 它。
使用 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 is friend of 的所有人是谁?
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"
或者,antirez 和 matteocollina 之间以第一个为主语、第二个为宾语的所有关系是什么?
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 字典序范围高效地执行查询。
假设我们在空间中有点,代表我们的数据样本,其中 x 和 y 是我们的坐标。两个变量的最大值都是 400。
在下图中,蓝色框代表我们的查询。我们想要所有 x 在 50 到 100 之间且 y 在 100 到 300 之间的点。

为了表示数据以快速执行这类查询,我们首先用 0 填充数字。例如,假设我们要将点 (10,25)(x,y)添加到索引中。由于示例中的最大范围是 400,我们可以填充到三位数,得到:
x = 010
y = 025
现在我们交错数字,取 x 的最左边数字,然后 y 的最左边数字,依此类推,以创建一个单独的数字:
001205
这就是我们的索引,但是为了更轻松地重建原始表示(如果我们需要,以空间为代价),我们还可以添加原始值作为额外的列:
001205:10:25
现在,让我们思考这种表示以及为什么它在范围查询的上下文中很有用。例如,取我们蓝色框的中心,位于 x=75 和 y=200。我们可以像之前那样交错数字来编码这个数字,得到:
027050
如果我们分别将最后两位替换为 00 和 99 会发生什么?我们得到一个字典序上连续的范围:
027000 到 027099
这映射到一个正方形,代表所有 x 在 70 到 79 之间且 y 在 200 到 209 之间的值。为了标识这个特定区域,我们可以写出该区间内的随机点。

因此,上述字典序查询允许我们轻松查询图片中特定正方形内的点。但是这个正方形对于我们要搜索的框来说可能太小,因此需要太多查询。所以我们可以做同样的事情,但不是将最后两位替换为 00 和 99,而是对最后四位进行替换,得到以下范围:
020000 029999
这次的范围代表所有 x 在 0 到 99 之间且 y 在 200 到 299 之间的点。在这个区间内绘制随机点向我们展示了这个更大的区域。

现在我们的区域对于查询来说太大了,而且我们的搜索框仍然没有完全包含在内。我们需要更细的粒度,但我们可以通过以二进制形式表示数字轻松获得。这次,当我们替换数字时,我们得到的不是十倍大的正方形,而是仅两倍大的正方形。
我们的数字以二进制形式表示,假设每个变量只需要 9 位(以便表示最大 400 的值),将是:
x = 75 -> 001001011
y = 200 -> 011001000
因此,通过交错数字,我们在索引中的表示将是:
000111000011001010:75:200
让我们看看在交错表示中,当我们把最后 2、4、6、8... 位替换为 0 和 1 时,我们的范围是什么:
2 位:x 在 74 和 75 之间,y 在 200 和 201 之间(范围=2)
4 位:x 在 72 和 75 之间,y 在 200 和 203 之间(范围=4)
6 位:x 在 72 和 79 之间,y 在 200 和 207 之间(范围=8)
8 位:x 在 64 和 79 之间,y 在 192 和 207 之间(范围=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}"
# 转换为交错形式以进行 ZRANGE 查询。
# 我们假设每个整数需要 9 位,因此最终的交错表示将为 18 位。
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("")
# 现在我们有了范围的起始点,通过将指定位数从 0 替换为 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 维数据。
带负数或浮点数的多维索引
表示负值的最简单方法是使用无符号整数并通过偏移量表示它们,这样当您索引时,在将数字转换为索引表示之前,先加上最小负整数的绝对值。
对于浮点数,最简单的方法可能是将它们乘以 10 的幂(取决于您想要保留的小数位数)来转换为整数。
非范围索引
到目前为止,我们检查了用于按范围或单个项目查询的索引。然而,其他 Redis 数据结构,如集合或列表,也可用于构建其他类型的索引。它们非常常用,但也许我们并不总是意识到它们实际上是一种索引形式。
例如,我可以将对象 ID 索引到集合数据类型中,以便通过 SRANDMEMBER 使用 获取随机元素 操作来检索一组随机对象。集合还可用于检查存在性,当只需要测试给定项目是否存在或是否具有某个布尔属性时。
类似地,列表可用于按固定顺序索引项目。我可以将所有项目添加到 Redis 列表中,并使用 RPOPLPUSH 旋转列表,使用相同的键名作为源和目标。当我想按相同顺序永远重复处理一组项目时,这很有用。想象一个 RSS 提要系统,需要定期刷新本地副本。
另一个常与 Redis 一起使用的流行索引是带帽列表,其中项目通过 LPUSH 添加,并通过 LTRIM 修剪,以创建仅包含最近遇到的 N 个项目、且按看到顺序排列的视图。
索引不一致
保持索引更新可能具有挑战性,在数月或数年的时间里,由于软件错误、网络分区或其他事件,可能会出现不一致。
可以使用不同的策略。如果索引数据在 Redis 外部,读修复 可能是一种解决方案,即在请求时以懒方式修复数据。当我们索引存储在 Redis 本身中的数据时,可以使用 SCAN 系列命令来验证、更新或从头增量重建索引。