lucene搜索引擎技术的分析与整理
1. <!--[endif]-->引言<!--[if !supportLists]-->1.1. <!--[endif]-->编写目的介绍开源软件搜索引擎——lucene的各个实现的功能,性能,以及代 <!--[if !supportLists]-->1.2. <!--[endif]-->背景
<!--[if !supportLists]-->1.3. <!--[endif]-->该项目的使用现状
经过多年的发展,Lucene在全文检索领域已经有了很多的成 基于Lucene的全文检索产品和应用Lucene的项目在世界各地已经非常之多, <!--[if !supportLists]-->1. <!--[endif]--> Eclipse:主流Java开发工具,其帮助文档采用Lucene作为检索引擎 <!--[if !supportLists]-->2. <!--[endif]-->Jive:知名论坛系统,其检索功能基于 <!--[if !supportLists]-->3. <!--[endif]-->Ifinder:出自德国的网站检索系统,基于 <!--[if !supportLists]-->4. <!--[endif]-->MIT DSpace Federation:一个文档管理系统(http://www.dspa 国内外采用Lucene作为网站全文检索引擎的也很多, <!--[if !supportLists]-->1. <!--[endif]-->http://www.blog <!--[if !supportLists]-->2. <!--[endif]-->http://www.ioff <!--[if !supportLists]-->3. <!--[endif]-->http://search.s <!--[if !supportLists]-->4. <!--[endif]-->http://www.tami (更多案例,参见http://wiki.apa 在所有这些案例中,开源应用占了
2. <!--[endif]-->功能分析
<!--[if !supportLists]-->2.1. <!--[endif]-->与Oracle数据库对比Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。
<!--[if !supportLists]-->2.2. <!--[endif]--> Lucene全文搜索相对数据库搜索的优点
全文检索 ≠ like "%keyword%" 通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页,上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题。 由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。 所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引: 关键词==>出现关键词的文章编号、出现次数、起始偏移量、结束偏移量,出现频率 检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。 由此可以看出,模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。 可以通过一下表格对比一下数据库的模糊查询:
全文检索和数据库应用最大的不同在于:让最相关的头100条结果满足98%以上用户的需求 <!--[if !supportLists]-->2.3. <!--[endif]-->相对一般的搜索引擎优点大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。 Lucene和其他一些全文检索系统/应用的比较:
<!--[if !supportLists]-->2.4. <!--[endif]-->对索引过程优化索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。 Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。 <!--[if !supportLists]-->2.5. <!--[endif]-->对搜索过程优化lucene支持内存索引:这样的搜索比基于文件的I/O有数量级的速度提升。 Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录内容都取得以后再开始返回给应用结果集的。 所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。 如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。 Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了,过滤过程我们可以通过设置最低的匹配度来进行过滤。这也是和数据库应用需要将搜索的结果全部返回不同之处。 3. <!--[endif]-->Lucene的特性分析<!--[if !supportLists]-->3.1. <!--[endif]-->Lucene核心部分——索引排序Lucene 的索引排序是使用了倒排序原理。 该结构及相应的生成算法如下: <!--[if !supportLists]-->1. <!--[endif]-->由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施 <!--[if !supportLists]-->a. <!--[endif]-->我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理。 <!--[if !supportLists]-->b. <!--[endif]-->文章中的”in”, “once” “too”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义, 这些不代表概念的词可以过滤掉,这个也就是在《Lucene详细分析》中所讲的StopTokens <!--[if !supportLists]-->c. <!--[endif]-->用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。 <!--[if !supportLists]-->d. <!--[endif]-->用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live” <!--[if !supportLists]-->e. <!--[endif]-->文章中的标点符号通常不表示某种概念,也可以过滤掉,在lucene中以上措施由Analyzer类完成,经过上面处理后: 文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou] <!--[if !supportLists]-->2. <!--[endif]-->有了关键词后,我们就可以建立倒排索引了 上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。文章1,2经过倒排后变成
通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:a)字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快);b)关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置。
以live 这行为例我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5, <!--[if !supportLists]-->3.2. <!--[endif]-->Lucene的相关度积分公式
score_d = sum_t(tf_q * idf_t / norm_q * tf_d * idf_t / norm_d_t * boost_t) * coord_q_d 注解: score_d : 该文档d的得分 sum_t : 所有项得分的总和 tf_q : 查询串q中,某个项出项的次数的平方根 tf_d : 文档d中 ,出现某个项的次数的平方根 numDocs : 在这个索引里,找到分数大于0的文档的总数 docFreq_t : 包含项t的文档总数 idf_t : log(numDocs/docFreq+1)+1.0 norm_q : sqrt(sum_t((tf_q*idf_t)^2)) norm_d_t : 在文档d中,与项t相同的域中,所有的项总数的平方根 boost_t : 项t的提升因子,一般为 1.0 coord_q_d : 在文档d中,命中的项数量除以查询q的项总数 <!--[if !supportLists]-->3.3. <!--[endif]-->Lucene的其他特性<!--[if !supportLists]-->3.3.1. <!--[endif]-->Boosting特性luncene对Document和Field提供了一个可以设置的Boosting参数, 这个参数的用处是告诉lucene, 某些记录更重要,在搜索的时候优先考虑他们 比如在搜索的时候你可能觉得几个门户的网页要比垃圾小站更优先考虑 lucene默认的boosting参数是1.0, 如果你觉得这个field重要,你可以把boosting设置为1.5, 1.2....等, 对Document设置boosting相当设定了它的每个Field的基准boosting,到时候实际Field的boosting就是(Document-boosting*Field-boosting)设置了一遍相同的boosting. 似乎在lucene的记分公式里面有boosting参数,不过我估计一般人是不会去研究他的公式的(复杂),而且公式也无法给出最佳值,所以我们所能做的只能是一点一点的改变boosting, 然后在实际检测中观察它对搜索结果起到多大的作用来调整 一般的情况下是没有必要使用boosting的, 因为搞不好你就把搜索给搞乱了, 另外如果是单独对Field来做Bossting, 也可以通过将这个Field提前来起到近似的效果 <!--[if !supportLists]-->3.3.2. <!--[endif]-->Indexing Date
日期是lucene需要特殊考虑的地方之一, 因为我们可能需要对日期进行范围搜索, Field.keyword(string,Date)提供了这样的方法,lucene会把这个日期转换为string, 值得注意的是这里的日期是精确到毫秒的,可能会有不必要的性能损失, 所以我们也可以把日期自行转化为YYYYMMDD这样的形势,就不用精确到具体时间了,通过File.keyword(Stirng,String) 来index, 使用PrefixQuery 的YYYY一样能起到简化版的日期范围搜索(小技巧), lucene提到他不能处理1970年以前的时间,似乎是上一代电脑系统遗留下来的毛病 <!--[if !supportLists]-->3.3.3. <!--[endif]-->Indexing 数字
如果数字只是简单的数据, 比如中国有56个民族. 那么可以简单的把它当字符处理 如果数字还包含数值的意义,比如价格, 我们会有范围搜索的需要(20元到30元之间的商品),那么我们必须做点小技巧, 比如把3,34,100 这三个数字转化为003,034,100 ,因为这样处理以后, 按照字符排序和按照数值排序是一样的,而lucene内部按照字符排序,003->034->100 NOT(100->3->34) <!--[if !supportLists]-->3.3.4. <!--[endif]-->排序
Lucene默认按照相关度(score)排序,为了能支持其他的排序方式,比如日期,我们在add Field的时候,必须保证field被Index且不能被tokenized(分词),并且排序的只能是数字,日期,字符三种类型之一 <!--[if !supportLists]-->3.3.5. <!--[endif]-->Lucene的IndexWriter调整
IndexWriter提供了一些参数可供设置,列表如下
设置每mergeFactor个document写入一个段,比如每10个document写入一个段 设置每mergeFacotr个小段合并到一个大段,比如10个document的时候合并为1小段,以后有10个小段以后合并到一个大段,有10个大段以后再合并,实际的document数目会是mergeFactor的指数 简单的来说mergeFactor 越大,系统会用更多的内存,更少磁盘处理,如果要打批量的作index,那么把mergeFactor设置大没错, mergeFactor 小了以后, index数目也会增多,searhing的效率会降低, 但是mergeFactor增大一点一点,内存消耗会增大很多(指数关系),所以要留意不要"out of memory" <!--[if !supportLists]-->3.3.6. <!--[endif]-->RAMDirectory 和 FSDirectory 转化
RAMDirectory(RAMD)在效率上比FSDirectyr(FSD)高不少, 所以我们可以手动的把RAMD当作FSD的buffer,这样就不用去很费劲的调优FSD那么多参数了,完全可以先用RAM跑好了index, 周期性(或者是别的什么算法)来回写道FSD中。 RAMD完全可以做FSD的buffer。 <!--[if !supportLists]-->3.3.7. <!--[endif]-->为查询优化索引(index)
Indexwriter.optimize()方法可以为查询优化索引(index),之前提到的参数调优是为indexing过程本身优化,而这里是为查询优化,优化主要是减少index文件数,这样让查询的时候少打开文件,优化过程中,lucene会拷贝旧的index再合并,合并完成以后删除旧的index,所以在此期间,磁盘占用增加, IO符合也会增加,在优化完成瞬间,磁盘占用会是优化前的2倍,在optimize过程中可以同时作search。 <!--[if !supportLists]-->3.3.8. <!--[endif]-->并发操作Lucene和locking机制
<!--[if !supportLists]-->v <!--[endif]-->所有只读操作都可以并发 <!--[if !supportLists]-->v <!--[endif]-->在index被修改期间,所有只读操作都可以并发 <!--[if !supportLists]-->v <!--[endif]-->对index修改操作不能并发,一个index只能被一个线程占用 <!--[if !supportLists]-->v <!--[endif]-->index的优化,合并,添加都是修改操作 <!--[if !supportLists]-->v <!--[endif]-->IndexWriter和IndexReader的实例可以被多线程共享,他们内部是实现了同步,所以外面使用不需要同步 <!--[if !supportLists]-->3.3.9. <!--[endif]-->Locing
lucence内部使用文件来locking, 默认的locking文件放在java.io.tmpdir,可以通过-Dorg.apache.lucene.lockDir=xxx指定新的dir,有write.lock commit.lock两个文件,lock文件用来防止并行操作index,如果并行操作, lucene会抛出异常,可以通过设置-DdisableLuceneLocks=true来禁止locking,这样做一般来说很危险,除非你有操作系统或者物理级别的只读保证,比如把index文件刻盘到CDROM上。
4. <!--[endif]-->Lucene文档结构Lucene中最基础的概念是索引(index),文档(document.,域(field)和项(term)。 <!--[if !supportLists]-->4.1. <!--[endif]-->Lucene概念详细介绍<!--[if !supportLists]-->4.1.1. <!--[endif]-->域的类型Lucene中,域的文本可能以逐字的非倒排的方式存储在索引中。而倒排过的域称为被索引过了。域也可能同时被存储和被索引。 <!--[if !supportLists]-->4.1.2. <!--[endif]--> 段(Segment)
Lucene索引可能由多个子索引组成,这些子索引成为段。每一段都是完整独立的索引,能被搜索。索引是这样作成的: <!--[if !supportLists]-->4.1.3. <!--[endif]--> 文档号(document.nbspNumber)内部的来说,Lucene用一个整形(interger)的文档号来指示文档。第一个被加入到索引中的文档就是0号,顺序加入的文档将得到一个由前一个号码递增而来的号码。 注意文档号是可能改变的,所以在Lucene外部存储这些号码时必须小心。特别的,号码的改变的情况如下: <!--[if !supportLists]-->4.1.4. <!--[endif]--> 索引信息索引段维护着以下的信息: <!--[if !supportLists]-->4.1.5. <!--[endif]--> 文件的命名(File Naming)
同属于一个段的文件拥有相同的文件名,不同的扩展名。扩展名由以下讨论的各种文件格式确定。 <!--[if !supportLists]-->4.2. <!--[endif]-->Lucene基本数据类型(Primitive Types)<!--[if !supportLists]-->4.2.1. <!--[endif]-->字节Byte
最基本的数据类型就是字节(byte,8位)。文件就是按字节顺序访问的。其它的一些数据类型也定义为字节的序列,文件的格式具有字节意义上的独立性。 <!--[if !supportLists]-->4.2.2. <!--[endif]-->字符串Chars
Lucene输出UNICODE字符序列,使用标准UTF-8编码。 <!--[if !supportLists]-->4.3. <!--[endif]-->索引包含的文件(Per-Index Files)
<!--[if !supportLists]-->4.3.1. <!--[endif]-->Segments文件
索引中活动的段存储在Segments文件中。每个索引只能含有一个这样的文件,名为"segments".这个文件依次列出每个段的名字和每个段的大小。 <!--[if !supportLists]-->4.3.2. <!--[endif]--> Lock文件 有一些文件用来表示另一个进程在使用索引。 <!--[if !supportLists]-->4.3.3. <!--[endif]--> Deleteable文件名为"deletetable"的文件包含了索引不再使用的文件的名字,这些文件可能并没有被实际的删除。这种情况只存在与Win32平台下,因为Win32下文件仍打开时并不能删除。 <!--[if !supportLists]-->4.3.4. <!--[endif]--> 段包含的文件(Per-Segment Files)剩下的文件是每段中包含的文件,因此由后缀来区分。 <!--[if !supportLists]-->4.3.5. <!--[endif]-->域值存储表(Stored Fields)域值存储表使用两个文件表示: <!--[if !supportLists]-->4.3.6. <!--[endif]--> 项字典(Term Dictionary) 项字典用以下两个文件表示: 项信息按项排序。项信息排序时先按项所属的域的文字顺序排序,然后按照项的字串的文字顺序排序。 FieldNum指明了项属于的域号,而域名存储在.fdt文件中。 DocFreg表示的是含有该项的文档的数量。 FreqDelta指明了项所属TermFreq变量在.frq文件中的位置。详细的说,就是指相对于前一个项的数据的位置偏移量(或者是0,表示文件中第一个项)。 ProxDelta指明了项所属的TermPosition变量在.prx文件中的位置。详细的说,就是指相对于前一个项的数据的位置偏移量(或者是0,表示文件中第一个项)。 2. 项信息索引(.tii文件)。 每个项信息索引文件包含.tis文件中的128个条目,依照条目在.tis文件中的顺序。这样设计是为了一次将索引信息读入内存能,然后使用它来随机的访问.tis文件。 <!--[if !supportLists]-->4.3.7. <!--[endif]-->项频数(Frequencies) .frq文件包含每一项的文档的列表,还有该项在对应文档中出现的频数。 <!--[if !supportLists]-->4.3.8. <!--[endif]--> 项位置(Position).prx文件包含了某文档中某项出现的位置信息的列表。 <!--[if !supportLists]-->4.3.9. <!--[endif]--> 标准化因子(Normalization Factor) .nrm文件包含了每个文档的标准化因子,标准化因子用来以后乘以这个这个域的命中数。 <!--[if !supportLists]-->4.3.10. <!--[endif]--> 被删除的文档(Deleted document).del文件是可选的,只有在某段中存在删除操作后才存在: <!--[if !supportLists]-->4.3.11. <!--[endif]--> 局限性(Limitations)在以上的文件格式中,好几处都有限制项和文档的最大个数为32位数的极限,即接近于40亿。今天看来,这不会造成问题,但是,长远的看,可能造成问题。因此,这些极限应该或者换为UInt64类型的值,或者更好的,换为VInt类型的值(VInt值没有上限)。 有两处地方的代码要求必须是定长的值,他们是: 1. FieldvaluesPosition变量(存储于域索引文件中,.fdx文件)。它已经是一个UInt64型,所以不会有问题。 5. <!--[endif]-->Lucene代码分析
应用情景分析
6. <!--[endif]-->测试的主程序
<!--[if !supportLists]-->1.1. <!--[endif]-->Searcher searcher = new IndexSearcher(directory)
<!--[if !supportLists]-->1.1.1. <!--[endif]-->初始化通过目录,创建一个索引搜索器, 调用类 IndexSearcher ::public IndexSearcher(Directory directory) throws IOException { this(IndexReader.open(directory), true); } 调用 private IndexSearcher(IndexReader r, boolean closeReader) { reader = r; this.closeReader = closeReader; } 调用 private static IndexReader open(final Directory directory, final boolean closeDirectory) throws IOException { synchronized (directory) { // in- & inter-process sync return (IndexReader)new Lock.With( directory.makeLock(IndexWriter.COMMIT_LOCK_NAME), IndexWriter.COMMIT_LOCK_TIMEOUT) { public Object doBody() throws IOException { SegmentInfos infos = new SegmentInfos(); 从目录中读取SegmentInfos infos.read(directory); if (infos.size() == 1) { // index is optimized return new SegmentReader(infos, infos.info(0), closeDirectory); } else { IndexReader[] readers = new IndexReader[infos.size()]; for (int i = 0; i < infos.size(); i++) readers[i] = new SegmentReader(infos.info(i)); return new MultiReader(directory, infos, closeDirectory, readers); } } }.run(); } } 代码到这里,已经读取了文件segments文件,获得段信息,该测试只有一个段,所以执行了return new SegmentReader(infos, infos.info(0), closeDirectory);,记住IndexReader = SegmentReader infos.read(directory): /** 读取输入参数的目录,下的segments文件 * 代码分析: * 1。读取格式,小于0表示该文件有隐含的格式信息,小于-1就表示该格式是未知的,因为最小的格式是-1 * 2。小于0时,再读取版本信息以及段的计数 * 3。大于0,表示segments文件开头部分没有版本信息,只有段的计数 * 4。读取段的数量 * 5。循环读取段信息,然后构建段信息对象,最后把这些对象都加入到段集合中 * 6。大于0时,判断是否文件最后有版本信息,有的话就赋值version,没有的话,version =0 */,该段代码比较简单,读者可以从看src中代码 return new SegmentReader(infos, infos.info(0), closeDirectory); SegmentReader(SegmentInfos sis, SegmentInfo si, boolean closeDir) throws IOException { super(si.dir, sis, closeDir); initialize(si); } super(si.dir, sis, closeDir); IndexReader ::IndexReader(Directory directory, SegmentInfos segmentInfos, boolean closeDirectory) { this.directory = directory; this.segmentInfos = segmentInfos; directoryOwner = true; this.closeDirectory = closeDirectory; stale = false; hasChanges = false; writeLock = null; } SegmentReader ::initialize(si); /** 初始化这个段信息 该段代码是初始化了 * 1。读入域信息,只有域的名字 * 2. 打开保存域、保存域索引的文件 */ private void initialize(SegmentInfo si) throws IOException { segment = si.name; // Use compound file directory for some files, if it exists Directory cfsDir = directory();// 就是保存该段的目录 // CompoundFileReader(组合文件读取器)也是(目录)的子类 if (directory().fileExists(segment + ".cfs")) { cfsReader = new CompoundFileReader(directory(), segment + ".cfs"); cfsDir = cfsReader; } // 1。读入域信息,只有域的名字 fieldInfos = new FieldInfos(cfsDir, segment + ".fnm"); // 这个过程读入所有的域信息了 // 2。打开保存域、保存域索引的文件 fieldsReader = new FieldsReader(cfsDir, segment, fieldInfos); tis = new TermInfosReader(cfsDir, segment, fieldInfos); if (hasDeletions(si)) deletedDocs = new BitVector(directory(), segment + ".del");// 读入删除表 freqStream = cfsDir.openFile(segment + ".frq");// 读入频率文件 proxStream = cfsDir.openFile(segment + ".prx");// 读入位置文件 openNorms(cfsDir);// 读入文件segment.f1,segment.f2……,建立hashtable if (fieldInfos.hasVectors()) { // open term vector files only as needed termVectorsReader = new TermVectorsReader(cfsDir, segment, fieldInfos); } } <!--[if !supportLists]-->1.2. <!--[endif]-->hits = searcher.search(query);
这时,searcher = IndexSearcher,对该代码的跟踪如下: 调用:return search(query, (Filter)null) 调用:return new Hits(this, query, filter); 调用:Hit::Hits(Searcher s, Query q, Filter f) throws IOException { query = q; searcher = s; filter = f; getMoreDocs(50); // retrieve 100 initially } getMoreDocs(int min)调用::TopDocs topDocs = searcher.search(query, filter, n) searcher.search(query, filter, n) 调用Scorer scorer = query.weight(this).scorer(reader); IndexSearcher::public TopDocs search(Query query, Filter filter, final int nDocs) throws IOException { Scorer scorer = query.weight(this).scorer(reader); if (scorer == null) return new TopDocs(0, new ScoreDoc[0]); final BitSet bits = filter != null ? filter.bits(reader) : null; final HitQueue hq = new HitQueue(nDocs); final int[] totalHits = new int[1]; scorer.score(new HitCollector() { private float minScore = public final void collect(int doc, float score) { if (score > (bits==null || bits.get(doc))) { // skip docs not in bits totalHits[0]++; if (hq.size() < nDocs || score >= minScore) { hq.insert(new ScoreDoc(doc, score)); minScore = ((ScoreDoc)hq.top()).score; // maintain minScore } } } }); ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()]; for (int i = hq.size()-1; i >= 0; i--) // put docs in array scoreDocs[i] = (ScoreDoc)hq.pop(); return new TopDocs(totalHits[0], scoreDocs); } <!--[if !supportLists]-->1.2.1. <!--[endif]-->Scorer scorer = query.weight(this).scorer(reader);参数分析:query = PhraseQuery (该参数由主测试程序中的Query query = parser.parse(queries[j]);初始化) this = IndexSearcher(该参数初始化,已经初始化了主要的文件,具体可参考1.1) 由代码
query.weight(this) 创建了PhraseWeight(searcher) Scorer scorer = query.weight(this).scorer(reader) 就相当于PhraseWeight(searcher). .scorer(reader),即调用以下代码:
<!--[if !supportLists]-->ü <!--[endif]-->TermPositions p = reader.termPositions((Term)terms.elementAt(i)); 这时Term文本为查询里的项 public TermPositions termPositions(Term term) throws IOException { TermPositions termPositions = termPositions(); termPositions.seek(term); return termPositions; } termPositions():: SegmentReader ::public final TermPositions termPositions() throws IOException { return new SegmentTermPositions(this); } parent =SegmentReader,即刚才的段读取器 tis = new TermInfosReader(cfsDir, segment, fieldInfos); 即项信息读取器 SegmentTermPositions(this):: SegmentTermPositions ::SegmentTermPositions(SegmentReader p) throws IOException { super(p); this.proxStream = (InputStream)parent.proxStream.clone(); } super(p):: SegmentTermDocs(SegmentReader parent) throws IOException { this.parent = parent; this.freqStream = (InputStream) parent.freqStream.clone(); this.deletedDocs = parent.deletedDocs; this.skipInterval = parent.tis.getSkipInterval(); } termPositions.seek(term); public void seek(Term term) throws IOException { 根据项,从项信息读取器中读取对应的项信息,该方法是线程安全的 TermInfo ti = parent.tis.get(term); seek(ti); } seek(TermInfo ti) SegmentTermDocs的项信息转变为现在读入的项的信息 void seek(TermInfo ti) throws IOException { count = 0; if (ti == null) { df = 0; } else { df = ti.docFreq; doc = 0; skipDoc = 0; skipCount = 0; numSkips = df / skipInterval; freqPointer = ti.freqPointer; proxPointer = ti.proxPointer; skipPointer = freqPointer + ti.skipOffset; freqStream.seek(freqPointer); haveSkipped = false; } } new ExactPhraseScorer(this, tps, getPositions(), getSimilarity(searcher),reader.norms(field)); 调用构造器 ExactPhraseScorer(Weight weight, TermPositions[] tps, int[] positions, Similarity similarity, byte[] norms) throws IOException { super(weight, tps, positions, similarity, norms); 调用超类构造器,获得短语位置的频繁度信息和位置信息,并构造一个优先队列 PhraseScorer(Weight weight, TermPositions[] tps, int[] positions, Similarity similarity, byte[] norms) { super(similarity); this.norms = norms; this.weight = weight; this.value = weight.getValue(); // convert tps to a list // 把 PhrasePositions 放在一个一般的队列里面(以链表形式) for (int i = 0; i < tps.length; i++) { PhrasePositions pp = new PhrasePositions(tps[i], positions[i]); if (last != null) { // add next to end of list last.next = pp; } else first = pp; last = pp; } pq = new PhraseQueue(tps.length); // construct empty pq } 使用该记分器记分,并收集 scorer.score(new HitCollector() public void score(HitCollector hc) throws IOException { while (next()) { hc.collect(doc(), score()); } } hc.collect(doc(), score()); score()调用,value为权值 PhraseScorer::public float score() throws IOException { //System.out.println("scoring " + first.doc); float raw = getSimilarity().tf(freq) * value; // raw score return raw * Similarity.decodeNorm(norms[first.doc]); // normalize } 把各个位置的文档和得分收集 public final void collect(int doc, float score) { if (score > (bits==null || bits.get(doc))) { // skip docs not in bits totalHits[0]++; if (hq.size() < nDocs || score >= minScore) { hq.insert(new ScoreDoc(doc, score)); minScore = ((ScoreDoc)hq.top()).score; // maintain minScore } } } 到这里就出来了查询的文档和分数,并且这些文档和分数经过了指定的排序和过滤
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1454992 [收藏到我的网摘] [发送Trackback] Johnny.Deng发表于 2006年12月23日 07:42:00
|
|