利用Redis如何实现自动补全功能

 更新时间:2020年4月17日 18:02  点击:2566

忘了redis从哪个版本开启,能够根据输入的部分命令前缀给出提示,即自动补全。接下来笔者介绍基于redis实现这个很酷的功能。

about sorted set

假设结果中有mara,marabel,marcela。现在我们输入mar,就能得到这三个名字,并且输出结果按照字典排序。在实现这个需求之间,我们先简单介绍sorted set。

大家都知道sorted set是按照score排序的:

127.0.0.1:6380> zadd test 85 sida
127.0.0.1:6380> zadd test 80 xiaolang
127.0.0.1:6380> zadd test 60 afei
127.0.0.1:6380> zadd test 90 chenssy
127.0.0.1:6380> zadd test 98 yunaiv
127.0.0.1:6380> zrange test 0 -1
1) "afei"
2) "xiaolang"
3) "sida"
4) "chenssy"
5) "yunaiv"

但是如果score都一样,sorted set是按照什么排序的呢?是按照字典排序的:

127.0.0.1:6380> zadd exam 0 sida
127.0.0.1:6380> zadd exam 0 xiaolang
127.0.0.1:6380> zadd exam 0 chenssy
127.0.0.1:6380> zadd exam 0 yunaiv
127.0.0.1:6380> zadd exam 0 afei
127.0.0.1:6380> zrange exam 0 -1
1) "afei"
2) "chenssy"
3) "sida"
4) "xiaolang"
5) "yunaiv"

这是sorted set一个非常重要的特性,也是我们自动补全需求的一个要点。但是这还不够,离我们的最终需求还有一段路要走。幸运的是sorted set还有另一个很酷的命令:ZRANK。这个命令能知道你要查询的key在sorted set中的位置:

127.0.0.1:6380> zrank exam sida
(integer) 2
127.0.0.1:6380> zrank exam yunaiv
(integer) 4
127.0.0.1:6380> zrange exam 2 4
1) "sida"
2) "xiaolang"
3) "yunaiv"

到这里感觉离我们实现自动补全的第一个版本非常接近了,我们能得到sorted set中按照字典排序后任意一个member及其后面N个member。

简单实现

为了实现最终的自动补全,我们需要付出一些代价:空间。

意思是,对于某个准备添加到sorted set中的member,例如afei,我们不仅要把完整的词(afei)添加到sorted set中,而且还要添加所有可能的前缀(a, af, afe, afei)。这里为了解决某个词是真正的member还是某个member的前缀(例如bar既是一个完整的词,也是某个member例如bark的可能前缀),我们加了一个小把戏,即在真正member的后面增加一个特殊字符,例如"$",那么afei这个member就会添加下列这些词:

a, af, afe, afei, afei$

现在假设我们需要添加三个词:foo, bar, foobar 来进行一些测试, 那么sorted set中就会有如下这些词:

127.0.0.1:6380> zrange autoc 0 -1
 1) "b"
 2) "ba"
 3) "bar"
 4) "bar$"
 5) "f"
 6) "fo"
 7) "foo"
 8) "foo$"
 9) "foob"
10) "fooba"
11) "foobar"
12) "foobar$"

离我们最终的目标又要近了很多。现在假设用户输入"fo",那么为了实现自动补全,我们需要执行如下命令,仔细查看结果,foo$和foobar$就是我们需要的结果,只需要将特殊后缀$去掉即可(其他没有以$结尾的词全部忽略):

127.0.0.1:6380> zrank autoc fo
(integer) 5
127.0.0.1:6380> zrange autoc 5 -1
1) "fo"
2) "foo"
3) "foo$"
4) "foob"
5) "fooba"
6) "foobar"
7) "foobar$"

更多词的测试

网址http://antirez.com/misc/female-names.txt 提供了近5000个女性名字。按照前面说的规则,将所有名字的所有可能前缀全部添加到sorted set中。假定用户输入member,那么只需要通过如下两个命令,得到字典排序后用户输入的member后面的50个词,然后只取$结尾的词:

