1、全文检索我们生活中的数据总体分为两种:结构化数据和非结构化数据。 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。 非结构化数据:指不定长或无固定格式的数据,如邮件,word 文档等。 当然有的地方还会提到第三种,半结构化数据,如 XML,HTML 等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。非结构化数据又一种叫法叫全文数据。按照数据的分类,搜索也分为两种: 对结构化数据的搜索:如对数据库的搜索,用 SQL 语句。再如对元数据的搜索,如利用 windows 搜索对文件名,类型,修改时间进行搜索等。 对非结构化数据的搜索:如利用 windows 的搜
2、索也可以搜索文件内容,Linux 下的 grep 命令,再如用 Google 和百度可以搜索大量内容数据。 对非结构化数据也即对全文数据的搜索主要有两种方法:一种是顺序扫描法(Serial Scanning):所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用 windows 的搜索也可以搜索文件内容,只是相当的慢。如果你有一个 80G 硬盘,如果想在上面找到一个内容包含某字符串的文件,不花他几个小时,怕是做不到。Linux 下的 grep 命令
3、也是这一种方式。大家可能觉得这种方法比较原始,但对于小数据量的文件,这种方法还是最直接,最方便的。但是对于大量的文件,这种方法就很慢了。有人可能会说,对非结构化数据顺序扫描很慢,对结构化数据的搜索却相对较快(由于结构化数据有一定的结构可以采取一定的搜索算法加快速度),那么把我们的非结构化数据想办法弄得有一定结构不就行了吗?这种想法很天然,却构成了全文检索的基本思路,也即将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。这种说法比较抽象,举几个例子就很容
4、易明白,比如字典,字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据也即对字的解释。这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。下面这幅图描述了全文检索的一般过程。全文检索大体分两个过程,索引创建(Inde
5、xing)和搜索索引(Search)。 索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。 搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。 于是全文检索就存在三个重要问题:1. 索引里面究竟存些什么?(Index)2. 如何创建索引?(Indexing)3. 如何对索引进行搜索?(Search)下面我们顺序对每个个问题进行研究。二、索引里面究竟存些什么索引里面究竟需要存些什么呢?首先我们来看为什么顺序扫描的速度慢:其实是由于我们想要搜索的信息和非结构化数据中所存储的信息不一致造成的。非结构化数据中所存储的信息是每个文件包含哪些字符串,也即已知文件
6、,欲求字符串相对容易,也即是从文件到字符串的映射。而我们想搜索的信息是哪些文件包含此字符串,也即已知字符串,欲求文件,也即从字符串到文件的映射。两者恰恰相反。于是如果索引总能够保存从字符串到文件的映射,则会大大提高搜索速度。由于从字符串到文件的映射是文件到字符串映射的反向过程,于是保存这种信息的索引称为反向索引。反向索引的所保存的信息一般如下:假设我的文档集合里面有 100 篇文档,为了方便表示,我们为文档编号从 1 到100,得到下面的结构左边保存的是一系列字符串,称为词典。每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。有了索引
7、,便使保存的信息和要搜索的信息一致,可以大大加快搜索的速度。比如说,我们要寻找既包含字符串“lucene”又包含字符串“solr”的文档,我们只需要以下几步:1. 取出包含字符串“lucene”的文档链表。2. 取出包含字符串“solr”的文档链表。3. 通过合并链表,找出既包含“lucene”又包含“solr”的文件。看到这个地方,有人可能会说,全文检索的确加快了搜索的速度,但是多了索引的过程,两者加起来不一定比顺序扫描快多少。的确,加上索引的过程,全文检索不一定比顺序扫描快,尤其是在数据量小的时候更是如此。而对一个很大量的数据创建索引也是一个很慢的过程。然而两者还是有区别的,顺序扫描是每次
8、都要扫描,而创建索引的过程仅仅需要一次,以后便是一劳永逸的了,每次搜索,创建索引的过程不必经过,仅仅搜索创建好的索引就可以了。这也是全文搜索相对于顺序扫描的优势之一:一次索引,多次使用。三、如何创建索引全文检索的索引创建过程一般有以下几步:第一步:一些要索引的原文档(Document)。为了方便说明索引创建过程,这里特意用两个文件为例:文件一:Students should be allowed to go out with their friends, but not allowed to drink beer.文件二:My friend Jerry went to school to se
9、e his students but found them drunk which is not allowed.第二步:将原文档传给分次组件(Tokenizer)。分词组件(Tokenizer)会做以下几件事情(此过程称为 Tokenize):1. 将文档分成一个一个单独的单词。2. 去除标点符号。3. 去除停词(Stop word)。所谓停词(Stop word)就是一种语言中最普通的一些单词,由于没有特别的意义,因而大多数情况下不能成为搜索的关键词,因而创建索引时,这种词会被去掉而减少索引的大小。英语中挺词(Stop word)如:“the”,“a”,“this”等。对于每一种语言的分词
10、组件(Tokenizer),都有一个停词(stop word)集合。经过分词(Tokenizer)后得到的结果称为词元(Token)。在我们的例子中,便得到以下词元(Token):“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,“beer”,“My”,“friend”,“Jerry”,“went”,“school”,“see”,“his”,“students”,“found”,“them”,“drunk”,“allowed”。第三步:将得到的词元(Token)传给语言处理组件(Linguistic Processor)
11、。语言处理组件(linguistic processor)主要是对得到的词元(Token)做一些同语言相关的处理。对于英语,语言处理组件(Linguistic Processor)一般做以下几点:1. 变为小写(Lowercase)。2. 将单词缩减为词根形式,如“cars”到“car”等。这种操作称为:stemming。3. 将单词转变为词根形式,如“drove”到“drive”等。这种操作称为:lemmatization。Stemming 和 lemmatization 的异同: 相同之处:Stemming 和 lemmatization 都要使词汇成为词根形式。 两者的方式不同: o S
12、temming 采用的是“缩减”的方式:“cars”到“car”,“driving”到“drive”。 o Lemmatization 采用的是“转变”的方式:“drove”到“drove”,“driving”到“drive”。 两者的算法不同: o Stemming 主要是采取某种固定的算法来做这种缩减,如去除“s”,去除“ing”加“e”,将“ational”变为“ate”,将“tional”变为“tion”。 o Lemmatization 主要是采用保存某种字典的方式做这种转变。比如字典中有“driving”到“drive”,“drove”到“drive”,“am, is, are”到
13、“be”的映射,做转变时,只要查字典就可以了。 Stemming 和 lemmatization 不是互斥关系,是有交集的,有的词利用这两种方式都能达到相同的转换。 语言处理组件(linguistic processor)的结果称为词(Term)。在我们的例子中,经过语言处理,得到的词(Term)如下:“student”,“allow”,“go”,“their”,“friend”,“allow”,“drink”,“beer”,“my”,“friend”,“jerry”,“go”,“school”,“see”,“his”,“student”,“find”,“them”,“drink”,“allo
14、w”。也正是因为有语言处理的步骤,才能使搜索 drove,而 drive 也能被搜索出来。第四步:将得到的词(Term)传给索引组件(Indexer)。索引组件(Indexer)主要做以下几件事情:1. 利用得到的词(Term)创建一个字典。在我们的例子中字典如下:Term Document IDstudent 1allow 1go 1their 1friend 1allow 1drink 1beer 1my 2friend 2jerry 2go 2school 2see 2his 2student 2find 2them 2drink 2allow 22. 对字典按字母顺序进行排序。 Ter
15、m Document IDallow 1allow 1allow 2beer 1drink 1drink 2find 2friend 1friend 2go 1go 2his 2jerry 2my 2school 2see 2student 1student 2their 1them 23. 合并相同的词(Term)成为文档倒排(Posting List)链表。在此表中,有几个定义: Document Frequency 即文档频次,表示总共有多少文件包含此词(Term)。 Frequency 即词频率,表示此文件中包含了几个此词(Term)。 所以对词(Term) “allow”来讲,总共有
16、两篇文档包含此词(Term),从而词(Term)后面的文档链表总共有两项,第一项表示包含“allow”的第一篇文档,即 1 号文档,此文档中,“allow”出现了 2 次,第二项表示包含“allow”的第二个文档,是 2 号文档,此文档中,“allow”出现了 1 次。到此为止,索引已经创建好了,我们可以通过它很快的找到我们想要的文档。而且在此过程中,我们惊喜地发现,搜索“drive”,“driving”,“drove”,“driven”也能够被搜到。因为在我们的索引中,“driving”,“drove”,“driven”都会经过语言处理而变成“drive”,在搜索时,如果您输入“drivin
17、g”,输入的查询语句同样经过我们这里的一到三步,从而变为查询“drive”,从而可以搜索到想要的文档。三、如何对索引进行搜索?到这里似乎我们可以宣布“我们找到想要的文档了”。然而事情并没有结束,找到了仅仅是全文检索的一个方面。不是吗?如果仅仅只有一个或十个文档包含我们查询的字符串,我们的确找到了。然而如果结果有一千个,甚至成千上万个呢?那个又是您最想要的文件呢?打开 Google 吧,比如说您想在微软找份工作,于是您输入“Microsoft job”,您却发现总共有 22600000 个结果返回。好大的数字呀,突然发现找不到是一个问题,找到的太多也是一个问题。在如此多的结果中,如何将最相关的放在最前面呢?