倒排表数据结构、通配符查询、拼写纠正详解

目录:

  • Dictionary Data Structure  词典数据结构
  • Wild-Card Query  通配符查询
  • Spelling Correction  拼写纠正

搜索引擎里的dictionary data通常存储着这些信息:

  • 索引词(term vocabulary)。
  • 文档频率(document frequency,即这个词在多少个文档里出现)。
  • 指向倒排表的指针(pointers to each postings list )。

那么,他是怎样的一个数据结构呢?

一种非常naive的词典结构就是:

其中,term的类型是char[20],占20bytes,document frequency类型int,占4-8 bytes,pointer指针占4-8 bytes。

这种数组的存储方式会有两个问题:

空间上:  How do we store a dictionary in memory efficiently?

时间上:  How do we quickly look up elements at query time?

当然,比较好的数据结构是:

1,Hashtables。2,Trees。

(一些搜索引擎用的是Hashtables,一些用的是trees。)

其实,具体判断该选择什么样的数据结构,主要从以下三点来衡量:

1,数据是不是持续增长的?

2,访问查找是不是很频繁?

3,词典的规模是不是很大?

具体来看看Hashtables和Trees。

如果用的是Hashtables,那么每一个索引词都会被一个hash函数转换成一个整数。

优点:

查找起来十分迅速,时间是O(1)。

缺点:

如果两个词相差十分的小,不容易发现。比如说 judgment/judgement 。

无法实现前缀查询。即查询所有给点前缀的词。

如果词典是持续增长的,需要时不时的对原来的hashtables重新进行hash计算,这个代价是很大的。

Example:

如果数据结构是树的话。最简单的就是二叉树了。

树可以支持前缀查找。搜索速度略低于哈希表方式,时间是O(logM),其中M是词汇表的大小,即所有词汇的数目。

O(logM)仅仅对平衡树成立。但是使二叉树保持平衡的开销很大。

如何解决保持平衡的开销问题呢?

B-树!!!

首先看B-树的定义是这样子的:B-树是一种平衡的多路查找树。

一棵m 阶的B-树满足下列特性的m 叉树:

  • 树中每个结点至多有m 棵子树;
  • 若根结点不是叶子结点,则至少有两棵子树;
  • 除根结点之外的所有非终端结点至少有[m/2] 棵子树;
  • 所有的非终端结点中包含以下信息数据:(n,A0,K1,A1,K2,…,Kn,An)。其中:Ki(i=1,2,…,n)为关键码,且Ki<Ki+1,Ai 为指向子树根结点的指针(i=0,1,…,n),且指针Ai-1 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn。n为关键码的个数。
  • 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

这样讲起来或许比较枯燥难懂,看这张图就好了:

树的优点就是可以解决前缀查找的问题了。

缺点是速度比哈希慢点,是O(logM)并且要求是平衡的,重新平衡一棵树的代价大。(虽然B-树减轻了这种代价)


Wildcard queries,通配符查询。

比如查询语句 mon*:找出所有以mon开头的单词。如果采用树(或者B-树)结构词典,我们可以很容易的解决,只需要查询范围在mon ≤ w < moo的所有单词就ok了。

但是查询语句 *mon:找出所有以mon结尾的单词就比较困难了。其中一种办法就是我们增加一个额外的B-树来存储所有单词,以从后向前的顺序,然后在这个树上查询范围在nom ≤ w < non的所有单词。

可是如何处理通配符在单词中间的查询呢?

比如query是co*tion的话。我们当然可以分别在B-树查询到co*和*tion的所有单词然后合并这些单词,但是这样开销太大了。

解决办法就是:轮排索引(Permuterm Index),我们把query的通配符转换到结尾处。

设置一个标志$表示单词的结尾。

以hello举例,hello可以被转换成hello$, ello$h, llo$he, lo$hel, o$hell。$代表中hello的结束。现在,查询X等于查询X$,查询X*等于查询X*$,查询*X等于查询X$*,查询X*Y等于查询Y$X*。对于hel*o来说,X等于hel,Y等于o。