127.0.0.1:6380> zrank autoc member
(integer) 8
127.0.0.1:6380> zrange autoc 8 50

时间&空间复杂度

根据官方文档可知,zrank和zrange的事件复杂度都是O(log(N))。因此,即使数据量比较大,这种方案也是可行的。

ZRANK key member
起始版本:2.0.0
时间复杂度:O(log(N))

ZRANGE key start stop [WITHSCORES]
起始版本:1.2.0
时间复杂度:O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.

那么需要多少内存呢?

假设在最糟糕的情况下,一个长度为M的词需要添加M+1个词到sorted set中。那么如果有N个词,总计需要添加N*(Ma+1)个词到sorted set中,Ma是这N个词的平均长度。

幸运的是,实际情况远比这个要好得多,以近5000个女性名字为例,只需要添加大约15000个词到sorted set中,因为这些名词存在大量重复的前缀。以3个名字为例:marci,marcia,marcile。如果按照最糟糕的情况,需要添加3*(6+1)=21个词到sorted set中,然而实际情况呢,只需要添加11个词到sorted set中即可:

m, ma , mar, marc, marci, marci$, marcia, marcia$, marcil, marcile, marcile$。

而且,随着样本越来越大,重复的前缀会越大越多。

所以,需要的内存也还OK。是不是觉得美滋滋?哈

查询预测

我们这次的自动补全是按照字典排序,很多时候,这是我们需要的。但是也有一些情况,我们希望按照相似度来排序。例如google搜索那样,搜索结果按照频率和热度等维度进行排序。例如,当用户输入ne,用户应该希望看到Netflix,news,new york times等这些热门关键词。

这样做起来就不那么容易了,如果要能达到根据频率排序,我们需要一个特别的sorted set能以非阻塞的方式实时更新每个前缀的频率:

当用户输入一个例如"foo"这种查询,由于它的所有前缀为:"f", "fo", "foo"。那么对每个前缀都执行命令:ZINCRBY <prefix> 1 foo ;如果用户输入"foobar",那么对每个前缀都执行:ZINCRBY <prefix> 1 foobar;

这种方法还有一个问题,许多自动补全系统只需要展示TOP N个关键词,假设N为10。但是我们这种方法,如果要计算TOP 10,我们需要取得关键词远不止10个,理论上我们要这个前缀作为key的sorted set中所有member都取出来。

幸运的是,从统计学上来讲,每个对于sorted set中有300个member的前缀,就能得到TOP 5关键词。如果查询越频繁,它的得分越高,它最终被选中的概率也就越高。因此,我们要做的就是对搜索字符串的每个前缀:

  • 如果前缀作为key的sorted set中member数量还没有达到300,那么只需要简单的对其zincrby即可;

  • 如果前缀作为key的sorted set中member数量已经达到了300,我们将那些得分比较低的member删除,增加新的member进来,从而达到关键词频率不断迭代的效果。

这个方法一下子可能理解不过来,没关系,举个栗子。假设现在用户输入了next,其前缀n为key的sorted set中已经有netflix(100), news(120), new york(80), near(23), nequ(1)。由于这个前缀对应的sorted set中的member数量还没有300,所以,执行:zincrby n 1 next。其中n是前缀,next是用户输入的关键词。假设现在用户输入了next,其前缀n为key的sorted set中已经有netflix(100), news(120), new york(80), near(23), 省略295个score大于1的关键词, nequ(1)。由于这个前缀对应的sorted set中的member数量达到了300,所以,先删除得分比较低的nequ,再执行:zincrby n 1 next。

这个方法跟用户输入关键词分布有很大的关联性,如果用户输入的所有关键词频率比较接近,那么这个方法得到的数据并不是很可靠。但是我们知道这不是问题,因为搜索就是绝大部分人在搜索那一小部分关键词集合。

清理阶段

由于上面提到的搜索长尾效应,我们可以讲那些得分为1的member清理掉,因为用户对这些关键词几乎没有任何关注度。清理操作还能够减少使用内存,从而让redis保存更多更有用的数据。

