该系列文章是《An Introduce to Information Retrieval》Chapter 1 的读书笔记。
IR的概念很广泛,即使从钱包中拿出一张信用卡并输入卡号也是一种形式的信息检索。在学术领域,我们这样定义IR:
信息检索(IR)就是一种从大量数据集合中(通常指存储在计算机中文档)寻找满足信息需求的非结构化(通常指文本)得数据(通常指文档)。
布尔检索模型(Boolean Retrieval)
要点: (1) 倒排/反向索引模型 inverted indexes
(2) 简单的布尔表达式如何处理这些索引
1.1 词—文档的关联矩阵索引 a term-document matrix
(1) Unix/Linux grep- 命令
这个命令或许大家都用过,它是Unix/Linux中用于在指定文件中查找特定的搜索字符串的命令。它的原理是利用正则表达式 在文档集合中进行线性顺序扫描(sort of linear scan)。 这种方式对于现代计算机的运行速度而言,在有限的数据规模下做简单的查询足够应付了。
(2) Web data 的搜索面临的现实问题
▲ 网络在线数据量(web data/online data)巨大,其增长的速度远大于计算机的硬件发展速度。如何快速的检索需要查询的内容? 这一点线性顺序扫描时永远做不到的。
▲ web搜索面临的是广大用户群,其查询表达式的方式灵活多样(并不一定是布尔表达式)。甚至有的时候并没有准确的查询含义。比如查询query: Romans NEAR courtyman。 这里的NEAR到底是指Romans,courtyman这两词需要在文章中同一个句子里出现,还是相隔若干词。如何更好的响应用户的灵活多变的查询方法,提供更加人性化得服务呢?
▲ 检索结果的排序问题也是一个现实问题。用户需要看到的是最满意的答案,那么查询返回的若干文档,到底哪些与用户查询最相关呢?
(3)布尔模型的词—文档关联矩阵索引模型
线性顺序扫面对于web data来说是不可能的。目前,解决高效检索大量非结构化的信息的公认最好手段就是建立索引(indexes) 。下面就是一个简单的索引模型——关联矩阵。
1. 词—文档关联矩阵 如下图,列表示文档,行表示文档中的词。
其中如果Term1出现在Doc1中,则矩阵(1,1)标示为1,否则为0。
2. 建立布尔查询表达式(boolean query)。 Antony and Brutus not Caesar 也就是我们需要找到包含Antony ,Brutus同时不包含Caesar 词语的文档。
3. 使用位运算: Antony and Brutus not Caesar = 110001 & 110100 & (~110111) =000000. 很可惜,一篇都没有。
(4) 关联矩阵模型的缺陷
上面这个简单索引模型并不适合Web data的检索。对于大数据量而言,这个矩阵实在是太大了,不可能全部放进内存。而且更严重的是矩阵太稀疏了。况且对于检索结果的排序问题也是解决不了的。
1.2 倒排索引 inverted index
倒排索引绝对是一个伟大的发现。当前很多搜索引擎或者开发包都使用了这个模型,比如Lucene。
(1) 倒排索引结构: 1. 词语组成的字典结构 ——Dictionary 如下图左侧
2. 文档组成的位置链 —— Postiong 如下图右侧
(2) 创建过程
1. 将每一个文档中的词语与文档ID(唯一标示文档)组成一个Pair,存入index。如左图A
2. 将index中的词语按字典序排序。如中图B
3. 如果相同词语来自同一个文档,则只记录一次。相同词语来自不同文档,则合并成进posting。如右图C
(3) 索引存储方法
很显然,对于倒排索引,我们必须把Dictionary和Posting都存储起来。一般Dictionary可以全部加载进内存中,而Posting存放在磁盘中,当需要查找Posting的时候,再会将某一个词语所指向的Posting加载进内存。
Dictionary in menory
很多时候使用Hash表的形式,也用连续存储的数组结构。
Posting in menory:
1. 单链表( singly linked lists) ,在将新文档插入Posting中的时候付出的代价较少。这一点很适合高频率从网上抓取内容并更新文档。
2. 可变长数组(variable length arrays),节约了指针所需要的额外空间。并且对于拥有内存缓存区的现代计算机而言,连续内存的结构无疑会增加查询的速度。
3. 跳跃表(skip lists),一种很先进的存储结构。除了需要额外耗费一些指针空间之外,查找效率极高。Lucene就是用了这种结构。
1.3 布尔查询表达式的处理
(1) Posting的合并算法(merge algorithm)
假如我们需要在倒排索引上查找这样一个表达式: Brutus AND Calpurnia。很明显我们需要下面几个步骤:
1. 在Dictionary中定位Brutus.
2. 检索Brutus所指向的Posting: 1、2、4、11、31....
3. 在Dictionary中定位Calpurnia.
4. 检索Calpurnia所指向的Posting: 2、31、54....
5. 合并两个Posting.
对于两个有序表的合并算法,可以采用下面的算法: 时间复杂度为O(m+n)
//指针P1,P2分别指向两个Posting链 Posting intersect(p1,p2){ Posting answer; while(p1!=null&&p2!=null){ if(p1==p2){ add(answer, p1->docID); p1=p1->next; p2=p2->next; } if(p1>p2) p2=p2->next; if(p1<p2) p1=p1->next; } }
(2) 布尔表达式的优化
Brutus AND Caesar AND Calpurnia
表达式的AND顺序按照每一个词的文档频率递增进行优化。 比如 Brutus‘s Ducument Frequence(Brutus所在文档的数量,符号表示DF(Brutus))。DF(Brutus)=1000,DF(Caesar)=10000,DF(Calpurnia)=100。那么查询表达式可以优化成: (Calpurnia AND Brutus) AND Caesar。
理由很简单,Calpurnia AND Brutus的时间复杂度(利用上面的合并算法)为O(1100),其合并后的中间结果R=DF(Calpurnia AND Brutus)<DF(Calpurnia)=100。此时将这个中将结果R AND Caesar 的时间复杂度不会超过O(100+10000)。而且最后结果页不会操作DF<DF(Calpurnia AND Brutus)<100。总的时间复杂度为O(1100+10100)=O(11200)
如果使用原表达式,Brutus AND Caesa的时间复杂度为O(11000),且中间结果为R=DF( Brutus AND Caesa )< DF(Brutus)=1000。然后R AND Calpurnia的时间复杂度可能达到O(1000+100)。两次AND操作的总时间复杂度为O(11000+1100)=O(12100)
明显优化之后的时间复杂度会少。如果查询表达式更长,查询词语的DF差距更大,那么优化的效率会更明显 。
根据上面的优化原理,对于更加复杂的查询表达式(madding OR crowd) AND (ignoble OR strife) AND (killed OR slain)。我们一般都会估算OR操作两个词的DF之和的数量,然后进行AND递增排序。
事实上,对于任意的布尔表达式,每一次操作的中间结果(intermediate)越小越好。 这是我们进行优化的原则所在。
(3) 自然语言查询 AND 布尔查询表达式
为什么我们对布尔表达式的操作只将AND,很少说OR或NOT呢?
在搜索引擎实际应用的环境下,用户的查询都是自然语言叙述的,很少直接用布尔表达式(你不能要求所有的用户必须逻辑思维缜密吧)。那么对于用户提交的这些查询,都是纯粹的合并操作。
正是这个原因,现实中很多搜索引擎的应用已经退化成了只有AND的布尔模型了。