二级索引
在Redis中构建二级索引
Redis 并不完全是一个键值存储,因为值可以是复杂的数据结构。然而,它有一个外部的键值外壳:在 API 级别,数据是通过键名来寻址的。可以说,原生地,Redis 只提供主键访问。然而,由于 Redis 是一个数据结构服务器,它的功能可以用于索引,以创建不同类型的二级索引,包括复合(多列)索引。
本文档解释了如何使用以下数据结构在Redis中创建索引:
- 哈希和JSON文档,使用多种字段类型;与Redis查询引擎结合使用。
- 使用有序集合通过ID或其他数字字段创建二级索引。
- 使用字典序范围的排序集合,用于创建更高级的二级索引、复合索引和图遍历索引。
- 用于创建随机索引的集合。
- 用于创建简单可迭代索引和最后N项索引的列表。
- 带有标签的时间序列。
使用Redis实现和维护索引是一个高级主题,因此大多数需要对数据执行复杂查询的用户应该了解他们是否更适合使用关系型存储。然而,特别是在缓存场景中,通常需要将索引数据显式地存储到Redis中,以加速需要某种形式索引才能执行的常见查询。
哈希和JSON索引
Redis查询引擎提供了使用各种字段类型对哈希和JSON键进行索引和查询的能力:
TEXT
TAG
NUMERIC
GEO
VECTOR
GEOSHAPE
一旦使用FT.CREATE
命令对哈希或JSON键进行了索引,所有使用索引中定义的前缀的键都可以通过FT.SEARCH
和FT.AGGREGATE
命令进行查询。
有关创建哈希和JSON索引的更多信息,请参阅以下页面。
使用排序集合的简单数值索引
你可以使用Redis创建的最简单的二级索引是通过使用有序集合数据类型,这是一种表示一组元素的数据结构,这些元素按照一个浮点数排序,这个浮点数就是每个元素的分数。元素按照从最小到最大的分数排序。
由于分数是双精度浮点数,你可以使用普通排序集合构建的索引仅限于索引字段是给定范围内的数字的情况。
构建这类索引的两个命令是ZADD
和ZRANGE
,分别使用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"
通过使用ZRANGE
的WITHSCORES选项,还可以获取与返回元素相关联的分数。
ZCOUNT
命令可用于检索给定范围内的元素数量,而无需实际获取元素,这在某些情况下非常有用,特别是考虑到该操作在对数时间内执行,无论范围的大小如何。
范围可以是包含的或排除的,请参阅ZRANGE
命令文档以获取更多信息。
注意: 使用ZRANGE
与BYSCORE
和REV
参数,可以以相反的顺序查询范围,这在数据按给定方向(升序或降序)索引但我们希望以相反方式检索信息时非常有用。
使用对象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。因此,一旦我使用ZRANGE
和BYSCORE
参数查询索引,我还需要使用HGETALL
或类似的命令来检索我需要的信息。明显的优势是,只要我们不更改索引字段,对象可以在不触及索引的情况下更改。
在接下来的示例中,我们几乎总是使用ID作为与索引关联的值,因为这通常是更合理的设计,只有少数例外。
更新简单的排序集合索引
我们经常索引随时间变化的事物。在上面的例子中,用户的年龄每年都会变化。在这种情况下,使用出生日期作为索引而不是年龄本身是有意义的,但在其他情况下,我们只是希望某些字段不时变化,并且索引反映这种变化。
ZADD
命令使得更新简单索引变得非常容易,因为重新添加一个具有不同分数和相同值的元素将简单地更新分数并将元素移动到正确的位置,所以如果用户 antirez
年满39岁,为了更新表示用户的哈希中的数据以及索引中的数据,我们需要执行以下两个命令:
HSET user:1 age 39
ZADD user.age.index 39 1
该操作可能被包装在一个MULTI
/EXEC
事务中,以确保两个字段都被更新或都不被更新。
将多维数据转换为线性数据
使用有序集合创建的索引只能索引单个数值。因此,你可能会认为使用这种索引无法索引具有多个维度的内容,但实际上这并不总是正确的。如果你能有效地以线性方式表示多维内容,那么通常可以使用简单的有序集合进行索引。
例如,Redis地理索引API使用一种称为地理哈希的技术,通过纬度和经度来索引地点。排序集合的分数代表经度和纬度的交替位,因此我们将排序集合的线性分数映射到地球表面的许多小方块。通过进行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数据结构来实现非常复杂的索引。
在我们深入使用字典序索引之前,让我们先检查一下在这种特殊操作模式下,排序集合的行为。由于我们需要添加具有相同分数的元素,我们将始终使用特殊的零分数。
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"
现在我们可以使用ZRANGE
与BYLEX
参数来执行范围查询。
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
对于每个遇到的搜索查询都是如此。然后,当我们想要完成用户输入时,我们使用ZRANGE
和BYLEX
参数执行一个范围查询。想象一下,用户在搜索表单中输入“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的数据结构。
六元组存储提供了一种表示对象之间关系的方式,由主体、谓词和对象组成。对象之间的一个简单关系可以是:
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"
或者,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 to 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
让我们看看当我们用0和1替换交错表示中的最后2、4、6、8、...位时,我们的范围是什么:
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位开启的相同表示作为范围的结束。
对于找到的每个方块,我们执行查询并获取其中的元素,移除位于搜索框之外的元素。
将其转化为代码很简单。以下是一个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索引到Set数据类型中,以便通过SRANDMEMBER
使用获取随机元素操作来检索一组随机对象。当我只需要测试某个给定项目是否存在或是否具有单个布尔属性时,集合也可以用于检查存在性。
同样,列表可以用来将项目索引到一个固定的顺序中。
我可以将所有项目添加到一个Redis列表中,并使用RPOPLPUSH
将列表旋转,使用相同的键名作为源和目标。这在我想要以相同的顺序一次又一次地处理一组给定的项目时非常有用。想象一个需要定期刷新本地副本的RSS订阅系统。
另一个常用于Redis的流行索引是有上限的列表,其中项目通过LPUSH
添加,并通过LTRIM
进行修剪,以便创建一个仅包含最近遇到的N个项目的视图,按照它们被看到的顺序排列。
索引不一致
保持索引更新可能具有挑战性,在几个月或几年的过程中,由于软件错误、网络分区或其他事件,可能会添加不一致性。
可以使用不同的策略。如果索引数据在Redis之外,读取修复可以作为一种解决方案,即在请求数据时以懒散的方式修复数据。当我们索引存储在Redis本身中的数据时,可以使用SCAN
系列命令来验证、更新或从头开始逐步重建索引。