既然我们已经把通配符都弄到了单词尾部,现在我们又可以通过B-树像以前那样查询拉。

以上,我们已经完成了对query的转换,那么那些存储的索引的词要怎么处理才能配合这种query查询呢?

我们对索引来建立索引!!

Bigram indexes。就是两两个字母来索引。

举例来说,一个文本是“April is the cruelest month”,分别成Bigram indexes就是“$a,ap,pr,ri,il,l$,$i,is,s$,$t,th,he,e$,$c,cr,ru,ue,el,le,es,st,t$, $m,mo,on,nt,h$”,其中$ 代表着单词边界的符号。

那么如何对索引建立索引??

维护第二个倒排表,倒排表的索引词是Bigram indexes,posting list的值就是与之匹配的dictionary terms。

像这样:

好了!目前为止,我们既对query进行了处理,也对terms进行了处理。

所以现在如果我们要查询 mon*,query会被分解成 $m AND mo AND on ,然后从上图中的倒排表做两个AND可以得到匹配的terms了!!

但是,我们会发现 $m AND mo AND on 也会匹配到单词moon,而moon不符合mon*的格式,这是他的一个缺点。我们必须要过滤掉这些词。

另外,一条查询语句往往相当多的布尔查询,这个开销也挺大的。


Spelling correction,拼写校正。

我们google一下 Alanis Morisett ,得到结果如图:

对了,就是这个提示,您找的是不是:xxxxxx。

在搜索引擎中,需要有一个可以查询到所有正确单词的词典。

给定一个词典和一个query,返回一个和query最接近的words。

怎么用才算是最接近??

  • 编辑距离算法。
  • 带权编辑距离算法。

编辑距离(Edit distance)

给定两个字符串S1和S2,从S1转换到S2的最小步骤就是他们的编辑距离。这些步骤包括,Insert(1步), Delete(1步), Replace(1步),copy(0步)

比如说:

dof到dog的编辑距离是1,cat到act的编辑距离是2,cat到dog的编辑距离是3.

算法导论里关于编辑距离的伪代码如下:

举例子:算cats到fast的编辑距离~

括号里圈出来的表示实际应该填的值,其他的只是用来进行对比,取其中最小的数。

具体到表中的每一格中四个数字的含义就是:

从左边的格子过来代表增加,上边的格子过来代表删除,斜上角的格子过来代表替换(此时两个字符不相等)或复制(此时两个字符不相等)。

编辑距离就是这样子。那么什么是带权编辑距离呢?

比如说,我们打字的时候,m被错打成n的几率会比m错打成p的几率更大,所以我们应该认为m和n的编辑距离小于m到p的编辑距离。因此将m替换为n时计算编辑距离应该比将m替换为p时的编辑距离小。

实现带权编辑距离,我们需要一个额外的权值矩阵。

那么,给定一个查询词,我们是不是得计算这个查询词和所有的索引词之间编辑距离呢?

答案是否定的,因为这样开销很大而且慢。

怎么用减少计算呢??

比如说,如果query是lord:

我们只取lo,or,rd中有重叠两次或以上的term,然后合并这些term,以这个为范围进行编辑距离的计算。

索引

其实在计算机中我们早已接触过跟索引有关的东西,比如数据库里的索引(index),还有硬盘文件系统中其实也有类似的东西,简而言之,索引是一种为了方便找到自己需要的东西而设计出来的条目,你可以通过找索引找到自己想要内容的位置。索引过程是: 关键字->索引->文档。在图书馆内的书分门别类,就是一种按类别来分的索引。当然索引还有很多其他的实现。

  仅仅有索引的概念是不够的。虽然分门别类是一种方法,但是我们在拥有一堆文档的时候必须要有从文档到索引的规范过程,并且索引的结构要满足能够让人(或者计算机)快速找到的方法。面对一条长长的没有处理的索引列表,甚至还没有排好序,你可能要用O(N)的时间去看,头都大了。

  为了满足这个要求,B+树,哈希表可以是比较好的选择,它们的复杂度分别是O(log N) 和 O(1)。

    但是事实上为了满足特殊的要求,有些时候还要设计更加特殊的数据结构,比如后缀树组和trie树用来处理非文本的序列子串搜索。

  在主要的搜索引擎中,还是靠文本搜索,而索引的设计其实并不固定,还要跟搜索策略结合,这些都是搜索引擎的部分。