知识总结

  • sorted set数据结构中,如果member的score一样,那么按照字典排序。

  • zrank命令能得到指定member在结果中的位置,并可以取排在它后面N个member。

  • zincrby能给指定的sorted set中的member加分。

参考:http://oldblog.antirez.com/post/autocomplete-with-redis.html

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值。

[!--infotagslink--]

相关文章

  • Redis连接池配置及初始化实现

    这篇文章主要介绍了Redis连接池配置及初始化实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-29
  • 详解如何清理redis集群的所有数据

    这篇文章主要介绍了详解如何清理redis集群的所有数据,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-18
  • MyBatis-Plus自动填充功能失效导致的原因及解决

    这篇文章主要介绍了MyBatis-Plus自动填充功能失效导致的原因及解决,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-04
  • 详解redis desktop manager安装及连接方式

    这篇文章主要介绍了redis desktop manager安装及连接方式,本文图文并茂给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-15
  • 浅谈redis key值内存消耗以及性能影响

    这篇文章主要介绍了浅谈redis key值内存消耗以及性能影响,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-07
  • lua读取redis数据的null判断示例代码

    最近在工作中遇到了一个问题,通过查找相关资料才得知原因是因为返回结果的问题,下面这篇文章主要给大家介绍了关于lua读取redis数据的null判断的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下...2020-06-30
  • SpringBoot集成Redis实现消息队列的方法

    这篇文章主要介绍了SpringBoot集成Redis实现消息队列的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-10
  • redis setIfAbsent和setnx的区别与使用说明

    这篇文章主要介绍了redis setIfAbsent和setnx的区别与使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-08-04
  • Redis的Expire与Setex区别说明

    这篇文章主要介绍了Redis的Expire与Setex区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-15
  • C#实现延时并自动关闭MessageBox的方法

    这篇文章主要介绍了C#实现延时并自动关闭MessageBox的方法,非常实用的功能,需要的朋友可以参考下...2020-06-25
  • 查看Redis内存信息的命令

    Redis 是一个开源、高性能的Key-Value数据库,被广泛应用在服务器各种场景中。本文介绍几个查看Redis内存信息的命令,包括常用的info memory、info keyspace、bigkeys等。...2021-01-15
  • Redis的持久化方案详解

    在本篇文章里小编给大家整理的是关于Redis的持久化方案详解,有兴趣的朋友们可以参考下。...2021-01-15
  • @CacheEvict + redis实现批量删除缓存

    这篇文章主要介绍了@CacheEvict + redis实现批量删除缓存方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-12
  • JAVA中 redisTemplate 和 jedis的配合使用操作

    这篇文章主要介绍了JAVA中 redisTemplate 和 jedis的配合使用操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-13
  • redis 交集、并集、差集的具体使用

    这篇文章主要介绍了redis 交集、并集、差集的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-23
  • SpringMvc自动装箱及GET请求参数原理解析

    这篇文章主要介绍了SpringMvc自动装箱及GET请求参数原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-09-19
  • 解决redisTemplate中leftPushAll隐性bug的问题

    这篇文章主要介绍了解决redisTemplate中leftPushAll隐性bug的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-13
  • Redis集群水平扩展、集群中添加以及删除节点的操作

    这篇文章主要介绍了Redis集群水平扩展、集群中添加以及删除节点的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-25
  • 解决Redis开启远程访问及密码问题

    这篇文章主要介绍了Redis开启远程访问及密码的教程,文中给大家提到了Redis启动报错解决方法,需要的朋友可以参考下...2021-01-15
  • 深入理解redis中multi与pipeline

    pipeline 只是把多个redis指令一起发出去,redis并没有保证这些指定的执行是原子的;multi相当于一个redis的transaction的,保证整个操作的原子性,避免由于中途出错而导致最后产生的数据不一致。本文详细的介绍,感兴趣的可以了解一下...2021-06-02