倒排索引结构,示例如下:
设有 2 篇文章:
文章1的内容为:Tom lives in Guangzhou,I live in Guangzhou too.
文章2的内容为:He once lived in Shanghai.
文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有关键词为:[he] [live] [shanghai]
建立倒排索引:
Term |
文章号(出现频率) |
出现位置 |
guangzhou |
1[2] |
3,6 |
he |
2[1] |
2 |
i |
1[1] |
4 |
live |
1[2],2[1] |
2,5,2 |
shanghai |
2[1] |
3 |
tom |
1[1] |
1 |
lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。Lucene中使用了field的概念,用于表达信息所在位置(如标题中,文章中,url中),在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息(因为每个关键字一定属于一个或多个field)。
压缩:
关键词压缩为<前缀长度,后缀>。例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>
数字只保存与上一个值的差值。例如当前文章号是16389,上一文章号是16382,压缩后保存7。
lucene 建立索引的流程:
IndexWriter writer = new IndexWriter(index_dir,new StandardAnalyzer(),false);
第一个参数:生成 index 的目录;
第二个参数:生成索引所用的分析器;
第三个参数: true 为重新索引, false 为增量索引。
Document doc = new Document();
doc.add(new Field(name,value,Field.Store.YES,Field.Index,TOKENIZED));
域的属性:是否 store ,是否 index ,是否切词
writer.add(doc);
writer.Optimize(); // 将所有的 segments 合并为一个
writer.close();
lucene 索引文件结构:
逻辑结构:
每当往 index 中加入一个 doc ,都会新生成一个 segment 保存这个 doc ,通过判断合并 segment 。
最后通过优化索引的命令,将所有的 segment 合并为一个 index 。一般以一个 doc 为单位往 index
添加记录,一个 doc 有几个 field 组成,每个 field 保存不同的信息,可以指定在某个域搜索。
每个域由很多 term 组成。
物理结构:
s egments 文件的结构是:
Format+version+counter+Count+<si0.name+si0.docCount>+<si1.name+si1.docCount>…+<sin.name+sin.docCount>
即:索引文件格式 + index 修改次数 +segment 总数 + 给新生成的 segment 取一个随机的名字
+<segment0 名字 +segment0 文档数 >+<segment1 名字 +segment1 文档数 >
index.lock 文件:
表示有进程在向索引中加入文档,或者是从索引中删除文档。这个文件防止很多文件同时修改一个索引。
即:该 doc 中 field 的总数 +< 域 0 名字 + 域 0 基本信息 >+< 域 1 名字 + 域 1 基本信息 >
基本信息 :低第一位 1 表示已经索引, 0 表示未索引。低第二位 1 表示有 term 矢量, 0 表示没有 term 矢量
用于在域信息文件中定位一个特定文档的域信息。
文档 n 的域信息是第 n*8 开始的 Uint64 。
.fdt 文件的结构是:
storecount+<field0.number+bits0+field0.value>+<field1.number+bits1+field1.value>+…+<fieldn.number+bitsn+fieldn.value>
即 : 记录这个 doc 中需要存储的 filed 总数 +< 域 0 编号 + 域 0 属性(是否分词、是否包括 2 进制、是否压缩) + 记录 filed 的具体内容 >
对每个 field ,都会生成一个 .f 文件,其中记录了每个 doc 在命中该域时乘的因子的大小。
.f 的文件结构是: doc0.filed0.norm+ doc1.field0.norm+…+docn.field0.norm
每个 doc 都会使用哈希表 postingTable 来保存该 doc 所包含的 term 的信息 。 这个哈希表的 key 是 term ,值是 posting , posting 是实际记录一个 term 在这个 doc 中信息的类。
Posting 主要包含以下信息:
term :一个 term 对象,表明这个 posting 是属于哪个 term 的;
freq: 记录这个 term 在 doc 中出现的频率;
positions[]: 记录这个 term 每次出现在 doc 的哪个位置;
offset :保存与 termvector 相关的内容 。
将 这个 postingTable 写入文件 :
.tis 文件的结构: FORMAT+size+indexInterval+skipInterbal+<term0.start+term0.length+term0.text+term0.FieldNumber+term0.DocFreq+term0.FreqpointerDelta+term0.proxPointerDelta+term0.skipOffset>+…+<termn.start+termn.length+termn.text+termn.FieldNumber+termn.DocFreq+termn.FreqpointerDelta+termn.proxPointerDelta+termn.skipOffset >
即:
假设前一个 term 是 bit ,当前 term 是 bike ,则 start=2 , length=2 , text== “ ke ”。
.tis 文件的索引,记录了 .tis 文件中部分 term 及在 .tis 文件中的位置,默认为 term 总数的 1/128
.frq 文件的结构 : term0.freq+term1.freq+…+termn.freq
.prx 文件的结构 : term0.position[0]+(term0.position[1]-term0.position[0])+ (term0.position[2]-term0.position[1])+…+(term0.position[n-1]-term0.position[n-2])+….(termn.position[n-1]-termn.position[n-2])
其中 term.position[n] 表示该 term 在 field 中出现第 n+1 次时的偏移位置。
要删除的文件保存在 deletable 文件中,这个文件记录以下数据:
删除文件所采用的策略是 :当每次合并时尝试删除 deletable 中所标识的所有文件,有些文件可能正在被使用而无法删除,生成一个新的 deletable 文件取代原来的文件,记录新增的要删除的文件和保留那些没能够删除的文件。
判断是否合并的规则:
设置一个 targetMergeDocs ,第一次合并时值为 10 ,接下来依次× 10 ,即第 2 次合并时值为 100 ,第 3 次合并时值为 1000 ,依次类推。每次判断时,读取一个 segment 的 doc 数,这时有 3 种可能。
1. 如果该 segment 的 doc 数大于等于当前的 targetMergeDocs ,则对该 segment 不进行任何操作;
2. 如小于,则把他的 doc 数和后续那些同样 doc 数小于 targetMergeDocs 的 segment 的 doc 数累加,直到累加所得的 doc 数大于或等于 targetMergeDocs 时,把这些参与了累加的 segment 合并成一个新的 segment 。
3. 如果累加到最后一个 segment ,累加的 doc 数还不大于等于 targetMergeDocs 的话,则不进行合并。
循环这个过程,直到再也不能合并时退出 MaybeMergeSegments()
合并举例 :
现在按照顺序,有如下几个 segment ,括号内为该 segment 的 doc 数:
s0(10), s1(10), s2(10), s3(10), s4(10), s5(10), s6(10), s7(10), s8(10), s9(1), s10(1), s11(1), s12(1), s13(1), s14(1), s15(1), s16(1), s17(1), s18(1), s19(1)
第一遍合并后:
s0(10), s1(10), s2(10), s3(10), s4(10), s5(10), s6(10), s7(10), s8(10) news9(10),s19(1)
第二遍合并后:
news0(100),s19(1)
在完成索引最后阶段,有个 Optimize() 函数,这是为了在最后把所有的 segment 全部 合并成最后的一个 segment 文件 ,不论是否满足合并的条件。