如何设计一个简单的倒排索引查询

293 阅读7分钟

什么是倒排索引

倒排索引是业内比较常用的一种的全文索引设计思路,它通过提前对需要存储的内容进行一定的分词处理,生成多个Term(词语),以及统计该Term的一些必要属性,最后生成一份类似字典一样的目录记录,以便后续可以快速根据Term进行查找。

应用案例

假设某供应商平台的商品后台管理系统中,商家们需要通过输入“自行车”关键字搜索出一批相关商品,然后再筛选产地,价位,供货商类别等信息,最终导出一份excel给到客户。

如果是传统的技术实现方案,大家一般都会想到用关系型数据库,例如MySQL将这些数据先提前保存下来,然后执行sql:

select * from t_goods where name like '%自行车%'
and production = '深圳' and price < 2500
and catrgory='自营' limit 1000

这类查询其实正常是可以满足使用场景的,但是如果数据量一旦变多,它就得依靠一些优化策略进行加速了,例如给关键字增加索引,分库分表等等。如果我们有1000张商品信息的表进行数据存储的话,那么sql的查询性能就会急剧下降(需要分发1000条sql到1000张分表去检索)。

但是如果我们换种思路来思考,如果提前存储下“自行车”关键字出现在哪些行,对应的行号id,以及在全库中出现的次数等信息,是不是就会快速很多。这种思路就是倒排索引的核心思想。

动手设计

上边的介绍相信大家已经对倒排索引有了一个简单的认识和了解了,那么我们来画个流程图深入分析下具体实现,看看会有什么坑。

假设我们的自行车相关商品信息有如下几种:

商品id商品名称
19越野山地自行车
22幼儿自行车三轮防跌
25高密度橡胶轮带避震自行车
32抓地力强自行车男士
35轻便上班族可折叠自行车
67电动自行车
89后座可载人自行车

此时我们可以尝试按照商品名称关键字进行分词,那么得到的结果就是:

商品名称分词结果
越野,山地自行车[越野,山地,自行车]
幼儿自行车,三轮,防跌[幼儿,自行车,三轮,防跌]
高密度橡胶轮,带避震自行车[高密度,橡胶,避震,自行车]
抓地力强,自行车,男士[抓地力,自行车,男士]
轻便上班族,可折叠自行车[轻便,上班族,折叠,自行车]
电动自行车[电动,自行车]
后座可载人自行车[可载人,自行车]

可以看到,这些商品的名称通过分词计算后,都能得到一个关于自行车关键字的分词。此时我们可以整理下自行车关键字的存储记录:(其他关键字我就不写了)

关键字存储地址
自行车19,22,25,32,35,67,89

存储压缩

上边我们已经将关键字的存储地址给计算了出来,但是如果是有1000w个关键字,难不成1000w个关键字都要保存在内存中吗。

这里我们可以尝试参考操作系统的多级页表设计思路,做个数据的分级存储。例如我们的汉字在国际编码中通用utf-8存储,而按照utf-8的编码格式,一共存储了大约2w个字符,而每个字符其实可以翻译成unicode编码,即数字的格式。

汉字如何转换为对应的数字计算

这里需要了解一些关于unicode的知识,例如我们写入的汉字“啊”,它底层在计算机里面会有一层编码转译,这套转译的规则是国际规定的。例如采用utf-8编码格式的话,“啊”就会被转成 /u554a,这里的/u可以去掉,然后后边的554a其实是一组十六进制的数字,也就是unicode的码数,即21834。

这样就能实现一个汉字和数字之间的转换。基于此,我们的存储可以拆解为如下所示:

图片

通过上图的示例,我们可以很清楚看到,通过利用unicode编码提前创建多级索引,可以加速我们关键字的检索能力。

存储地址是否可以再压缩

看到这里,我们可以再进行深入思考,如果存储地址都是int类型的数字,那么一个数字就会占用了4个字节,如果一个关键字所在的商品id有100个,这里就会占用了400byte的体积。

那么我们是否可以针对这块也进行优化呢?

间隔编码

这里我们是否可以将存储的商品id值进行加工处理成如下所示呢:

原始数据19,22,25,32,35,67,89
压缩后数据19,3,3,7,3,32,22

这里我们将数字从小到大排序,首个数字存储的是最小的商品id值,后续的数字存储的是每个商品id之间的间隔。例如22-19=3,67-35=32等。

这样存储的体积就会比原先的数值要低一些。这种压缩思路,我们称之为“增量编码”。

bit存储

看看我们之前计算好的存储内容:[19,3,3,7,3,32,22] ,可以看到这组数字的值最大只有32,也就是2^5,也就是5和bit的空间。对于小数目,采用bit存储,一下子要比使用传统的int类型存储占用的空间小了许多。

复杂查询

上边我们介绍的都是简单的查询逻辑,下边我们可以做一个稍微复杂一些的查询案例。例如查询关键字 “自行车” + “避震” 的商品。那么这种时候我们的倒排索引需要如何进行关联查询呢?

首先我们利用“自行车”关键字在索引底层检索出对应的存储记录,这块我们暂时称之为termInfo:19,3,3,7,3,32,22。然后利用对间隔编码的反向计算,得出真实的商品id为:19,22,25,32,35,67,89。

接着我们用同样的思路,计算出“避震”关键字的商品id为:9,15,25,982,4135。

于是这里我们就会遇到一个新的问题了,如何计算出以下两个数组:[19,22,25,32,35,67,89] 和 [9,15,25,982,4135]的交集。

由于我们先前采用了bit进行数字的存储,因此这里只需提取出对应的数字进行【与】计算就可以了。从而能够实现我们交集查询的效果。

小结

好了,到这里我们就大概理清楚了倒排索引底层的一些设计思想。其实上边有些方面我是参考了Lucene的设计原理,如果直接去看其底层源码,你会发现Lucenc的作者在性能压缩这块还做了很多有意思的设计。

倒排索引虽然是能够很好地帮助我们从海量数据中快速根据关键字提取信息,但是它也有一定的缺点,也就是不能很好支持一些范围/排序类型的搜索,因此在实际业务场景的技术选型时候,大家需要结合业务特性来进行决定。