为了提高阅读体验,请移步到:搜索引擎关键字智能提升实践
一、背景
搜索关键字智能提示是一个搜索应用的标配,主要作用是避免用户输入错误的搜索词,并将用户引导到相应的关键词上,以提升用户使用体验。
雪球以连接人与资产,让财富的雪球越滚越大为使命,在投资社区领域处于领先地位。为了让用户快速准确找到目标的股票,大V,我们搭建搜索提示系统(以下简称Sug系统)来自动补全 Query。用户在查找股票时主要输入股票名称、股票 Symbol 进行搜索,为了提升用户体验和输入效率,本文实现了一种基于 Trie 树前缀匹配查询 + 模型排序关键词智能提示(Suggestion)实现。
二、需求分析
-
支持前缀匹配原则:在搜索框中输入 “中国”,搜索框下面会以 “中国” 为前缀,展示 “中国平安”、“中国神华”、“中国中免” 等搜索词;输入“贵州”,会提示 “贵州茅台”、“贵州燃气”、“贵州百灵”等搜索词。
-
同时支持汉字、拼音输入:由于中文的特点,如果搜索自动提示可以支持拼音的话会给用户带来更大的方便,免得切换输入法。比如,输入 [zhongguo] 提示的关键字和输入 “中国” 提示的一样,输入 [guizhou] 与输入 “贵州” 提示的关键字一样。
-
支持拼音缩写输入:对于较长关键字,为了提高输入效率,有必要提供拼音缩写输入。比如输入 “zg” 应该能提示出 [zhongguo] 相似的关键字,输入 [gzmt] 也一样能提示出 “贵州茅台” 关键字。
-
支持多音字输入提示:比如输入 [chongqing] 或者 [zhongqing] 可以提示出 “重庆啤酒”、“重庆钢铁”、“重庆百货”。
-
支持大写数字转阿拉伯数字:比如输入 [360] 可以提示出 “三六零” 这只股票。
-
支持 Query 纠错:比如输入 “贵州毛台” 可以提示出 “贵州茅台”。
三、解决方案
什么是 Tire 树
Tire 树,也叫字典树,又称单词查找树或键树,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie 树的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
它有3个基本性质:
-
根节点不包含字符,除根节点外,每一个节点都只包含一个字符。
-
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
-
每个节点的所有子节点包含的字符都不相同。
Trie Tree 是一颗存储多个字符串的树,树的每条分支代表一个字符串。和普通树不同的地方是,相同的字符串前缀共享同一条分支。例如给出一组字符串:中国平安、中国银行、中兴通讯、中信证券、平安银行、平安、zgpn 等。我们可以构建出下面的 Trie 树:

其中,绿色节点是数据节点,黄色节点是普通节点。从根节点到绿色节点的一条路径表示一个字符串(注意:数据节点并不都是叶子节点)。
由上图可知,当用户输入 “中国” 的时候,搜索框会展示以 “中国” 为前缀的 “中国平安”、“中国银行”等关键词;当用户输入 “中兴” 的时候,搜索框里面会提示以 “中兴” 为前缀的“中兴通讯”等关键词。
Trie 树的实现
插入过程
为了更容易理解 Trie 树是怎么构造出来的,我画了一个 Trie 树构造的分解过程。构造过程的每一步,都相当于往 Trie 树中插入一个字符串。当所有字符串都插入完成之后,Trie 树就构造好了。

以下代码是 Trie 树插入的 demo
class TrieNode: def __init__(self, char=None): self.char = char self.data = None self.children = {} def is_data_node(self): return self.data is not None def is_leaf_node(self): return len(self.children) == 0class Trie: def __init__(self): self.root = TrieNode("/") def insert(self, text): if not text: return p = self.root for char in text: if char not in p.children: p.children[char] = TrieNode(char) p = p.children[char] p.data = text
查询过程
精确查询
在 Trie 树中查询 “中国平安”时,需要将字符串分割成单个字符:中、国、平、安,然后从 Trie 树的根节点开始匹配。如图所示,红色路径就是在 Trie 树中的匹配路径。

以下代码是 Trie 树精确查询的 demo
def find_node(self, pattern): if not pattern: return p = self.root for char in pattern: if char not in p.children: return p = p.children[char] return p