前言
对于跳表,我想大家都不陌生吧,这里不多解释,感兴趣的小伙伴可以看我的这篇文章:http://www.cnblogs.com/haolujun/archive/2012/12/24/2830683.html。 这段时间在做我们拍搜的优化,今天我就讲讲我是如何用跳表优化检索系统的。
搜索引擎的夹角余弦计算
都知道,搜索引擎利用夹角余弦计算query与文档的相似度,感兴趣的小伙伴可以看我的这篇文章:http://www.cnblogs.com/haolujun/archive/2013/01/08/2847503.html, 这里面需要计算两个向量的余弦值。
假设查询向量为:$ Q = [q_{1},q_{2},......,q_{n}] $
假设文档向量为:$ D = [d_{1},d_{2},......,d_{n}] $
query与文档的相似度为:$ sim(Q,D) = \frac{QD}{|Q||D|} $ 。这里面需要Q和D模长相乘做分母,对应分量相乘之和做分子。
模长的计算
对于query可以在每次查询之前做统一的预处理,在预处理过程中计算模长;对于文档,不能每次查询都计算一遍模长,这样效率很低,可以事先在建立索引的时候计算模长并保存。
向量相乘
在搜索引擎检索过程中,首先需要对query进行分词并得到查询向量,之后我们用分出的词从倒排表中拉取文档,不熟悉倒排表的小伙伴可以看这篇文章:http://www.cnblogs.com/haolujun/archive/2013/01/06/2847510.html。 通过倒排拉取出来的只是文档的ID,而文档本身包含的内容其实是以正排的方式存储的:即key=文档ID,$value=(word_{1}, word_{2}, ....word_{n}) $,实际上为了节省存储空间,正排只存储该文档中出现过的词。而今天的向量相乘就是利用正排与query进行两个集合的求交计算(向量相乘只对同时出现在query以及文档中的词进行计算,其它的都计为0),那么这就引出一个新问题,如何求两个集合的交集呢?聪明的小伙伴肯定想到了解决办法:对Q和D分别按照字典序排序,之后求两个有序列表的交集可以在线性时间内完成,示例代码如下:
int i = 0, j = 0;
double sum = 0.0;
while(i < len_q && j < len_doc) {
if(q[i] < d[j]) {
i++;
} else if(q[i] > d[j]) {
j++;
} else {
sum += sim(q[i], d[j]);
}
}
但是,还能再优化这个代码么?答案是肯定的,那就是利用跳表。
对于搜索引擎来说,通常query较短,而文档较长,我们估计排完序的文档序列中会有一大段一大段的词都不在query中,可以直接跳过这些段而不用一一遍历,这就启发我们可以用跳表进行加速,优化代码如下:
double sum = 0.0;
int step = ceil(sqrt(len_doc)), i = 0, j = 0;
while(i < len_q && j < len_doc) {
if(q[i] < d[j]) {
int k = (j - step + 1) < 0 ? 0 : j - step + 1;
while(k <= j && d[k] < q[i]) k++;
if(d[k] == q[i]) {
sum += sim(i, j); j = k + 1; i++;
} else {
j = k; i++;
}
} else if(q[i] > d[j]) {
j = j + step < len_doc ? j + step : j + 1;
} else {
sum += sim(i, j);
++i;
j = j + step < len_doc ? j + step : j + 1;
}
}
}
我们在这里选择步长为 $ \sqrt[]{len_{doc}} $只是表示一种理论指导,实际中还需要不断测试不同的步长从而找到实际最优解。
当然,优化这个问题并不一定用跳表,如果内存足够大,我们可以为每个文档建立哈希或者map,这样只需用O(1)或者 O(log(len_doc))时间判断文档中是否包含一个词。
总结
利用简单的跳表,加起来不过几十行代码,直接使检索的效率提高一倍,优化了50%的硬件成本,可以高高兴兴领取年终奖回家过大年了。 5年前我就研究了跳表,没想到5年后的今天我竟然用到了它,所以没事多看看感兴趣的技术、研究一下感兴趣的问题对你的将来只有利没有弊。