搜索引擎入门 -- 心脏

来源:互联网 发布:microsoft fix it 编辑:程序博客网 时间:2024/05/17 07:09

  抛开"评分"(Sore)的那一部分不算,从本质上讲搜索引擎就是一全文检索系统。全文检索的核心就是要如何快速的找到要找的内容,所以,如何组织数据的存储就是关键所以。说了那么一长串话的意思就是,index是搜索引擎的核心。这部分将决定处理查询的速度有多快,要是这么部分慢了,那你查出来的结果再精确也是白搭。Lucene就是一颗很好的心脏,让我们一起来解剖这颗心脏,来看看这颗心脏里都有些什么?

  Lucene项目始于2000年,现在(2010/09)的版本是3.0.2。说白了,Lucene就是全文检索引擎,所以,我们有必要把全文检索介绍一下先。所谓的全文检索就是用户根据自己的需要输入一串文字,检索系统在某种数据库中找到与用户输入的字串相匹配的数据。最傻瓜的做法就是"顺序扫描"(Serial Scanning),就是从一堆文件中一个个的去打开这些文件,然后一个一个字的去比对其中的内容。这样的做法存在三个明显的问题: 速度,只要待查找数据一多,速度一定是人不能接受的;结果数量太多,如果在大量的数据中查找一个比较常用的关键字,那得到的结果也是大量的;正确性,在某些语言中,词和词的匹配不是顺序扫描可以解决的,特别是东方语言,例如中文。

 

  如何解决字词匹配的问题?分词引擎可以解决这个问题。对类似英语的语言分词是比较简单的。把所有标点符号先替换成空格(或视为空格),然后,以空格为标识,把单词一个一个的抓出来就可以了。但中文就麻烦了,如果把一个个的汉字抓出来,那词就没有了。汉语词比字更重要,但是汉语词的组成没有规律,并不是任意两个或多个字就能组成一个词,一个比较长的词里还会包含一个或多个短的词。所以,需要建立一张字典, 分词引擎根据字典来分词。Lucene自身是带英文的分词引擎,因为英文的引擎比较简单。中文的开源分词引擎也有好几个,其中最早的且最有影响力的就是"庖丁"(http://code.google.com/p/paoding/)

下面来举个例子:

原文:

今天真热,是游泳的好日子。

分出来的词可能是这样的:

今天 | 天真 | 热 | 是 | 游泳 | 的 | 好 | 日子 |

这是一个"庖丁"的实际的例子,从这个例子里可以看出来,虽然有分词,但智能的对中文进行分词相当有挑战性。(Google使用的是这家公司提供的分词引擎,www.basistech.com)。分出来的词,并不能都加到索引库中去,例如英文的"the", "a", "this"...,中文的"的","是","了"...。这些字/词太常用了,以至于对这些词进行索引会变的没有意义。所以,这些高频词多数搜索引擎,不会对它们进行索引。很显然,用这些常用字来做关键字是搜不到有效的结果的,因为几乎每份文档里都会包含一堆这些常用字/词。而且,也很少会有人来用这些字来进行查询。但是,万事没有绝对呀,比如"how to",这两个字拆开来常用吧?但,我们经常会以'how to"开头来查一些资料不是?我记得2003年以前,如果用"how to"在google里查的话,它会提示你,你要查的关键字里有"to",所以,查到的结果会很多,它已经把"to"过滤掉了,当然,现在的google已经没这个问题了。所以,仅仅是一个分词,不管是针对哪一种语言的,要做到比较好,都相当有挑战。Lucene在这方面做的不是很好,只默认支持英文,而且也只支持比较基本的分词,智能度并不高。

   

  如果解决快速查找的问题?答案是空间换时间。其实很多问题解决方案的基本方法都是很基本的,搜索引擎提高速度的最基本思路就是以空间换时间。文件经过分词后,就会变成我们不能理解的字/词表。假设,我们有两文件A和B,它们的原文是A: 今天真热,是游泳的好日子。B: 今天天气真热,今天我一定要去海边游泳。那经过分词处理后,将会得到:

A --> |今天|天真|热|游泳|好|日子|

B --> |今天|天气|真|热|今天|我|一定|要|去|海边|游泳| 。

得到这两个张表后(我们暂且反这两张表称为正向表,这张表可能通过文件名来得到它所包含的所有字词),索引器就开始构造反向表,所谓反向表就是可能通过字词来得到文件名。

A的反向表:

今天 --> A [1]

天真 --> A [1]

热    --> A [1]

游泳 --> A [1]

好    --> A [1]

日子 --> A [1]

B的反向表:

今天  --> B [2]

天气  --> B [1]

真     --> B [1]

热     --> B [1]

我     --> B [1]

一定  --> B [1]

要     --> B [1]

去     --> B [1]

海边  --> B [1]

游泳  --> B [1]

方括号"[]"里存的数字表示这个词在这份文件中出现的次数。

索引库里当然不会为每份文件存一张反向表,这样即浪费空间,也不能快速的查。所以,A和B的反向表就会合并到索引库的反向表中。假设索引库里只有A和B,那么,索引库里的反向表就会是这样的。

今天{2} --> A [1]  --> B [2]

天气{1}  --> B [1]

天真{1} --> A [1]

真{1}     --> B [1]

热{2}    --> A [1]  --> B [1]

我{1}      --> B [1]

一定{1}  --> B [1]

要{1}      --> B [1]

去{1}      --> B [1]

海边  --> B [1]

游泳{2} --> A [1]  --> B [1]

好{1}     --> A [1]

日子{1} --> A [1]

花括号"{}"里的数字表示有多少文件包含这个词。s

这样,如果进行简单的查询,那就是一目了然了。如果我们查"天气",那马上可以知道是文件A;查"今天"马上可以知道是文件A和B。但是,人们并不会总是进行简单的查询,也有可能查这样的"天气真热"。如果是查询比较复杂的查询字串,那么查询器会先进行分词,得到 |天气|真|热|,然后,在反向表里查到,B,B,A,B,把结果B,A,显示出来。查询还可以支持查询语言例如, "天气 AND 热 NOT 海边", 查询的结果是 天气 --> B, 热 --> A,B, 海边 --> B,结果就是AB - B,得到A,显示结果A。因为索引库里只有两个文件,所以,你查到的结果总也不会超过两个,这个问题不大。但是,实际的情况,索引库里可能会有成千万上亿份文件,如果只是查询"海边"可能会得到成百上千个结果。(在Google上就能查出将近700万个结果),总之,在一页上,肯定是显示不下了,那如果来对这些结果排序,就是下面要讲的一个问题了。

 

  用评分并排序来解决结果过多的问题。我们这里人讨论Lucene实现的最基本的评分算法。其实评分是一个搜索最核心的部分,各家的算法都不一样,而我们这个文章的目的是入门,所以只讨论最基本的方法。如果我们把评分定义成,查询语句与待查文件的相似度,这样是不是更容易理解。所以,搜索引擎的查询就变成在反向表中找出包含查询字串的文件,从一堆结果文件中,找出与查询字串最相似的文件(或相似度值最高的文件)。第一步,为所有分好的词计算权重。由二方面影响一个词(term)的权重:一个词在一份文件中出现的次数;一个词在多少文件中出现。它们分别被称为Term Frequency和Document Frequency。一个词在一份文件中出现的次数越多,说明对该文件越重要;一个词在越多的文件中出现,这个词越普遍,说明这个词的重要性越低。那我们把查询字串也认为是一个文件(只是比较小),然后,以查询字串为Term Frequency,以索引库为Document Frequency。这样,我们就可以计算查询字串中所有词在查询字串中的权重。在索引库里的所有文件,各文件以自己为Term Frequency,以索引库为Document Frequency。这样我们可以计算出在索引库中的每个文件中的词的权重。这样,我们会得到,一个查询字串的各个词的权重序列,很多索引库中的文件的各个词的权重序列。我们把这些序列看成是一个个的向量,那么,相似度的计算就转换成了计算两个向量间的夹角的数学问题。两个向量间的夹角越小说明越相似。这个,最基本的查询评分的过程就完成了,之后就是根据分值排序。但以上陈述的只是最基本的方法,在实际的实现上会有一些调整,而且复杂的多。其它,这个评分的算法,应该是有一堆的公式,还要再配上几张N维向量的图的。但是,相信需要自己写一个搜索引擎的可能性不是很大,所以,大家只要了解最基本的算法就可以了。如果有兴趣,可以找些资料来看,或者直接查看Lucene的源代码。

  至此,大功告成,我们已经了解Lucene的基本工作原理了,接下來,就是要去Apache的网站上,下一个安装包,让这个心脏跳起来就可以了。下期预告,跳动的心脏。

 

 

下面这张图里,深色的框,就是Lucene实现的部分。

 

 

 

原创粉丝点击