Lucene 的索引排序是使用了倒排序原理。 该结构及相应的生成算法如下: 设有两篇文章1和2 文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too. 文章2的内容为:He once lived in Shanghai. 1. 由于lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施 a. 我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间是连在一起的需要特殊的分词处理。 b. 文章中的”in”, “once” “too”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义, 这些不代表概念的词可以过滤掉,这个也就是在《Lucene详细分析》中所讲的StopTokens c. 用户通常希望查“He”时能把含“he”,“HE”的文章也找出来,所以所有单词需要统一大小写。 d. 用户通常希望查“live”时能把含“lives”,“lived”的文章也找出来,所以需要把“lives”,“lived”还原成“live” e. 文章中的标点符号通常不表示某种概念,也可以过滤掉,在lucene中以上措施由Analyzer类完成,经过上面处理后: 文章1的所有关键词为:[tom] [live] [guangzhou]
[live] [guangzhou] 文章2的所有关键词为:[he] [live] [shanghai] 2. 有了关键词后,我们就可以建立倒排索引了 上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。文章1,2经过倒排后变成 <!--[if !supportLineBreakNewLine]--> 关键词 文章号 guangzhou 1 he 2 i 1 live 1,2 shanghai 2 tom 1 通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现次数和出现的位置,通常有两种位置:a)字符位置,即记录该词是文章中第几个字符(优点是关键词亮显时定位快);b)关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组(phase)查询快),lucene中记录的就是这种位置。 加上“出现频率”和“出现位置”信息后,我们的索引结构变为: 关键词 文章号[出现频率] 出现位置 guangzhou 1[2] 3,6 he 2[1] 1 i 1[1] 4 live 1[2],2[1] 2,5,2 shanghai 2[1] 3 tom 1[1] 1 以live 这行为例我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5,2”这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么“2,5”就表示live在文章1中出现的两个位置,文章2中出现了一次,剩下的“2”就表示live是文章2中第 2个关键字。 以上就是lucene索引结构中最核心的部分。我们注意到关键字是按字符顺序排列的(lucene没有使用B树结构),因此lucene可以用二元搜索算法快速定位关键词。 实现时 lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。 Lucene中使用了field的概念,用于表达信息所在位置(如标题中,文章中,url中),在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息(因为每个关键字一定属于一个或多个field)。 为了减小索引文件的大小,Lucene对索引还使用了压缩技术。首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>,例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>。其次大量用到的是对数字的压缩,数字只保存与上一个值的差值(这样可以减小数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节保存),上一文章号是16382,压缩后保存7(只用一个字节)。 下面我们可以通过对该索引的查询来解释一下为什么要建立索引。 假设要查询单词 “live”,lucene先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。 而用普通的顺序匹配算法,不建索引,而是对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。3.2. 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的项总数 3.3. Lucene的其他特性 3.3.1. 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提前来起到近似的效果 3.3.2. Indexing Date 日期是lucene需要特殊考虑的地方之一, 因为我们可能需要对日期进行范围搜索, Field.keyword(string,Date)提供了这样的方法,lucene会把这个日期转换为string, 值得注意的是这里的日期是精确到毫秒的,可能会有不必要的性能损失, 所以我们也可以把日期自行转化为YYYYMMDD这样的形势,就不用精确到具体时间了,通过File.keyword(Stirng,String) 来index, 使用PrefixQuery 的YYYY一样能起到简化版的日期范围搜索(小技巧), lucene提到他不能处理1970年以前的时间,似乎是上一代电脑系统遗留下来的毛病 3.3.3. Indexing 数字 如果数字只是简单的数据, 比如中国有56个民族. 那么可以简单的把它当字符处理 如果数字还包含数值的意义,比如价格, 我们会有范围搜索的需要(20元到30元之间的商品),那么我们必须做点小技巧, 比如把3,34,100 这三个数字转化为003,034,100 ,因为这样处理以后, 按照字符排序和按照数值排序是一样的,而lucene内部按照字符排序,003->034->100 NOT(100->3->34) 3.3.4. 排序 Lucene默认按照相关度(score)排序,为了能支持其他的排序方式,比如日期,我们在add Field的时候,必须保证field被Index且不能被tokenized(分词),并且排序的只能是数字,日期,字符三种类型之一 3.3.5. Lucene的IndexWriter调整 IndexWriter提供了一些参数可供设置,列表如下 属性 默认值 说明 mergeFactor org.apache.lucene.mergeFactor 10 控制index的大小和频率,两个作用 maxMergeDocs org.apache.lucene.maxMergeDocs Integer.MAX_ 限制一个段中的document数目 minMergeDocs org.apache.lucene.minMergeDocs 10 缓存在内存中的document数目,超过他以后会写入到磁盘 maxFieldLength 1000 一个Field中最大Term数目,超过部分忽略,不会index到field中,所以自然也就搜索不到 这些参数的的详细说明比较复杂:mergeFactor有双重作用 设置每mergeFactor个document写入一个段,比如每10个document写入一个段 设置每mergeFacotr个小段合并到一个大段,比如10个document的时候合并为1小段,以后有10个小段以后合并到一个大段,有10个大段以后再合并,实际的document数目会是mergeFactor的指数 简单的来说mergeFactor 越大,系统会用更多的内存,更少磁盘处理,如果要打批量的作index,那么把mergeFactor设置大没错, mergeFactor 小了以后, index数目也会增多,searhing的效率会降低, 但是mergeFactor增大一点一点,内存消耗会增大很多(指数关系),所以要留意不要"out of memory" 把maxMergeDocs设置小,可以强制让达到一定数量的document写为一个段,这样可以抵消部分mergeFactor的作用. minMergeDocs相当于设置一个小的cache,第一个这个数目的document会留在内存里面,不写入磁盘。这些参数同样是没有最佳值的, 必须根据实际情况一点点调整。 maxFieldLength可以在任何时刻设置, 设置后,接下来的index的Field会按照新的length截取,之前已经index的部分不会改变。可以设置为Integer.MAX_ 3.3.6. RAMDirectory 和 FSDirectory 转化 RAMDirectory(RAMD)在效率上比FSDirectyr(FSD)高不少, 所以我们可以手动的把RAMD当作FSD的buffer,这样就不用去很费劲的调优FSD那么多参数了,完全可以先用RAM跑好了index, 周期性(或者是别的什么算法)来回写道FSD中。 RAMD完全可以做FSD的buffer。 3.3.7. 为查询优化索引(index) Indexwriter.optimize()方法可以为查询优化索引(index),之前提到的参数调优是为indexing过程本身优化,而这里是为查询优化,优化主要是减少index文件数,这样让查询的时候少打开文件,优化过程中,lucene会拷贝旧的index再合并,合并完成以后删除旧的index,所以在此期间,磁盘占用增加, IO符合也会增加,在优化完成瞬间,磁盘占用会是优化前的2倍,在optimize过程中可以同时作search。 3.3.8. 并发操作Lucene和locking机制 v 所有只读操作都可以并发 v 在index被修改期间,所有只读操作都可以并发 v 对index修改操作不能并发,一个index只能被一个线程占用 v index的优化,合并,添加都是修改操作 v IndexWriter和IndexReader的实例可以被多线程共享,他们内部是实现了同步,所以外面使用不需要同步 3.3.9. 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上。
|