当搜索谷歌或在亚马逊购物时,当您键入搜索框时,1或搜索词的更多匹配项会呈现在你面前。这个特性被称为自动补全、提前输入、按需搜索或增量搜索。如图13-1所示当输入“dinner”时,显示自动完成结果列表的谷歌搜索示例进入搜索框。搜索补全是许多产品的一个重要功能。这导致我们给面试的问题:设计一个搜索自动补全系统,也叫“design top k”或者“设计前k个最受搜索的查询”。
解决任何系统设计面试问题的第一步是问足够多的问题澄清要求。下面是应聘者和面试官互动的例子:
候选人:匹配只支持在搜索查询的开头还是在也是中等吗?
面试官:只在搜索查询的开始。
候选人:系统应该返回多少个自动补全建议?
面试官:5
候选人:系统怎么知道返回哪5个建议?
采访者:这是由流行度决定的,由历史查询频率决定的。
候选人:系统支持拼写检查吗?
面试官:不支持拼写检查或自动更正。
候选人:搜索查询是用英语写的吗?
面试官:是的。如果最后时间允许,我们可以讨论多语言支持。
候选人:我们允许大写和特殊字符吗?
面试官:不,我们假设所有搜索查询都是小写字母字符。
候选人:有多少用户使用这个产品?
采访者:1000万DAU。
需求
以下是需求摘要:
后面的包络估计
在高层上,系统可以分为两部分。
数据收集服务:收集用户输入的查询并实时聚合。
查询服务
假设我们有一个如表13-1所示的频率表。它有两个字段。
在高层设计中,我们讨论了数据收集服务和查询服务。高级别的设计不是最优的,但它可以作为一个很好的起点。在本节中,我们将深入探讨深入了解一些组件并探索优化方法,如下所示:Trie数据结构数据收集服务;
Trie数据结构
高层设计使用关系数据库进行存储。然而,取顶部5从关系数据库中搜索查询是低效的。数据结构trie (prefix tree,前缀树)是用来克服这个问题。由于trie数据结构对系统至关重要,我们将投入大量时间设计定制的trie。请注意其中的一些想法来自文章[2]和[3]。
理解基本的trie数据结构对于这个面试问题是至关重要的。然而,这更像是一个数据结构问题,而不是系统设计问题。此外,许多网上材料解释了这个概念。在本章中,我们将只讨论trie的概述重点研究了如何优化基本的trie数据结构以提高响应时间。
Trie(发音为“try”)是一种类似树的数据结构,可以紧凑地存储字符串。的Name来自word retrieval,这表明它是为字符串检索而设计的操作。trie的主要思想包括以下几个部分:
获取前k个被搜索次数最多的查询的步骤如下:
该算法的时间复杂度是上述每一步花费的时间之和:
O§ + O? + O(clogc)
上面的算法很简单。但是,它太慢了,因为我们需要遍历整个trie树在最坏情况下得到前k个结果。下面是两种优化方法:
让我们逐一来看这些优化。
限制前缀的最大长度
用户很少在搜索框中输入冗长的查询。因此,可以肯定地说p很小整数,比如50。如果我们限制前缀的长度,“查找”的时间复杂度将会降低前缀’ '可由O§缩减为O(小常数),即O(1)。
在每个节点缓存top
搜索查询为了避免遍历整个trie树,我们将前k个最常用的查询存储在每个节点上。由于5 ~ 10条自动补全建议对用户来说已经足够了,所以k是一个相对较小的数字。在我们的特定情况下,只有前5个搜索查询被缓存。
通过在每个节点缓存top搜索查询,显著降低了时间复杂度检索前5个查询。然而,这种设计需要大量的空间来存储顶级查询每一个节点。用空间换取时间是非常值得的,因为快速响应时间非常重要。更新后的trie数据结构
如图13-8所示。前5个查询存储在每个节点上。为例如,前缀为“be”的节点存储如下信息:[best: 35, bet: 29, bee: 20, be: 15,啤酒:10)。
在应用了这两种优化之后,让我们重新审视一下算法的时间复杂度:
由于每一步的时间复杂度降低到O(1),因此算法只需执行一次O(1)来获取前k个查询。
数据收集服务
在我们之前的设计中,每当用户输入搜索查询时,数据都会实时更新。这种方法是不实际的,原因如下。
为了设计一个可扩展的数据收集服务,我们要研究数据的来源和方式使用数据。像Twitter这样的实时应用程序需要最新的自动补全建议。然而,对于许多谷歌关键字的自动补全建议可能不会有太大的变化每天。
尽管用例存在差异,但数据收集服务的底层基础保持不变,因为用于构建trie树的数据通常来自分析或日志记录服务。
图13-9展示了重新设计的数据收集服务。每个组件都被检查一个由一个。
分析日志。它存储有关搜索查询的原始数据。日志只支持追加,不支持索引。日志文件示例如表13-3所示。
聚合器。分析日志的大小通常非常大,数据不正确格式。我们需要聚合数据,以便系统可以轻松地处理它。
根据不同的用例,我们可能会以不同的方式聚合数据。对于实时应用例如Twitter,我们在较短的时间间隔内聚合数据,因为实时结果很重要。另一方面,不那么频繁地聚合数据,比如一周一次,可能更好对于许多用例来说已经足够了。在面试过程中,验证实时结果是否有效重要的。我们假设trie是每周重建一次的。
聚合数据。
表13-4展示了一个每周汇总数据的例子。“time”字段表示开始一周的时间。“frequency”字段是对应查询出现次数的总和在那个星期。
工人。worker是一组定期执行异步任务的服务器。它们构建trie数据结构并将其存储在trie DB中。
单词查找
树缓存。Trie缓存是一种分布式缓存系统,它将Trie树保存在内存中以实现快速读取。它每周对数据库进行快照。单词查找树DB。
Trie DB是持久存储。有两种存储数据的方式。
trie树和hash表的映射关系如图13-10所示。
在图13-10中,左边的每个trie节点都被映射到<key, value>右边是一对。如果如果你不清楚键值存储是如何工作的,请参考第6章。
查询服务
在高层设计中,查询服务直接调用数据库获取前5个结果。图13-11展示了改进后的设计,原来的设计效率很低。
3. 搜索查询被发送到负载均衡器。
4. 负载均衡器将请求路由到API服务器。
5. API服务器从trie缓存中获取trie数据并为其构造自动补全建议客户端。
6. 如果数据不在Trie缓存中,我们将数据补充回缓存。这样,所有对相同前缀的后续请求将从缓存返回。缓存未命中当缓存服务器内存不足或脱机时发生。
查询服务需要闪电般的速度。我们提出以下优化:
n
到z
。按照这个逻辑,我们可以拆分最多26个服务器的查询,因为有26个字母英文字符。我们将基于第一个字符的分片定义为第一层分片。要在26台服务器以上存储数据,可以在第二台甚至第三台服务器上进行分片的水平。例如,以“a”开头的数据查询可以分为4个服务器:“aa-ag”、“ahan”、“ao-au”和“av-az”。
乍一看,这种方法似乎是合理的,直到您意识到还有更多的方法以字母“c”开头的单词,而不是“x”。这就造成了分布不均。为了缓解数据不平衡问题,分析了历史数据的分布模式应用更智能的分片逻辑,如图13-15所示。分片映射管理器维护一个查找数据库,以确定行应该存储在哪里。例如,如果有一个s
和u
、v
、w
、x
、y
和z
的历史查询次数加起来差不多,我们可以维护两个分片:一个用于s
,另一个用于u
到z
。
在你完成深入调查后,面试官可能会问你一些后续问题。
面试官:你如何扩展你的设计以支持多种语言?
为了支持其他非英语查询,我们将Unicode字符存储在trie节点中。如果你是如果你不熟悉Unicode,下面是它的定义:“一种编码标准涵盖所有世界上所有书写系统的汉字,现代和古代的“[5]”。
面试官:如果一个国家的搜索结果和其他国家不一样怎么办?
在这种情况下,我们可能会为不同的国家构建不同的尝试。提高响应速度此时,我们可以将try语句存储在cdn中。
面试官:我们如何支持趋势化(实时)搜索查询?
假设发生了一个新闻事件,搜索查询突然变得流行起来。我们最初
设计将无法工作,因为:
构建实时搜索自动补全功能很复杂,超出了本书的范围所以我们只给出一些想法:通过分片减少工作数据集;
恭喜你走到了这一步!现在给自己一点鼓励吧。好工作!
参考资料
[1]预输入查询的寿命:https://www.facebook.com/notes/facebookengineering/ The - Life -of-a- Typeahead - Query /389105248919/
我们如何构建Prefix:一个可扩展的前缀搜索服务,支持自动完成:
https://medium.com/@prefixyteam how-we-built-prefixy-a-scalable-prefix-search-service for-powering-autocomplete-c20f98e2eff1
[3]前缀哈希树一种分布式哈希表上的索引数据结构:
https://people.eecs.berkeley.edu/~sylvia/papers/pht.pdf
[4] MongoDB wikipedia: https://en.wikipedia.org/wiki/MongoDB
[5] Unicode常见问题:https://www.unicode.org/faq/basic_q.html
[6] Apache hadoop: https://hadoop.apache.org/
[7] Spark streaming: https://spark.apache.org/streaming/
[8] Apache storm: https://storm.apache.org/
[9] Apache kafka: https://kafka.apache.org/documentation/