倒排索引

  倒排索引是索引的子集。在搜索引擎之中,正排索引跟倒排索引其实都有应用。

  正排索引:知道文档d,得到d的关键字的位置序列,实现方式是 文档编号+关键字数组

  倒排索引:知道关键字w,找到包含关键字的文档d1,d2,d3.... 实现方式是:关键字key做键的字典,值是文档编号数组

  无论是哪一种索引,都要用一种能够快速检索的数据结构的来实现,否则它们都会面临大规模甚至超大规模的数据下无法工作的问题。

哈希表

  哈希表,根据键找到值,复杂度为O(1)。它的实现是一组桶,每个桶=头部键+尾部链表。数据结构课程中对哈希表已经讲得很清楚了。它的问题在于空间消耗太大,而且可能会有哈希分配不平衡的问题。

跳表

  参考一下 https://juejin.im/post/587c6cec61ff4b006501e006

  跳表是一种特殊的链表,又称跳跃表,可以达到O(log N)的查询速度。这里的图说明了跳表的元素其实都在底层,但是可以有一些重复的层级为了方便检索。它的问题在于怎样平衡空间和时间效率上。

  

  跳表中用到了概率,它设定某个跳表元素的i副本出现在i+1层的概率为p,根据概率对每个值求和得到元素k的期望出现次数 (∑p^k=1/(1-p), k = 1,2,...)。现实中并不会完全按照概率来进行设计,而是用一个固定的步长来设计多级的并联链表。

结论

  倒排索引和跳表是为了方便检索和加快速度而设计的结构,并且在搜索引擎中为后续的其他操作提供了基础。现实中经常讲到的是倒排索引,以及跟它关联的tfidf。为了实现数据的快速搜索,还需要跟具体的数据结构相结合。

转载自:http://www.cnblogs.com/4everlove/p/3678414.html

原文地址:https://www.cnblogs.com/cy1993/p/10340916.html

时间: 2024-11-07 15:28:43

倒排表数据结构、通配符查询、拼写纠正详解的相关文章

Lucene 4.X 倒排索引原理与实现: (2) 倒排表的格式设计

1. 定长编码 最容易想到的方式就是常用的普通二进制编码,每个数值占用的长度相同,都占用最大的数值所占用的位数,如图所示. 这里有一个文档ID列表,254,507,756,1007,如果按照二进制定长编码,需要按照最大值1007所占用的位数10位进行编码,每个数字都占用10位. 和词典的格式设计中顺序列表方式遇到的问题一样,首先的问题就是空间的浪费,本来254这个数值8位就能表示,非得也用上10位.另外一个问题是随着索引文档的增多,谁也不知道最长需要多少位才够用. 2. 差值(D-gap)编码

倒排表压缩算法

倒排表内存放的都是整型数字,所以对倒排表的压缩其实就是对数字的压缩.而二进制数字都是存储在long(8byte) ,int(4byte),short(2byte)类型里面,这种存储方式最大的弊端就是每个数字不管大小消耗的空间都是等价的,比如int的1和int的100000000都是4个字节,而数字1其实仅仅用到了一个字节,其他3个字节的空间纯属浪费.倒排表压缩的核心思路就是在这些没用到的字节上做文章. 整型压缩分为两种方式,一种是以byte为最小单位压缩,一种是以bit为最小单位压缩. 基于by

邻接表无向图(一)之 C语言详解

