使用redis实现搜索词的自动匹配

  1. 要做什么?

  2. 可选方案有哪些?

  3. 具体如何实现?

要做什么

有这样一个需求,当在小程序中的搜索栏输入一个字或词时,下方自动显示出以这个字或词为前缀的相关词语。也就是要做字词的前缀模糊匹配。

可选方案

有这几种:(1)mysql模糊查询;(2)redis有序集合;(3)sphinx分词索引。考虑到具体场景涉及到的词语大约有几千个,最简单又能保证性能的就是redis方案。

使用redis实现词典排序的原理参考以下引文:

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

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

有一些命令,如ZRANGEBYLEXZLEXCOUNT,能够以字典方式查询和计数范围,假设它们与排序集一起使用,其中所有元素具有相同的分数。

这个Redis功能基本上等同于一个数据结构,它通常用于实现传统数据库的索引。正如您可以猜到的那样,因此可以使用此Redis数据结构来实现非常花哨的索引。b-tree

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

1
2
3
4
>ZADD myindex 0 baaa
>ZADD myindex 0 abbb
>ZADD myindex 0 aaaa
>ZADD myindex 0 bbbb

从排序的集合中获取所有元素会立即显示它们是按字典顺序排列的。

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

现在我们可以使用ZRANGEBYLEX来执行范围查询。

1
2
3
>ZRANGEBYLEX myindex [a (b
>1) "aaaa"
>2) "abbb"

请注意,在范围查询中,我们在 and 元素前面加上了特殊字符和 .此前缀是必需的,它们指定范围的元素是包含还是排除。因此,范围意味着在包含和排除之间给我字典上的所有元素,这些元素都是以 开头的元素。min``max``[``(``[a (b``a``b``a

还有两个特殊字符表示无限负字符串和无限正字符串,它们是 和 。-``+

1
2
3
>ZRANGEBYLEX myindex [b +
>1) "baaa"
>2) "bbbb"

原文链接:https://redis.io/topics/indexes?&_ga=2.247049793.476974759.1637664795-572733517.1637664795#lexicographical-indexes

具体实现

1. 构造索引

试验一下,将ab、ta、香、香蕉、香1、香2、香s、香t、香蕉梨、香1下以及二进制转为十六进制的香、香蕉、香1、香2、香s、香t、香蕉梨、香1下存入有序集合中,如图:

汉字转为十六进制后占用空间为原来的3倍,但是没有了\x字符,和英文、数字形式相同,便于查找。可以看出在score相同的情况下,词语已经按照ASCII顺序排列好了,含有相同前缀的词语(包含中、英文、数字、符号及其组合形成的词语)排列在相邻的位置。至此词语已经存入集合,并按相同前缀的相邻排列完成,后续以索引来指代这个有序集合。

2. 匹配查询

(1)只要找到匹配到的词语序列的起点和终点就可以了。搜索词可以作为查找的起点,终点必须是可能的最大值,6位十六进制是’ffffff’,因此可以以搜索词拼接’ffffff’作为查找的终点。(2)但是起点词和终点词不一定包含在索引中,如果没有则需要插入进去,并且如果是在搜索时插入进去的也要在搜索结束后删除掉,防止污染索引。

补充(索引中加入搜索数量)

在实现前缀匹配的基础上,还想要对匹配到的词按历史搜索量排序,既然索引的分数score已经确定不能修改了,只能想办法修改索引的成员了。设想将统计到的搜索数量缀到索引字符串的末尾,并将词语和搜索量用一个分隔符隔开,这个符号在0~9、a~f之外即可,假设为“|”,匹配到词语后需要靠这个分隔符分离出词语的搜索数。拼接起来后发现影响了成员原来的排列顺序。如:原来有这样3个连续成员:11111a、11111a1、11111c(在有序集合中排列顺序与这里写出的顺序一致),它们3个的搜索量分别是:1 20 5,拼接搜索量后3者排列为:11111a1|20、11111a|1、11111c|5。因此增加后缀后导致词语在有序集合里的排序发生了改变,所以修改分隔符为0|。前面举的例子在有序集合中会呈现为这样的顺序:11111a0|、11111a10|、11111c0|,0是0~9、a~f中的最小值,因此全部词语加此后缀,相对顺序不会改变。这样就能把搜索次数也保存起来,可以在查到匹配词后对匹配词按搜索次数做筛选或者进一步的排序。