本章介绍邻接表无向图.在"图的理论基础"中已经对图进行了理论介绍,这里就不再对图的概念进行重复说明了.和以往一样,本文会先给出C语言的实现:后续再分别给出C++和Java版本的实现.实现的语言虽不同,但是原理如出一辙,选择其中之一进行了解即可.若文章有错误或不足的地方,请不吝指出! 目录 1. 邻接表无向图的介绍 2. 邻接表无向图的代码说明 3. 邻接表无向图的完整源码 转载请注明出处:http://www.cnblogs.com/skywang12345/ 更多内容:数据结构与算法

Linq之查询表达式语法详解

1.闲言碎语 由于项目的需要接触到Linq,刚开始有些不适应,好多概念都很模糊.不过经过一段时间的摸索,慢慢地对Linq有了一个更加深入的了解.在此记录一下备忘.      2.查询表达式语法 执行Linq有两种方式,一种是方法形式eg:names.Contains('K');,另一种就是查询表达式eg:var query=from n in names select n; 下面用代码来详细解释. string[] fruitName = { "Apple", "Pear&q

Html5之高级-2 HTML5表单属性(属性介绍、属性详解)

一.属性介绍 属性介绍 - 有一些输入类型要求使用特定的属性才能显示效果,如前面提到过min,max,step. 其他输入类型需要使用一些属性来改进其他性能,或者决定验证过程的重要性.HTML5 标准中再原来的基础上增加了一些新的属性. - Placeholder 属性 - Nultiple 属性 - Autofocus 属性 - Form 属性 二.属性详解 Placeholder 属性 - Placeholder 属性通常用于search输入类型,也可以用在文本域.它表示一个简单提示.单词或

MySQL行级锁,表级锁,页级锁详解

页级:引擎 BDB. 表级:引擎 MyISAM , 理解为锁住整个表,可以同时读,写不行 行级:引擎 INNODB , 单独的一行记录加锁 表级,直接锁定整张表,在你锁定期间,其它进程无法对该表进行写操作.如果你是写锁,则其它进程则读也不允许 行级,,仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作. 页级,表级锁速度快,但冲突多,行级冲突少,但速度慢.所以取了折衷的页级,一次锁定相邻的一组记录. MySQL 5.1支持对MyISAM和MEMORY表进行表级锁定,对BD

SharePoint Search之(五)Query spelling correction— 查询拼写纠正

?? Query spelling correction 在使用搜索引擎的时候.假设一不小心输入错误,或者对于某个词语记得不太清楚,搜索引擎会自己主动纠正: 这个功能可以缩短用户的时间,很好用.在SharePoint 2013中.通过配置Search dictionary  能够实现类似的功能. SharePoint 2013自己维护了一个拼写词典.叫做默认拼写词典(default spelling dictionaries).这个拼写词典是SharePoint自己维护的,用户不能改动. 假设用

多表连接的三种方式详解 HASH JOIN MERGE JOIN NESTED LOOP

在多表联合查询的时候,如果我们查看它的执行计划,就会发现里面有多表之间的连接方式. 之前打算在sqlplus中用执行计划的,但是格式看起来有点乱,就用Toad 做了3个截图. 从3张图里我们看到了几点信息: 1.       CBO 使用的ALL_ROWS模式 Oracle Optimizer CBO RBO http://blog.csdn.NET/tianlesoftware/archive/2010/08/19/5824886.aspx 2.       表之间的连接用了hash Join

MySQL查询高速缓冲详解

查询高速缓冲概述 查询缓存存储SELECT查询的文本以及发送给客户端的相应结果.如果随后收到一个相同的查询,服务器从查询缓存中重新得到查询结果,而不再需要解析和执行查询.如果你有一个不经常改变的表并且服务器收到该表的大量相同查询,查询缓存在这样的应用环境中十分有用.对于许多Web服务器来说存在这种典型情况,它根据数据库内容生成大量的动态页面. 备注1.查询缓存不返回旧的数据.当表更改后(如INSERT.UPDATE.DELETE.TRUNCATE.ALTER TABLE.DROP TABLE或D