Lucene 3.0 原理与代码分析 -

Size: px
Start display at page:

Download "Lucene 3.0 原理与代码分析 -"

Transcription

1 - 做最棒的软件开发交流社区 Lucene 3.0 原理与代码分析 作者: forfuture1978 本系列文章将详细描述几乎最新版本的Lucene的基本原理和代码分析 第 1 / 199 页 本书由JavaEye提供的电子书DIY功能自动生成于

2 目录 1. Lucene 学习总结 1.1 Lucene学习总结之一 全文检索的基本原理 Lucene学习总结之二 Lucene的总体架构 Lucene学习总结之三 Lucene的索引文件格式 (1) Lucene学习总结之三 Lucene的索引文件格式 (2) Lucene学习总结之三 Lucene的索引文件格式 (3) Lucene学习总结之四 Lucene索引过程分析(1) Lucene学习总结之四 Lucene索引过程分析(2) Lucene学习总结之四 Lucene索引过程分析(3) Lucene学习总结之四 Lucene索引过程分析(4) 有关Lucene的问题 2.1 有关Lucene的问题(1):为什么能搜的到 中华 AND 共和国 却搜不到 中华共和国? 有关Lucene的问题(2):stemming和lemmatization 有关Lucene的问题(3): 向量空间模型与Lucene的打分机制 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 第 2 / 199 页

3 1.1 Lucene学习总结之一 全文检索的基本原理 1.1 Lucene学习总结之一 全文检索的基本原理 发表时间: 本文csdn中的位置 一 总论 根据 Lucene是一个高效的 基于Java的全文检索库 所以在了解Lucene之前要费一番工夫了解一下全文检索 那么什么叫做全文检索呢 这要从我们生活中的数据说起 我们生活中的数据总体分为两种 结构化数据和非结构化数据 结构化数据 指具有固定格式或有限长度的数据 如数据库 元数据等 非结构化数据 指不定长或无固定格式的数据 如邮件 word文档等 当然有的地方还会提到第三种 半结构化数据 如XML HTML等 当根据需要可按结构化数据来处理 也可 抽取出纯文本按非结构化数据来处理 非结构化数据又一种叫法叫全文数据 按照数据的分类 搜索也分为两种 对结构化数据的搜索 如对数据库的搜索 用SQL语句 再如对元数据的搜索 如利用windows搜索对 文件名 类型 修改时间进行搜索等 对非结构化数据的搜索 如利用windows的搜索也可以搜索文件内容 Linux下的grep命令 再如用 Google和百度可以搜索大量内容数据 对非结构化数据也即对全文数据的搜索主要有两种方法 一种是顺序扫描法(Serial Scanning) 所谓顺序扫描 比如要找内容包含某一个字符串的文件 就是一个文档 一个文档的看 对于每一个文档 从头看到尾 如果此文档包含此字符串 则此文档为我们要找的文件 接着 看下一个文件 直到扫描完所有的文件 如利用windows的搜索也可以搜索文件内容 只是相当的慢 如果你 有一个80G硬盘 如果想在上面找到一个内容包含某字符串的文件 不花他几个小时 怕是做不到 Linux下的 grep命令也是这一种方式 大家可能觉得这种方法比较原始 但对于小数据量的文件 这种方法还是最直接 最方便的 但是对于大量的文件 这种方法就很慢了 第 3 / 199 页

4 1.1 Lucene学习总结之一 全文检索的基本原理 有人可能会说 对非结构化数据顺序扫描很慢 对结构化数据的搜索却相对较快 由于结构化数据有一定的结 构可以采取一定的搜索算法加快速度 那么把我们的非结构化数据想办法弄得有一定结构不就行了吗 这种想法很天然 却构成了全文检索的基本思路 也即将非结构化数据中的一部分信息提取出来 重新组织 使其变得有一定结构 然后对此有一定结构的数据进行搜索 从而达到搜索相对较快的目的 这部分从非结构化数据中提取出的然后重新组织的信息 我们称之索引 这种说法比较抽象 举几个例子就很容易明白 比如字典 字典的拼音表和部首检字表就相当于字典的索引 对每一个字的解释是非结构化的 如果字典没有音节表和部首检字表 在茫茫辞海中找一个字只能顺序扫描 然而字的某些信息可以提取出来进行结构化处理 比如读音 就比较结构化 分声母和韵母 分别只有几种可 以一一列举 于是将读音拿出来按一定的顺序排列 每一项读音都指向此字的详细解释的页数 我们搜索时按 结构化的拼音搜到读音 然后按其指向的页数 便可找到我们的非结构化数据 也即对字的解释 这种先建立索引 再对索引进行搜索的过程就叫全文检索(Full-text Search) 下面这幅图来自 Lucene in action 但却不仅仅描述了Lucene的检索过程 而是描述了全文检索的一般过 程 第 4 / 199 页

5 1.1 Lucene学习总结之一 全文检索的基本原理 全文检索大体分两个过程 索引创建(Indexing)和搜索索引(Search) 索引创建 将现实世界中所有的结构化和非结构化数据提取信息 创建索引的过程 搜索索引 就是得到用户的查询请求 搜索创建的索引 然后返回结果的过程 于是全文检索就存在三个重要问题 1. 索引里面究竟存些什么 (Index) 2. 如何创建索引 (Indexing) 3. 如何对索引进行搜索 (Search) 下面我们顺序对每个个问题进行研究 二 索引里面究竟存些什么 索引里面究竟需要存些什么呢 首先我们来看为什么顺序扫描的速度慢 其实是由于我们想要搜索的信息和非结构化数据中所存储的信息不一致造成的 非结构化数据中所存储的信息是每个文件包含哪些字符串 也即已知文件 欲求字符串相对容易 也即是从文 件到字符串的映射 而我们想搜索的信息是哪些文件包含此字符串 也即已知字符串 欲求文件 也即从字符 串到文件的映射 两者恰恰相反 于是如果索引总能够保存从字符串到文件的映射 则会大大提高搜索速度 由于从字符串到文件的映射是文件到字符串映射的反向过程 于是保存这种信息的索引称为反向索引 反向索引的所保存的信息一般如下 假设我的文档集合里面有100篇文档 为了方便表示 我们为文档编号从1到100 得到下面的结构 第 5 / 199 页

6 1.1 Lucene学习总结之一 全文检索的基本原理 左边保存的是一系列字符串 称为词典 每个字符串都指向包含此字符串的文档(Document)链表 此文档链表称为倒排表(Posting List) 有了索引 便使保存的信息和要搜索的信息一致 可以大大加快搜索的速度 比如说 我们要寻找既包含字符串 lucene 又包含字符串 solr 的文档 我们只需要以下几步 1. 取出包含字符串 lucene 的文档链表 2. 取出包含字符串 solr 的文档链表 3. 通过合并链表 找出既包含 lucene 又包含 solr 的文件 看到这个地方 有人可能会说 全文检索的确加快了搜索的速度 但是多了索引的过程 两者加起来不一定比 顺序扫描快多少 的确 加上索引的过程 全文检索不一定比顺序扫描快 尤其是在数据量小的时候更是如 此 而对一个很大量的数据创建索引也是一个很慢的过程 然而两者还是有区别的 顺序扫描是每次都要扫描 而创建索引的过程仅仅需要一次 以后便是一劳永逸的 了 每次搜索 创建索引的过程不必经过 仅仅搜索创建好的索引就可以了 这也是全文搜索相对于顺序扫描的优势之一 一次索引 多次使用 三 如何创建索引 全文检索的索引创建过程一般有以下几步 第一步 一些要索引的原文档(Document) 为了方便说明索引创建过程 这里特意用两个文件为例 文件一 Students should be allowed to go out with their friends, but not allowed to drink beer. 文件二 My friend Jerry went to school to see his students but found them drunk which is not allowed. 第 6 / 199 页

7 1.1 Lucene学习总结之一 全文检索的基本原理 第二步 将原文档传给分次组件(Tokenizer) 分词组件(Tokenizer)会做以下几件事情(此过程称为Tokenize) 1. 将文档分成一个一个单独的单词 2. 去除标点符号 3. 去除停词(Stop word) 所谓停词(Stop word)就是一种语言中最普通的一些单词 由于没有特别的意义 因而大多数情况下不能成为搜 索的关键词 因而创建索引时 这种词会被去掉而减少索引的大小 英语中挺词(Stop word)如 the, a this 等 对于每一种语言的分词组件(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) 语言处理组件(linguistic processor)主要是对得到的词元(token)做一些同语言相关的处理 对于英语 语言处理组件(Linguistic Processor)一般做以下几点 1. 变为小写(Lowercase) 2. 将单词缩减为词根形式 如 cars 到 car 等 这种操作称为 stemming 3. 将单词转变为词根形式 如 drove 到 drive 等 这种操作称为 lemmatization Stemming 和 lemmatization的异同 第 7 / 199 页

8 1.1 Lucene学习总结之一 全文检索的基本原理 相同之处 Stemming和lemmatization都要使词汇成为词根形式 两者的方式不同 Stemming采用的是 缩减 的方式 cars 到 car driving 到 drive Lemmatization采用的是 转变 的方式 drove 到 drove driving 到 drive 两者的算法不同 Stemming主要是采取某种固定的算法来做这种缩减 如去除 s 去除 ing 加 e 将 ational 变为 ate 将 tional 变为 tion Lemmatization主要是采用保存某种字典的方式做这种转变 比如字典中有 driving 到 drive drove 到 drive am, is, are 到 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 allow 也正是因为有语言处理的步骤 才能使搜索drove 而drive也能被搜索出来 第四步 将得到的词(Term)传给索引组件(Indexer) 索引组件(Indexer)主要做以下几件事情 1. 利用得到的词(Term)创建一个字典 在我们的例子中字典如下 Term Document ID student 1 allow 1 go 1 第 8 / 199 页

9 their 1 friend 1 allow 1 drink 1 beer 1 my 2 friend 2 jerry 2 go 2 school 2 see 2 his 2 student 2 find 2 them 2 drink 2 allow 2 2. 对字典按字母顺序进行排序 Term Document ID allow 1 allow 1 allow 2 beer 1 drink 1 drink 2 find 2 friend 1 friend 2 go 1 第 9 / 199 页 1.1 Lucene学习总结之一 全文检索的基本原理

10 go 2 his 2 jerry 2 my 2 school 2 see 2 student 1 student 2 their 1 them 2 3. 合并相同的词(Term)成为文档倒排(Posting List)链表 第 10 / 199 页 1.1 Lucene学习总结之一 全文检索的基本原理

11 1.1 Lucene学习总结之一 全文检索的基本原理 在此表中 有几个定义 Document Frequency 即文档频次 表示总共有多少文件包含此词(Term) Frequency 即词频率 表示此文件中包含了几个此词(Term) 所以对词(Term) allow 来讲 总共有两篇文档包含此词(Term) 从而词(Term)后面的文档链表总共有两 项 第一项表示包含 allow 的第一篇文档 即1号文档 此文档中 allow 出现了2次 第二项表示包含 allow 的第二个文档 是2号文档 此文档中 allow 出现了1次 到此为止 索引已经创建好了 我们可以通过它很快的找到我们想要的文档 而且在此过程中 我们惊喜地发现 搜索 drive driving drove driven 也能够被搜到 因 为在我们的索引中 driving drove driven 都会经过语言处理而变成 drive 在搜索时 如 果您输入 driving 输入的查询语句同样经过我们这里的一到三步 从而变为查询 drive 从而可以搜索 到想要的文档 三 如何对索引进行搜索 到这里似乎我们可以宣布 我们找到想要的文档了 然而事情并没有结束 找到了仅仅是全文检索的一个方面 不是吗 如果仅仅只有一个或十个文档包含我们查 询的字符串 我们的确找到了 然而如果结果有一千个 甚至成千上万个呢 那个又是您最想要的文件呢 打开Google吧 比如说您想在微软找份工作 于是您输入 Microsoft job 您却发现总共有 个结 果返回 好大的数字呀 突然发现找不到是一个问题 找到的太多也是一个问题 在如此多的结果中 如何将 最相关的放在最前面呢 第 11 / 199 页

12 1.1 Lucene学习总结之一 全文检索的基本原理 当然Google做的很不错 您一下就找到了jobs at Microsoft 想象一下 如果前几个全部是 Microsoft does a good job at software industry 将是多么可怕的事情呀 如何像Google一样 在成千上万的搜索结果中 找到和查询语句最相关的呢 如何判断搜索出的文档和查询语句的相关性呢 这要回到我们第三个问题 如何对索引进行搜索 搜索主要分为以下几步 第一步 用户输入查询语句 查询语句同我们普通的语言一样 也是有一定语法的 不同的查询语句有不同的语法 如SQL语句就有一定的语法 查询语句的语法根据全文检索系统的实现而不同 最基本的有比如 AND, OR, NOT等 举个例子 用户输入语句 lucene AND learned NOT hadoop 说明用户想找一个包含lucene和learned然而不包括hadoop的文档 第二步 对查询语句进行词法分析 语法分析 及语言处理 由于查询语句有语法 因而也要进行语法分析 语法分析及语言处理 第 12 / 199 页

13 1.1 Lucene学习总结之一 全文检索的基本原理 1. 词法分析主要用来识别单词和关键字 如上述例子中 经过词法分析 得到单词有lucene learned hadoop, 关键字有AND, NOT 如果在词法分析中发现不合法的关键字 则会出现错误 如lucene AMD learned 其中由于AND拼错 导致 AMD作为一个普通的单词参与查询 2. 语法分析主要是根据查询语句的语法规则来形成一棵语法树 如果发现查询语句不满足语法规则 则会报错 如lucene NOT AND learned 则会出错 如上述例子 lucene AND learned NOT hadoop形成的语法树如下 3. 语言处理同索引过程中的语言处理几乎相同 如learned变成learn等 经过第二步 我们得到一棵经过语言处理的语法树 第 13 / 199 页

14 1.1 Lucene学习总结之一 全文检索的基本原理 第三步 搜索索引 得到符合语法树的文档 此步骤有分几小步 1. 首先 在反向索引表中 分别找出包含lucene learn hadoop的文档链表 2. 其次 对包含lucene learn的链表进行合并操作 得到既包含lucene又包含learn的文档链表 3. 然后 将此链表与hadoop的文档链表进行差操作 去除包含hadoop的文档 从而得到既包含lucene 又包含learn而且不包含hadoop的文档链表 4. 此文档链表就是我们要找的文档 第四步 根据得到的文档和查询语句的相关性 对结果进行排序 虽然在上一步 我们得到了想要的文档 然而对于查询结果应该按照与查询语句的相关性进行排序 越相关者 越靠前 如何计算文档和查询语句的相关性呢 不如我们把查询语句看作一片短小的文档 对文档与文档之间的相关性(relevance)进行打分(scoring) 分数高 的相关性好 就应该排在前面 那么又怎么对文档之间的关系进行打分呢 这可不是一件容易的事情 首先我们看一看判断人之间的关系吧 首先看一个人 往往有很多要素 如性格 信仰 爱好 衣着 高矮 胖瘦等等 第 14 / 199 页

15 1.1 Lucene学习总结之一 全文检索的基本原理 其次对于人与人之间的关系 不同的要素重要性不同 性格 信仰 爱好可能重要些 衣着 高矮 胖瘦可能 就不那么重要了 所以具有相同或相似性格 信仰 爱好的人比较容易成为好的朋友 然而衣着 高矮 胖瘦 不同的人 也可以成为好的朋友 因而判断人与人之间的关系 首先要找出哪些要素对人与人之间的关系最重要 比如性格 信仰 爱好 其次 要判断两个人的这些要素之间的关系 比如一个人性格开朗 另一个人性格外向 一个人信仰佛教 另一个信 仰上帝 一个人爱好打篮球 另一个爱好踢足球 我们发现 两个人在性格方面都很积极 信仰方面都很善 良 爱好方面都爱运动 因而两个人关系应该会很好 我们再来看看公司之间的关系吧 首先看一个公司 有很多人组成 如总经理 经理 首席技术官 普通员工 保安 门卫等 其次对于公司与公司之间的关系 不同的人重要性不同 总经理 经理 首席技术官可能更重要一些 普通员 工 保安 门卫可能较不重要一点 所以如果两个公司总经理 经理 首席技术官之间关系比较好 两个公司 容易有比较好的关系 然而一位普通员工就算与另一家公司的一位普通员工有血海深仇 怕也难影响两个公司 之间的关系 因而判断公司与公司之间的关系 首先要找出哪些人对公司与公司之间的关系最重要 比如总经理 经理 首 席技术官 其次要判断这些人之间的关系 不如两家公司的总经理曾经是同学 经理是老乡 首席技术官曾是 创业伙伴 我们发现 两家公司无论总经理 经理 首席技术官 关系都很好 因而两家公司关系应该会很 好 分析了两种关系 下面看一下如何判断文档之间的关系了 首先 一个文档有很多词(Term)组成 如search, lucene, full-text, this, a, what等 其次对于文档之间的关系 不同的Term重要性不同 比如对于本篇文档 search, Lucene, full-text就相对重要 一些 this, a, what可能相对不重要一些 所以如果两篇文档都包含search, Lucene fulltext 这两篇文档的 相关性好一些 然而就算一篇文档包含this, a, what 另一篇文档不包含this, a, what 也不能影响两篇文档的 相关性 因而判断文档之间的关系 首先找出哪些词(Term)对文档之间的关系最重要 如search, Lucene, fulltext 然 后判断这些词(Term)之间的关系 找出词(Term)对文档的重要性的过程称为计算词的权重(Term weight)的过程 第 15 / 199 页

16 1.1 Lucene学习总结之一 全文检索的基本原理 计算词的权重(term weight)有两个参数 第一个是词(Term) 第二个是文档(Document) 词的权重(Term weight)表示此词(term)在此文档中的重要程度 越重要的词(Term)有越大的权重(Term weight) 因而在计算文档之间的相关性中将发挥更大的作用 判断词(Term)之间的关系从而得到文档相关性的过程应用一种叫做向量空间模型的算法(Vector Space Model) 下面仔细分析一下这两个过程 1. 计算权重(Term weight)的过程 影响一个词(Term)在一篇文档中的重要性主要有两个因素 Term Frequency (tf) 即此Term在此文档中出现了多少次 tf 越大说明越重要 Document Frequency (df) 即有多少文档包含次Term df 越大说明越不重要 容易理解吗 词(Term)在文档中出现的次数越多 说明此词(Term)对该文档越重要 如 搜索 这个词 在本 文档中出现的次数很多 说明本文档主要就是讲这方面的事的 然而在一篇英语文档中 this出现的次数更多 就说明越重要吗 不是的 这是由第二个因素进行调整 第二个因素说明 有越多的文档包含此词(Term), 说明 此词(Term)太普通 不足以区分这些文档 因而重要性越低 这也如我们程序员所学的技术 对于程序员本身来说 这项技术掌握越深越好 掌握越深说明花时间看的越 多 tf越大 找工作时越有竞争力 然而对于所有程序员来说 这项技术懂得的人越少越好 懂得的人少df 小 找工作越有竞争力 人的价值在于不可替代性就是这个道理 道理明白了 我们来看看公式 这仅仅只term weight计算公式的简单典型实现 实现全文检索系统的人会有自己的实现 Lucene就与此稍有 不同 第 16 / 199 页

17 1.1 Lucene学习总结之一 全文检索的基本原理 2. 判断Term之间的关系从而得到文档相关性的过程 也即向量空间模型的算法(VSM) 我们把文档看作一系列词(Term) 每一个词(Term)都有一个权重(Term weight) 不同的词(Term)根据自己在 文档中的权重来影响文档相关性的打分计算 于是我们把所有此文档中词(term)的权重(term weight) 看作一个向量 Document = {term1, term2,,term N Document Vector = {weight1, weight2,,weight N 同样我们把查询语句看作一个简单的文档 也用向量来表示 Query = {term1, term 2,, term N Query Vector = {weight1, weight2,, weight N 我们把所有搜索出的文档向量及查询向量放到一个N维空间中 每个词(term)是一维 如图 第 17 / 199 页

18 1.1 Lucene学习总结之一 全文检索的基本原理 我们认为两个向量之间的夹角越小 相关性越大 所以我们计算夹角的余弦值作为相关性的打分 夹角越小 余弦值越大 打分越高 相关性越大 有人可能会问 查询语句一般是很短的 包含的词(Term)是很少的 因而查询向量的维数很小 而文档很长 包含词(Term)很多 文档向量维数很大 你的图中两者维数怎么都是N呢 在这里 既然要放到相同的向量空间 自然维数是相同的 不同时 取二者的并集 如果不含某个词(Term) 时 则权重(Term Weight)为0 相关性打分公式如下 举个例子 查询语句有11个Term 共有三篇文档搜索出来 其中各自的权重(Term weight) 如下表格 t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 D D D Q 于是计算 三篇文档同查询语句的相关性打分分别为 第 18 / 199 页

19 1.1 Lucene学习总结之一 全文检索的基本原理 于是文档二相关性最高 先返回 其次是文档一 最后是文档三 到此为止 我们可以找到我们最想要的文档了 说了这么多 其实还没有进入到Lucene 而仅仅是信息检索技术(Information retrieval)中的基本理论 然而当 我们看过Lucene后我们会发现 Lucene是对这种基本理论的一种基本的的实践 所以在以后分析Lucene的文 章中 会常常看到以上理论在Lucene中的应用 在进入Lucene之前 对上述索引创建和搜索过程所一个总结 如图 此图参照 开放源代码的全文检索引擎Lucene 1. 索引过程 1) 有一系列被索引文件 第 19 / 199 页

20 1.1 Lucene学习总结之一 全文检索的基本原理 2) 被索引文件经过语法分析和语言处理形成一系列词(Term) 3) 经过索引创建形成词典和反向索引表 4) 通过索引存储将索引写入硬盘 2. 搜索过程 a) 用户输入查询语句 b) 对查询语句经过语法分析和语言分析得到一系列词(Term) c) 通过语法分析得到一个查询树 d) 通过索引存储将索引读入到内存 e) 利用查询树搜索索引 从而得到每个词(Term)的文档链表 对文档链表进行交 差 并得到结果文档 f) 将搜索到的结果文档对查询的相关性进行排序 g) 返回查询结果给用户 下面我们可以进入Lucene的世界了 第 20 / 199 页

21 1.2 Lucene学习总结之二 Lucene的总体架构 1.2 Lucene学习总结之二 Lucene的总体架构 发表时间: 本文csdn中的位置 Lucene总的来说是 一个高效的 可扩展的 全文检索库 全部用Java实现 无须配置 仅支持纯文本文件的索引(Indexing)和搜索(Search) 不负责由其他格式的文件抽取纯文本文件 或从网络中抓取文件的过程 在Lucene in action中 Lucene 的构架和过程如下图 说明Lucene是有索引和搜索的两个过程 包含索引创建 索引 搜索三个要点 让我们更细一些看Lucene的各组件 第 21 / 199 页

22 1.2 Lucene学习总结之二 Lucene的总体架构 被索引的文档用Document对象表示 IndexWriter通过函数addDocument将文档添加到索引中 实现创建索引的过程 Lucene的索引是应用反向索引 当用户有请求时 Query代表用户的查询语句 IndexSearcher通过函数search搜索Lucene Index IndexSearcher计算term weight和score并且将结果返回给用户 返回给用户的文档集合用TopDocsCollector表示 那么如何应用这些组件呢 让我们再详细到对Lucene API 的调用实现索引和搜索过程 第 22 / 199 页

23 1.2 Lucene学习总结之二 Lucene的总体架构 索引过程如下 创建一个IndexWriter用来写索引文件 它有几个参数 INDEX_DIR就是索引文件所存放的位 置 Analyzer便是用来对文档进行词法分析和语言处理的 创建一个Document代表我们要索引的文档 将不同的Field加入到文档中 我们知道 一篇文档有多种信息 如题目 作者 修改时间 内 容等 不同类型的信息用不同的Field来表示 在本例子中 一共有两类信息进行了索引 一个 是文件路径 一个是文件内容 其中FileReader的SRC_FILE就表示要索引的源文件 IndexWriter调用函数addDocument将索引写到索引文件夹中 搜索过程如下 IndexReader将磁盘上的索引信息读入到内存 INDEX_DIR就是索引文件存放的位置 创建IndexSearcher准备进行搜索 创建Analyer用来对查询语句进行词法分析和语言处理 创建QueryParser用来对查询语句进行语法分析 QueryParser调用parser进行语法分析 形成查询语法树 放到Query中 第 23 / 199 页

24 1.2 Lucene学习总结之二 Lucene的总体架构 IndexSearcher调用search对查询语法树Query进行搜索 得到结果 TopScoreDocCollector 以上便是Lucene API函数的简单调用 然而当进入Lucene的源代码后 发现Lucene有很多包 关系错综复杂 然而通过下图 我们不难发现 Lucene的各源码模块 都是对普通索引和搜索过程的一种实现 此图是上一节介绍的全文检索的流程对应的Lucene实现的包结构 (参照 about.htm中文章 开放源代码的全文检索引擎Lucene ) Lucene的analysis模块主要负责词法分析及语言处理而形成Term Lucene的index模块主要负责索引的创建 里面有IndexWriter Lucene的store模块主要负责索引的读写 Lucene的QueryParser主要负责语法分析 Lucene的search模块主要负责对索引的搜索 Lucene的similarity模块主要负责对相关性打分的实现 了解了Lucene的整个结构 我们便可以开始Lucene的源码之旅了 第 24 / 199 页

25 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 发表时间: 本文csdn中的位置 Lucene的索引里面存了些什么 如何存放的 也即Lucene的索引文件格式 是读懂Lucene源代码的一把钥 匙 当我们真正进入到Lucene源代码之中的时候 我们会发现: Lucene的索引过程 就是按照全文检索的基本过程 将倒排表写成此文件格式的过程 Lucene的搜索过程 就是按照此文件格式将索引进去的信息读出来 然后计算每篇文档打分(score)的 过程 本文详细解读了Apache Lucene - Index File Formats( fileformats.html) 这篇文章 一 基本概念 下图就是Lucene生成的索引的一个实例 第 25 / 199 页

26 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) Lucene的索引结构是有层次结构的 主要分以下几个层次 索引(Index) 在Lucene中一个索引是放在一个文件夹中的 如上图 同一文件夹中的所有的文件构成一个Lucene索引 段(Segment) 一个索引可以包含多个段 段与段之间是独立的 添加新文档可以生成新的段 不同的段可以 合并 如上图 具有相同前缀文件的属同一个段 图中共两个段 "_0" 和 "_1" segments.gen和segments_5是段的元数据文件 也即它们保存了段的属性信息 文档(Document) 文档是我们建索引的基本单位 不同的文档是保存在不同的段中的 一个段可以包含多篇文 档 新添加的文档是单独保存在一个新生成的段中 随着段的合并 不同的文档合并到同一个段 中 域(Field) 第 26 / 199 页

27 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 一篇文档包含不同类型的信息 可以分开索引 比如标题 时间 正文 作者等 都可以保存 在不同的域里 不同域的索引方式可以不同 在真正解析域的存储的时候 我们会详细解读 词(Term) 词是索引的最小单位 是经过词法分析和语言处理后的字符串 Lucene的索引结构中 即保存了正向信息 也保存了反向信息 所谓正向信息 按层次保存了从索引 一直到词的包含关系 索引(Index) > 段(segment) > 文档(Document) > 域(Field) > 词(Term) 也即此索引包含了那些段 每个段包含了那些文档 每个文档包含了那些域 每个域包含了那些词 既然是层次结构 则每个层次都保存了本层次的信息以及下一层次的元信息 也即属性信息 比如一本 介绍中国地理的书 应该首先介绍中国地理的概况 以及中国包含多少个省 每个省介绍本省的基本概 况及包含多少个市 每个市介绍本市的基本概况及包含多少个县 每个县具体介绍每个县的具体情况 如上图 包含正向信息的文件有 segments_n保存了此索引包含多少个段 每个段包含多少篇文档 XXX.fnm保存了此段包含了多少个域 每个域的名称及索引方式 XXX.fdx XXX.fdt保存了此段包含的所有文档 每篇文档包含了多少域 每个域保存了那些信 息 XXX.tvx XXX.tvd XXX.tvf保存了此段包含多少文档 每篇文档包含了多少域 每个域包含了 多少词 每个词的字符串 位置等信息 所谓反向信息 保存了词典到倒排表的映射 词(Term) > 文档(Document) 如上图 包含反向信息的文件有 XXX.tis XXX.tii保存了词典(Term Dictionary) 也即此段包含的所有的词按字典顺序的排序 XXX.frq保存了倒排表 也即包含每个词的文档ID列表 XXX.prx保存了倒排表中每个词在包含此词的文档中的位置 在了解Lucene索引的详细结构之前 先看看Lucene索引中的基本数据类型 二 基本类型 Lucene索引文件中 用一下基本类型来保存信息 第 27 / 199 页

28 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) Byte 是最基本的类型 长8位(bit) UInt32 由4个Byte组成 UInt64 由8个Byte组成 VInt 变长的整数类型 它可能包含多个Byte 对于每个Byte的8位 其中后7位表示数值 最高1位 表示是否还有另一个Byte 0表示没有 1表示有 越前面的Byte表示数值的低位 越后面的Byte表示数值的高位 例如130化为二进制为 1000, 0010 总共需要8位 一个Byte表示不了 因而需要两个Byte来 表示 第一个Byte表示后7位 并且在最高位置1来表示后面还有一个Byte 所以为(1) 第二个Byte表示第8位 并且最高位置0来表示后面没有其他的Byte了 所以为(0) Chars 是UTF-8编码的一系列Byte String 一个字符串首先是一个VInt来表示此字符串包含的字符的个数 接着便是UTF-8编码的字符序 列Chars 三 基本规则 Lucene为了使的信息的存储占用的空间更小 访问速度更快 采取了一些特殊的技巧 然而在看Lucene文件格 式的时候 这些技巧却容易使我们感到困惑 所以有必要把这些特殊的技巧规则提取出来介绍一下 在下不才 胡乱给这些规则起了一些名字 是为了方便后面应用这些规则的时候能够简单 不妥之处请大家谅 解 第 28 / 199 页

29 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 1. 前缀后缀规则(Prefix+Suffix) Lucene在反向索引中 要保存词典(Term Dictionary)的信息 所有的词(Term)在词典中是按照字典顺序进行排 列的 然而词典中包含了文档中的几乎所有的词 并且有的词还是非常的长的 这样索引文件会非常的大 所 谓前缀后缀规则 即当某个词和前一个词有共同的前缀的时候 后面的词仅仅保存前缀在词中的偏移(offset) 以及除前缀以外的字符串(称为后缀) 比如要存储如下词:term termagancy termagant terminal 如果按照正常方式来存储 需要的空间如下 [VInt = 4] [t][e][r][m] [VInt = 10][t][e][r][m][a][g][a][n][c][y] [VInt = 9][t][e][r][m][a][g][a][n][t] [VInt = 8][t][e][r][m][i][n][a][l] 共需要35个Byte. 如果应用前缀后缀规则 需要的空间如下 [VInt = 4] [t][e][r][m] [VInt = 4 (offset)][vint = 6][a][g][a][n][c][y] [VInt = 8 (offset)][vint = 1][t] [VInt = 4(offset)][VInt = 4][i][n][a][l] 共需要22个Byte 大大缩小了存储空间 尤其是在按字典顺序排序的情况下 前缀的重合率大大提高 2. 差值规则(Delta) 在Lucene的反向索引中 需要保存很多整型数字的信息 比如文档ID号 比如词(Term)在文档中的位置等等 由上面介绍 我们知道 整型数字是以VInt的格式存储的 随着数值的增大 每个数字占用的Byte的个数也逐 渐的增多 所谓差值规则(Delta)就是先后保存两个整数的时候 后面的整数仅仅保存和前面整数的差即可 第 29 / 199 页

30 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 比如要存储如下整数 如果按照正常方式来存储 需要的空间如下 [(1) 000, 0010][(1) 000, 0000][(0) 000, 0001] [(1) 000, 0011][(1) 000, 0000][(0) 000, 0001] [(1) 000, 0100][(1) 000, 0000][(0) 000, 0001] [(1) 000, 0101][(1) 000, 0000][(0) 000, 0001] 供需12个Byte 如果应用差值规则来存储 需要的空间如下 [(1) 000, 0010][(1) 000, 0000][(0) 000, 0001] [(0) 000, 0001] [(0) 000, 0001] [(0) 000, 0001] 共需6个Byte 大大缩小了存储空间 而且无论是文档ID 还是词在文档中的位置 都是按从小到大的顺序 逐渐增大的 3. 或然跟随规则(A, B?) Lucene的索引结构中存在这样的情况 某个值A后面可能存在某个值B 也可能不存在 需要一个标志来表示后 面是否跟随着B 一般的情况下 在A后面放置一个Byte 为0则后面不存在B 为1则后面存在B 或者0则后面存在B 1则后面 不存在B 但这样要浪费一个Byte的空间 其实一个Bit就可以了 在Lucene中 采取以下的方式 A的值左移一位 空出最后一位 作为标志位 来表示后面是否跟随B 所以在 这种情况下 A/2是真正的A原来的值 如果去读Apache Lucene - Index File Formats这篇文章 会发现很多符合这种规则的.frq文件中的DocDelta[, Freq?] DocSkip,PayloadLength?.prx文件中的PositionDelta,Payload? (但不完全是 如下表分析) 第 30 / 199 页

31 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 当然还有一些带?的但不属于此规则的.frq文件中的SkipChildLevelPointer? 是多层跳跃表中 指向下一层表的指针 当然如果是最后一 层 此值就不存在 也不需要标志.tvf文件中的Positions?, Offsets? 在此类情况下 带?的值是否存在 并不取决于前面的值的最后一位 而是取决于Lucene的某项配置 当然这些配置也是保存在Lucene索引文件中的 如Position和Offset是否存储 取决于.fnm文件中对于每个域的配置 (TermVector.WITH_POSITIONS和TermVector.WITH_OFFSETS) 为什么会存在以上两种情况 其实是可以理解的 对于符合或然跟随规则的 是因为对于每一个A B是否存在都不相同 当这种情况大量存在的时候 从 一个Byte到一个Bit如此8倍的空间节约还是很值得的 对于不符合或然跟随规则的 是因为某个值的是否存在的配置对于整个域(Field)甚至整个索引都是有效 的 而非每次的情况都不相同 因而可以统一存放一个标志 文章中对如下格式的描述令人困惑 Positions --> <PositionDelta,Payload?> Freq Payload --> <PayloadLength?,PayloadData> PositionDelta和Payload是否适用或然跟随规则呢 如何标识PayloadLength是否存在呢 其实PositionDelta和Payload并不符合或然跟随规则 Payload是否存在 是由.fnm文件中对于每个域的配置中有关 Payload的配置决定的(FieldOption.STORES_PAYLOADS) 当Payload不存在时 PayloadDelta本身不遵从或然跟随原则 当Payload存在时 格式应该变成如下 Positions --> <PositionDelta,PayloadLength?,PayloadData> 从而PositionDelta和PayloadLength一起适用或然跟随规则 4. 跳跃表规则(Skip list) 为了提高查找的性能 Lucene在很多地方采取的跳跃表的数据结构 跳跃表(Skip List)是如图的一种数据结构 有以下几个基本特征 元素是按顺序排列的 在Lucene中 或是按字典顺序排列 或是按从小到大顺序排列 第 31 / 199 页 Freq

32 1.3 Lucene学习总结之三 Lucene的索引文件格式 (1) 跳跃是有间隔的(Interval) 也即每次跳跃的元素数 间隔是事先配置好的 如图跳跃表的间隔为3 跳跃表是由层次的(level) 每一层的每隔指定间隔的元素构成上一层 如图跳跃表共有2层 需要注意一点的是 在很多数据结构或算法书中都会有跳跃表的描述 原理都是大致相同的 但是定义稍有差 别 对间隔(Interval)的定义 如图中 有的认为间隔为2 即两个上层元素之间的元素数 不包括两个上层 元素 有的认为是3 即两个上层元素之间的差 包括后面上层元素 不包括前面的上层元素 有的认 为是4 即除两个上层元素之间的元素外 既包括前面 也包括后面的上层元素 Lucene是采取的第二 种定义 对层次(Level)的定义 如图中 有的认为应该包括原链表层 并从1开始计数 则总层次为3 为1 2 3层 有的认为应该包括原链表层 并从0计数 为0 1 2层 有的认为不应该包括原链表层 且 从1开始计数 则为1 2层 有的认为不应该包括链表层 且从0开始计数 则为0 1层 Lucene采取 的是最后一种定义 跳跃表比顺序查找 大大提高了查找速度 如查找元素72 原来要访问 总共10个元素 应用跳跃表后 只要首先访问第1层的50 发现72大于50 而第1层无下一个节点 然 后访问第2层的94 发现94大于72 然后访问原链表的72 找到元素 共需要访问3个元素即可 然而Lucene在具体实现上 与理论又有所不同 在具体的格式中 会详细说明 第 32 / 199 页

33 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 发表时间: 本文在csdn中的位置 四 具体格式 上面曾经交代过 Lucene保存了从Index到Segment到Document到Field一直到Term的正向信息 也包括了 从Term到Document映射的反向信息 还有其他一些Lucene特有的信息 下面对这三种信息一一介绍 4.1. 正向信息 Index > Segments (segments.gen, segments_n) > Field(fnm, fdx, fdt) > Term (tvx, tvd, tvf) 上面的层次结构不是十分的准确 因为segments.gen和segments_N保存的是段(segment)的元数据信息 (metadata) 其实是每个Index一个的 而段的真正的数据信息 是保存在域(Field)和词(Term)中的 段的元数据信息(segments_N) 一个索引(Index)可以同时存在多个segments_N(至于如何存在多个segments_N 在描述完详细信息之后会举 例说明) 然而当我们要打开一个索引的时候 我们必须要选择一个来打开 那如何选择哪个segments_N呢 Lucene采取以下过程 其一 在所有的segments_N中选择N最大的一个 基本逻辑参照 SegmentInfos.getCurrentSegmentGeneration(File[] files) 其基本思路就是在所有以segments开 头 并且不是segments.gen的文件中 选择N最大的一个作为genA 其二 打开segments.gen 其中保存了当前的N值 其格式如下 读出版本号(Version) 然后再读出 两个N 如果两者相等 则作为genB IndexInput geninput = directory.openinput(indexfilenames.segments_gen);//"segments.gen" int version = geninput.readint();//读出版本号 if (version == FORMAT_LOCKLESS) {//如果版本号正确 第 33 / 199 页

34 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) long gen0 = geninput.readlong();//读出第一个n long gen1 = geninput.readlong();//读出第二个n if (gen0 == gen1) {//如果两者相等则为genB genb = gen0; 其三 在上述得到的genA和genB中选择最大的那个作为当前的N 方才打开segments_N文件 其基 本逻辑如下 if (gena > genb) gen = gena; else gen = genb; 如下图是segments_N的具体格式 Format 索引文件格式的版本号 由于Lucene是在不断开发过程中的 因而不同版本的Lucene 其索引文件格式也不尽相同 于 是规定一个版本号 Lucene 2.1此值-3 Lucene 2.9时 此值为-9 当用某个版本号的IndexReader读取另一个版本号生成的索引的时候 会因为此值不同而报 错 Version 索引的版本号 记录了IndexWriter将修改提交到索引文件中的次数 第 34 / 199 页

35 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 其初始值大多数情况下从索引文件里面读出 仅仅在索引开始创建的时候 被赋予当前的时 间 已取得一个唯一值 其值改变在IndexWriter.commit->IndexWriter.startCommit>SegmentInfos.prepareCommit->SegmentInfos.write->writeLong(++version) 其初始值之所最初取一个时间 是因为我们并不关心IndexWriter将修改提交到索引的具体次 数 而更关心到底哪个是最新的 IndexReader中常比较自己的version和索引文件中的 version是否相同来判断此indexreader被打开后 还有没有被IndexWriter更新 //在DirectoryReader中有一下函数 public boolean iscurrent() throws CorruptIndexException, IOException { return SegmentInfos.readCurrentVersion(directory) == segmentinfos.getversion(); NameCount 是下一个新段(Segment)的段名 所有属于同一个段的索引文件都以段名作为文件名 一般为_0.xxx, _0.yyy, _1.xxx, _1.yyy 新生成的段的段名一般为原有最大段名加一 如同的索引 NameCount读出来是2 说明新的段为_2.xxx, _2.yyy 第 35 / 199 页

36 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) SegCount 段(Segment)的个数 如上图 此值为2 SegCount个段的元数据信息 SegName 段名 所有属于同一个段的文件都有以段名作为文件名 如上图 第一个段的段名为"_0" 第二个段的段名为"_1" SegSize 此段中包含的文档数 然而此文档数是包括已经删除 又没有optimize的文档的 因为在optimize之前 Lucene的段中包含了所有被索引过的文档 而被删除的文档是保存在.del文件中的 在 搜索的过程中 是先从段中读到了被删除的文档 然后再用.del中的标志 将这篇文档 过滤掉 如下的代码形成了上图的索引 可以看出索引了两篇文档形成了_0段 然后又删除了其 中一篇 形成了_0_1.del 又索引了两篇文档形成_1段 然后又删除了其中一篇 形成 _1_1.del 因而在两个段中 此值都是2 IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setusecompoundfile(false); indexdocs(writer, docdir);//docdir中只有两篇文档 //文档一为 Students should be allowed to go out with their friends, but not allowed to drink beer. //文档二为 My friend Jerry went to school to see his students but found them drunk which is not allowed. writer.commit();//提交两篇文档 形成_0段 writer.deletedocuments(new Term("contents", "school"));//删除文档二 writer.commit();//提交删除 形成_0_1.del indexdocs(writer, docdir);//再次索引两篇文档 Lucene不能判别文档与文档的不同 因而算两 篇新的文档 writer.commit();//提交两篇文档 形成_1段 writer.deletedocuments(new Term("contents", "school"));//删除第二次添加的文档二 writer.close();//提交删除 形成_1_1.del 第 36 / 199 页

37 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) DelGen.del文件的版本号 Lucene中 在optimize之前 删除的文档是保存在.del文件中的 在Lucene 2.9中 文档删除有以下几种方式 IndexReader.deleteDocument(int docid)是用indexreader按文档号删除 IndexReader.deleteDocuments(Term term)是用indexreader删除包含此词 (Term)的文档 IndexWriter.deleteDocuments(Term term)是用indexwriter删除包含此词 (Term)的文档 IndexWriter.deleteDocuments(Term[] terms)是用indexwriter删除包含这 些词(Term)的文档 IndexWriter.deleteDocuments(Query query)是用indexwriter删除能满足此 查询(Query)的文档 IndexWriter.deleteDocuments(Query[] queries)是用indexwriter删除能满 足这些查询(Query)的文档 原来的版本中Lucene的删除一直是由IndexReader来完成的 在Lucene 2.9中 虽可以用IndexWriter来删除 但是其实真正的实现是在IndexWriter中 保存 了readerpool 当IndexWriter向索引文件提交删除的时候 仍然是从 readerpool中得到相应的indexreader 并用IndexReader来进行删除的 下 面的代码可以说明 IndexWriter.applyDeletes() -> DocumentsWriter.applyDeletes(SegmentInfos) -> reader.deletedocument(doc); DelGen是每当IndexWriter向索引文件中提交删除操作的时候 加1 并生成 新的.del文件 第 37 / 199 页

38 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) IndexWriter.commit() -> IndexWriter.applyDeletes() -> IndexWriter$ReaderPool.release(SegmentReader) -> SegmentReader(IndexReader).commit() -> SegmentReader.doCommit(Map) -> SegmentInfo.advanceDelGen() -> if (delgen == NO) { delgen = YES; else { delgen++; IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setusecompoundfile(false); indexdocs(writer, docdir);//索引两篇文档 一篇包含"school" 另一篇包含"beer" writer.commit();//提交两篇文档到索引文件 形成段(Segment) "_0" writer.deletedocuments(new Term("contents", "school"));//删除包含"school"的文档 其实是 删除了两篇文档中的一篇 writer.commit();//提交删除到索引文件 形成"_0_1.del" writer.deletedocuments(new Term("contents", "beer"));//删除包含"beer"的文档 其实是删 除了两篇文档中的另一篇 writer.commit();//提交删除到索引文件 形成"_0_2.del" indexdocs(writer, docdir);//索引两篇文档 和上次的文档相同 但是Lucene无法区分 认为是 另外两篇文档 writer.commit();//提交两篇文档到索引文件 形成段"_1" writer.deletedocuments(new Term("contents", "beer"));//删除包含"beer"的文档 其中 段"_0"已经无可删除 段"_1"被删除一篇 writer.close();//提交删除到索引文件 形成"_1_1.del" 形成的索引文件如下 第 38 / 199 页

39 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) DocStoreOffset DocStoreSegment DocStoreIsCompoundFile 对于域(Stored Field)和词向量(Term Vector)的存储可以有不同的方式 即可以每个段 (Segment)单独存储自己的域和词向量信息 也可以多个段共享域和词向量 把它们存 储到一个段中去 如果DocStoreOffset为-1 则此段单独存储自己的域和词向量 从存储文件上来看 如果此段段名为XXX 则此段有自己的XXX.fdt XXX.fdx XXX.tvf XXX.tvd XXX.tvx文件 DocStoreSegment和DocStoreIsCompoundFile在此处不被保存 如果DocStoreOffset不为-1 则DocStoreSegment保存了共享的段的名字 比如为 YYY DocStoreOffset则为此段的域及词向量信息在共享段中的偏移量 则此段没有自 己的XXX.fdt XXX.fdx XXX.tvf XXX.tvd XXX.tvx文件 而是将信息存放在共享段 的YYY.fdt YYY.fdx YYY.tvf YYY.tvd YYY.tvx文件中 第 39 / 199 页

40 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) DocumentsWriter中有两个成员变量 String segment是当前索引信息存放的段 String docstoresegment是域和词向量信息存储的段 两者可以相同也可以不同 决 定了域和词向量信息是存储在本段中 还是和其他的段共享 IndexWriter.flush(boolean triggermerge, boolean flushdocstores, boolean flushdeletes)中第二个参数flushdocstores会影响到是否单独或是共享存储 其实最 终影响的是DocumentsWriter.closeDocStore() 每当flushDocStores为false时 closedocstore不被调用 说明下次添加到索引文件中的域和词向量信息是同此次共享 一个段的 直到flushDocStores为true的时候 closedocstore被调用 从而下次添加 到索引文件中的域和词向量信息将被保存在一个新的段中 不同此次共享一个段(在这 里需要指出的是Lucene的一个很奇怪的实现 虽然下次域和词向量信息是被保存到新 的段中 然而段名却是这次被确定了的 在initSegmentName中当 docstoresegment == null时 被置为当前的segment 而非下一个新的segment docstoresegment = segment 于是会出现如下面的例子的现象) 好在共享域和词向量存储并不是经常被使用到 实现也或有缺陷 暂且解释到此 第 40 / 199 页

41 第 41 / 199 页 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2)

42 第 42 / 199 页 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2)

43 第 43 / 199 页 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2)

44 第 44 / 199 页 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2)

45 第 45 / 199 页 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2)

46 第 46 / 199 页 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2)

47 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) HasSingleNormFile 在搜索的过程中 标准化因子(Normalization Factor)会影响文档最后的评分 不同的文档重要性不同 不同的域重要性也不同 因而每个文档的每个域都可以有自己 的标准化因子 如果HasSingleNormFile为1 则所有的标准化因子都是存在.nrm文件中的 如果HasSingleNormFile不是1 则每个域都有自己的标准化因子文件.fN NumField 域的数量 NormGen 如果每个域有自己的标准化因子文件 则此数组描述了每个标准化因子文件的版本号 也即.fN的N IsCompoundFile 是否保存为复合文件 也即把同一个段中的文件按照一定格式 保存在一个文件当中 这样可以减少每次打开文件的个数 是否为复合文件 由接口IndexWriter.setUseCompoundFile(boolean)设定 非符合文件同符合文件的对比如下图 非复合文件 复合文件 DeletionCount 记录了此段中删除的文档的数目 第 47 / 199 页

48 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) HasProx 如果至少有一个段omitTf为false 也即词频(term freqency)需要被保存 则HasProx 为1 否则为0 Diagnostics 调试信息 User map data 保存了用户从字符串到字符串的映射Map CheckSum 此文件segment_N的校验和 读取此文件格式参考SegmentInfos.read(Directory directory, String segmentfilename): int format = input.readint(); version = input.readlong(); // read version counter = input.readint(); // read counter for (int i = input.readint(); i > 0; i--) // read segmentinfos add(new SegmentInfo(directory, format, input)); name = input.readstring(); doccount = input.readint(); delgen = input.readlong(); docstoreoffset = input.readint(); docstoresegment = input.readstring(); docstoreiscompoundfile = (1 == input.readbyte()); hassinglenormfile = (1 == input.readbyte()); int numnormgen = input.readint(); normgen = new long[numnormgen]; for(int j=0;j normgen[j] = input.readlong(); iscompoundfile = input.readbyte(); delcount = input.readint(); hasprox = input.readbyte() == 1; diagnostics = input.readstringstringmap(); userdata = input.readstringstringmap(); final long checksumnow = input.getchecksum(); final long checksumthen = input.readlong(); 第 48 / 199 页

49 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 域(Field)的元数据信息(.fnm) 一个段(Segment)包含多个域 每个域都有一些元数据信息 保存在.fnm文件中.fnm文件的格式如下 FNMVersion 是fnm文件的版本号 对于Lucene 2.9为-2 FieldsCount 域的数目 一个数组的域(Fields) FieldName 域名 如"title" "modified" "content"等 FieldBits:一系列标志位 表明对此域的索引方式 最低位 1表示此域被索引 0则不被索引 所谓被索引 也即放到倒排表中去 仅仅被索引的域才能够被搜到 Field.Index.NO则表示不被索引 Field.Index.ANALYZED则表示不但被索引 而且被分词 比如索引"hello world"后 无论是搜"hello" 还是搜"world"都能够被搜到 Field.Index.NOT_ANALYZED表示虽然被索引 但是不分词 比如索引"hello world"后 仅当搜"hello world"时 能够搜到 搜"hello"和搜"world"都搜不 到 一个域出了能够被索引 还能够被存储 仅仅被存储的域是搜索不到的 但是 能通过文档号查到 多用于不想被搜索到 但是在通过其它域能够搜索到的情 况下 能够随着文档号返回给用户的域 第 49 / 199 页

50 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) Field.Store.Yes则表示存储此域 Field.Store.NO则表示不存储此域 倒数第二位 1表示保存词向量 0为不保存词向量 Field.TermVector.YES表示保存词向量 Field.TermVector.NO表示不保存词向量 倒数第三位 1表示在词向量中保存位置信息 Field.TermVector.WITH_POSITIONS 倒数第四位 1表示在词向量中保存偏移量信息 Field.TermVector.WITH_OFFSETS 倒数第五位 1表示不保存标准化因子 Field.Index.ANALYZED_NO_NORMS Field.Index.NOT_ANALYZED_NO_NORMS 倒数第六位 是否保存payload 要了解域的元数据信息 还要了解以下几点 位置(Position)和偏移量(Offset)的区别 位置是基于词Term的 偏移量是基于字母或汉字的 索引域(Indexed)和存储域(Stored)的区别 一个域为什么会被存储(store)而不被索引(Index)呢 在一个文档中的所有信息中 有这样一部 分信息 可能不想被索引从而可以搜索到 但是当这个文档由于其他的信息被搜索到时 可以 同其他信息一同返回 举个例子 读研究生时 您好不容易写了一篇论文交给您的导师 您的导师却要他所第一作者 而您做第二作者 然而您导师不想别人在论文系统中搜索您的名字时找到这篇论文 于是在论 文系统中 把第二作者这个Field的Indexed设为false 这样别人搜索您的名字 永远不知道您 写过这篇论文 只有在别人搜索您导师的名字从而找到您的文章时 在一个角落表述着第二作 者是您 payload的使用 我们知道 索引是以倒排表形式存储的 对于每一个词 都保存了包含这个词的一个链表 当 然为了加快查询速度 此链表多用跳跃表进行存储 Payload信息就是存储在倒排表中的 同文档号一起存放 多用于存储与每篇文档相关的一些信 息 当然这部分信息也可以存储域里(stored Field) 两者从功能上基本是一样的 然而当要存 储的信息很多的时候 存放在倒排表里 利用跳跃表 有利于大大提高搜索速度 第 50 / 199 页

51 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) Payload的存储方式如下图 Payload主要有以下几种用法 存储每个文档都有的信息 比如有的时候 我们想给每个文档赋一个我们自己的文档 号 而不是用Lucene自己的文档号 于是我们可以声明一个特殊的域(Field)"_ID"和特 殊的词(Term)"_ID" 使得每篇文档都包含词"_ID" 于是在词"_ID"的倒排表里面对于 每篇文档又有一项 每一项都有一个payload 于是我们可以在payload里面保存我们 自己的文档号 每当我们得到一个Lucene的文档号的时候 就能从跳跃表中查找到我 们自己的文档号 第 51 / 199 页

52 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) //声明一个特殊的域和特殊的词 public static final String ID_PAYLOAD_FIELD = "_ID"; public static final String ID_PAYLOAD_TERM = "_ID"; public static final Term ID_TERM = new Term(ID_PAYLOAD_TERM, ID_PAYLOAD_FIELD); //声明一个特殊的TokenStream 它只生成一个词(Term) 就是那个特殊的词 在特殊的域里面 static class SinglePayloadTokenStream extends TokenStream { private Token token; private boolean returntoken = false; SinglePayloadTokenStream(String idpayloadterm) { char[] term = idpayloadterm.tochararray(); token = new Token(term, 0, term.length, 0, term.length); void setpayloadvalue(byte[] value) { token.setpayload(new Payload(value)); returntoken = true; public Token next() throws IOException { if (returntoken) { returntoken = false; return token; else { return null; //对于每一篇文档 都让它包含这个特殊的词 在特殊的域里面 SinglePayloadTokenStream singlepayloadtokenstream = new SinglePayloadTokenStream(ID_PAYLOAD_TERM); singlepayloadtokenstream.setpayloadvalue(long2bytes(id)); doc.add(new Field(ID_PAYLOAD_FIELD, singlepayloadtokenstream)); //每当得到一个Lucene的文档号时 通过以下的方式得到payload里面的文档号 第 52 / 199 页

53 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) long id = 0; TermPositions tp = reader.termpositions(id_payload_term); boolean ret = tp.skipto(docid); tp.nextposition(); int payloadlength = tp.getpayloadlength(); byte[] payloadbuffer = new byte[payloadlength]; tp.getpayload(payloadbuffer, 0); id = bytes2long(payloadbuffer); tp.close(); 影响词的评分 在Similarity抽象类中有函数public float scorepayload(byte [] payload, int offset, int length) 可以根据payload的值影响评分 读取域元数据信息的代码如下 FieldInfos.read(IndexInput, String) int firstint = input.readvint(); size = input.readvint(); for (int i = 0; i < size; i++) String name = input.readstring(); byte bits = input.readbyte(); boolean isindexed = (bits & IS_INDEXED)!= 0; boolean storetermvector = (bits & STORE_TERMVECTOR)!= 0; 第 53 / 199 页

54 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) boolean storepositionswithtermvector = (bits & STORE_POSITIONS_WITH_TERMVECTOR)!= 0; boolean storeoffsetwithtermvector = (bits & STORE_OFFSET_WITH_TERMVECTOR)!= 0; boolean omitnorms = (bits & OMIT_NORMS)!= 0; boolean storepayloads = (bits & STORE_PAYLOADS)!= 0; boolean omittermfreqandpositions = (bits & OMIT_TERM_FREQ_AND_POSITIONS)!= 0; 域(Field)的数据信息(.fdt.fdx) 域数据文件(fdt): 真正保存存储域(stored field)信息的是fdt文件 在一个段(segment)中总共有segment size篇文档 所以fdt文件中共有segment size个项 每 一项保存一篇文档的域的信息 对于每一篇文档 一开始是一个fieldcount 也即此文档包含的域的数目 接下来是fieldcount 个项 每一项保存一个域的信息 第 54 / 199 页

55 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 对于每一个域 fieldnum是域号 接着是一个8位的byte 最低一位表示此域是否分词 (tokenized) 倒数第二位表示此域是保存字符串数据还是二进制数据 倒数第三位表示此域是 否被压缩 再接下来就是存储域的值 比如new Field("title", "lucene in action", Field.Store.Yes, ) 则此处存放的就是"lucene in action"这个字符串 域索引文件(fdx) 由域数据文件格式我们知道 每篇文档包含的域的个数 每个存储域的值都是不一样的 因而 域数据文件中segment size篇文档 每篇文档占用的大小也是不一样的 那么如何在fdt中辨别 每一篇文档的起始地址和终止地址呢 如何能够更快的找到第n篇文档的存储域的信息呢 就是 要借助域索引文件 域索引文件也总共有segment size个项 每篇文档都有一个项 每一项都是一个long 大小固 定 每一项都是对应的文档在fdt文件中的起始地址的偏移量 这样如果我们想找到第n篇文档 的存储域的信息 只要在fdx中找到第n项 然后按照取出的long作为偏移量 就可以在fdt文件 中找到对应的存储域的信息 读取域数据信息的代码如下 Document FieldsReader.doc(int n, FieldSelector fieldselector) long position = indexstream.readlong();//indexstream points to ".fdx" fieldsstream.seek(position);//fieldsstream points to "fdt" int numfields = fieldsstream.readvint(); for (int i = 0; i < numfields; i++) int fieldnumber = fieldsstream.readvint(); byte bits = fieldsstream.readbyte(); boolean compressed = (bits & FieldsWriter.FIELD_IS_COMPRESSED)!= 0; boolean tokenize = (bits & FieldsWriter.FIELD_IS_TOKENIZED)!= 0; boolean binary = (bits & FieldsWriter.FIELD_IS_BINARY)!= 0; if (binary) int toread = fieldsstream.readvint(); final byte[] b = new byte[toread]; fieldsstream.readbytes(b, 0, b.length); if (compressed) int toread = fieldsstream.readvint(); final byte[] b = new byte[toread]; fieldsstream.readbytes(b, 0, b.length); uncompress(b), else fieldsstream.readstring() 第 55 / 199 页

56 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 词向量(Term Vector)的数据信息(.tvx.tvd.tvf) 词向量信息是从索引(index)到文档(document)到域(field)到词(term)的正向信息 有了词向量信息 我们就可 以得到一篇文档包含那些词的信息 词向量索引文件(tvx) 一个段(segment)包含N篇文档 此文件就有N项 每一项代表一篇文档 每一项包含两部分信息 第一部分是词向量文档文件(tvd)中此文档的偏移量 第二部分是词向 量域文件(tvf)中此文档的第一个域的偏移量 词向量文档文件(tvd) 一个段(segment)包含N篇文档 此文件就有N项 每一项包含了此文档的所有的域的信息 第 56 / 199 页

57 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) 每一项首先是此文档包含的域的个数NumFields 然后是一个NumFields大小的数组 数组的 每一项是域号 然后是一个(NumFields - 1)大小的数组 由前面我们知道 每篇文档的第一个 域在tvf中的偏移量在tvx文件中保存 而其他(NumFields - 1)个域在tvf中的偏移量就是第一个 域的偏移量加上这(NumFields - 1)个数组的每一项的值 词向量域文件(tvf) 此文件包含了此段中的所有的域 并不对文档做区分 到底第几个域到第几个域是属于那篇文 档 是由tvx中的第一个域的偏移量以及tvd中的(NumFields - 1)个域的偏移量来决定的 对于每一个域 首先是此域包含的词的个数NumTerms 然后是一个8位的byte 最后一位是 指定是否保存位置信息 倒数第二位是指定是否保存偏移量信息 然后是NumTerms个项的数 组 每一项代表一个词(Term) 对于每一个词 由词的文本TermText 词频TermFreq(也即此 词在此文档中出现的次数) 词的位置信息 词的偏移量信息 读取词向量数据信息的代码如下 TermVectorsReader.get(int docnum, String field, TermVectorMapper) int fieldnumber = fieldinfos.fieldnumber(field);//通过field名字得到field号 seektvx(docnum);//在tvx文件中按docnum文档号找到相应文档的项 long tvdposition = tvx.readlong();//找到tvd文件中相应文档的偏移量 tvd.seek(tvdposition);//在tvd文件中按偏移量找到相应文档的项 int fieldcount = tvd.readvint();//此文档包含的域的个数 for (int i = 0; i < fieldcount; i++) //按域号查找域 number = tvd.readvint(); if (number == fieldnumber) found = i; position = tvx.readlong();//在tvx中读出此文档的第一个域在tvf中的偏移量 for (int i = 1; i <= found; i++) position += tvd.readvlong();//加上所要找的域在tvf中的偏移量 tvf.seek(position); int numterms = tvf.readvint(); byte bits = tvf.readbyte(); storepositions = (bits & STORE_POSITIONS_WITH_TERMVECTOR)!= 0; storeoffsets = (bits & STORE_OFFSET_WITH_TERMVECTOR)!= 0; for (int i = 0; i < numterms; i++) start = tvf.readvint(); deltalength = tvf.readvint(); totallength = start + deltalength; tvf.readbytes(bytebuffer, start, deltalength); term = new String(byteBuffer, 0, totallength, "UTF-8"); 第 57 / 199 页

58 1.4 Lucene学习总结之三 Lucene的索引文件格式 (2) if (storepositions) positions = new int[freq]; int prevposition = 0; for (int j = 0; j < freq; j++) positions[j] = prevposition + tvf.readvint(); prevposition = positions[j]; if (storeoffsets) offsets = new TermVectorOffsetInfo[freq]; int prevoffset = 0; for (int j = 0; j < freq; j++) int startoffset = prevoffset + tvf.readvint(); int endoffset = startoffset + tvf.readvint(); offsets[j] = new TermVectorOffsetInfo(startOffset, endoffset); prevoffset = endoffset; 第 58 / 199 页

59 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 发表时间: 本文在csdn中的位置 四 具体格式 4.2. 反向信息 反向信息是索引文件的核心 也即反向索引 反向索引包括两部分 左面是词典(Term Dictionary) 右面是倒排表(Posting List) 在Lucene中 这两部分是分文件存储的 词典是存储在tii tis中的 倒排表又包括两部分 一部分是文档号及 词频 保存在frq中 一部分是词的位置信息 保存在prx中 Term Dictionary (tii, tis) > Frequencies (.frq) > Positions (.prx) 词典(tis)及词典索引(tii)信息 在词典中 所有的词是按照字典顺序排序的 词典文件(tis) 第 59 / 199 页

60 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) TermCount 词典中包含的总的词数 IndexInterval 为了加快对词的查找速度 也应用类似跳跃表的结构 假设IndexInterval为 4 则在词典索引(tii)文件中保存第4个 第8个 第12个词 这样可以加快在词典文件中查找词 的速度 SkipInterval 倒排表无论是文档号及词频 还是位置信息 都是以跳跃表的结构存在的 SkipInterval是跳跃的步数 MaxSkipLevels 跳跃表是多层的 这个值指的是跳跃表的最大层数 TermCount个项的数组 每一项代表一个词 对于每一个词 以前缀后缀规则存放词的文本信 息(PrefixLength + Suffix) 词属于的域的域号(FieldNum) 有多少篇文档包含此词 (DocFreq) 此词的倒排表在frq prx中的偏移量(freqdelta, ProxDelta) 此词的倒排表的跳 跃表在frq中的偏移量(SkipDelta) 这里之所以用Delta 是应用差值规则 词典索引文件(tii) 词典索引文件是为了加快对词典文件中词的查找速度 保存每隔IndexInterval个词 词典索引文件是会被全部加载到内存中去的 IndexTermCount = TermCount / IndexInterval 词典索引文件中包含的词数 IndexInterval同词典文件中的IndexInterval SkipInterval同词典文件中的SkipInterval MaxSkipLevels同词典文件中的MaxSkipLevels IndexTermCount个项的数组 每一项代表一个词 每一项包括两部分 第一部分是词本身 (TermInfo) 第二部分是在词典文件中的偏移量(IndexDelta) 假设IndexInterval为4 此数组 中保存第4个 第8个 第12个词 读取词典及词典索引文件的代码如下 origenum = new SegmentTermEnum(directory.openInput(segment + "." + IndexFileNames.TERMS_EXTENSION,readBufferSize), fieldinfos, false);//用于读取tis文件 int firstint = input.readint(); size = input.readlong(); indexinterval = input.readint(); skipinterval = input.readint(); maxskiplevels = input.readint(); SegmentTermEnum indexenum = new SegmentTermEnum(directory.openInput(segment + "." + IndexFileNames.TERMS_INDEX_EXTENSION, readbuffersize), fieldinfos, true);//用于 读取tii文件 indexterms = new Term[indexSize]; indexinfos = new TermInfo[indexSize]; indexpointers = new long[indexsize]; 第 60 / 199 页

61 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) for (int i = 0; indexenum.next(); i++) indexterms[i] = indexenum.term(); indexinfos[i] = indexenum.terminfo(); indexpointers[i] = indexenum.indexpointer; 文档号及词频(frq)信息 文档号及词频文件里面保存的是倒排表 是以跳跃表形式存在的 第 61 / 199 页

62 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 此文件包含TermCount个项 每一个词都有一项 因为每一个词都有自己的倒排表 对于每一个词的倒排表都包括两部分 一部分是倒排表本身 也即一个数组的文档号及词频 另一部分 是跳跃表 为了更快的访问和定位倒排表中文档号及词频的位置 对于文档号和词频的存储应用的是差值规则和或然跟随规则 Lucene的文档本身有以下几句话 比较 难以理解 在此解释一下 For example, the TermFreqs for a term which occurs once in document seven and three times in document eleven, with omittf false, would be the following sequence of VInts: 15, 8, 3 If omittf were true it would be this sequence of VInts instead: 7,4 首先我们看omitTf=false的情况 也即我们在索引中会存储一个文档中term出现的次数 例子中说了 表示在文档7中出现1次 并且又在文档11中出现3次的文档用以下序列表示 那这三个数字是怎么计算出来的呢 首先 根据定义TermFreq --> DocDelta[, Freq?] 一个TermFreq结构是由一个DocDelta后面或 许跟着Freq组成 也即上面我们说的A+B 结构 DocDelta自然是想存储包含此Term的文档的ID号了 Freq是在此文档中出现的次数 所以根据例子 应该存储的完整信息为[DocID = 7, Freq = 1] [DocID = 11, Freq = 3](见全文检 索的基本原理章节) 然而为了节省空间 Lucene对编号此类的数据都是用差值来表示的 也即上面说的规则2 Delta 规则 于是文档ID就不能按完整信息存了 就应该存放如下 [DocIDDelta = 7, Freq = 1][DocIDDelta = 4 (11-7), Freq = 3] 然而Lucene对于A+B?这种或然跟随的结果 有其特殊的存储方式 见规则3 即A+B?规则 如果 DocDelta后面跟随的Freq为1 则用DocDelta最后一位置1表示 如果DocDelta后面跟随的Freq大于1 则DocDelta得最后一位置0 然后后面跟随真正的值 从而 对于第一个Term 由于Freq为1 于是放在DocDelta的最后一位表示 DocIDDelta = 7的二进制 是 必须要左移一位 且最后一位置一 = 15 对于第二个Term 由于Freq 第 62 / 199 页

63 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 大于一 于是放在DocDelta的最后一位置零 DocIDDelta = 4的二进制是 必须要左 移一位 且最后一位置零 = 8 然后后面跟随真正的Freq = 3 于是得到序列 [DocDleta = 15][DocDelta = 8, Freq = 3] 也即序列 如果omitTf=true 也即我们不在索引中存储一个文档中Term出现的次数 则只存DocID就可以 了 因而不存在A+B?规则的应用 [DocID = 7][DocID = 11] 然后应用规则2 Delta规则 于是得到序列[DocDelta = 7][DocDelta = 4 (11-7)] 也即序列 7 4. 对于跳跃表的存储有以下几点需要解释一下 跳跃表可根据倒排表本身的长度(DocFreq)和跳跃的幅度(SkipInterval)而分不同的层次 层次 数为NumSkipLevels = Min(MaxSkipLevels, floor(log(docfreq/log(skipinterval)))). 第Level层的节点数为DocFreq/(SkipInterval^(Level + 1)) level从零计数 除了最高层之外 其他层都有SkipLevelLength来表示此层的二进制长度(而非节点的个数) 方 便读取某一层的跳跃表到缓存里面 低层在前 高层在后 当读完所有的低层后 剩下的就是最后一层 因而最后一层不需要 SkipLevelLength 这也是为什么Lucene文档中的格式描述为 NumSkipLevels-1, SkipLevel 也 即低NumSKipLevels-1层有SkipLevelLength 最后一层只有SkipLevel 没有 SkipLevelLength 除最低层以外 其他层都有SkipChildLevelPointer来指向下一层相应的节点 每一个跳跃节点包含以下信息 文档号 payload的长度 文档号对应的倒排表中的节点在frq 中的偏移量 文档号对应的倒排表中的节点在prx中的偏移量 虽然Lucene的文档中有以下的描述 然而实验的结果却不是完全准确的 第 63 / 199 页

64 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) Example: SkipInterval = 4, MaxSkipLevels = 2, DocFreq = 35. Then skip level 0 has 8 rd th th th th rd th st SkipData entries, containing the 3, 7, 11, 15, 19, 23, 27, and 31 document numbers in TermFreqs. Skip level 1 has 2 SkipData entries, containing the 15 th and 31 st document numbers in TermFreqs. 按照描述 当SkipInterval为4 且有35篇文档的时候 Skip level = 0应该包括第3 第7 第11 第15 第19 第23 第27 第31篇文档 Skip level = 1应该包括第15 第31篇文档 然而真正的实现中 跳跃表节点的时候 却向前偏移了 偏移的原因在于下面的代码 FormatPostingsDocsWriter.addDoc(int docid, int termdocfreq) final int delta = docid - lastdocid; if ((++df % skipinterval) == 0) skiplistwriter.setskipdata(lastdocid, storepayloads, poswriter.lastpayloadlength); skiplistwriter.bufferskip(df); 从代码中 我们可以看出 当SkipInterval为4的时候 当docID = 0时 ++df为1 1%4不为0 不是跳跃节点 当docID = 3时 ++df=4 4%4为0 为跳跃节点 然而skipData里面保存的却 是lastDocID为2 所以真正的倒排表和跳跃表中保存一下的信息 第 64 / 199 页

65 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 词位置(prx)信息 词位置信息也是倒排表 也是以跳跃表形式存在的 此文件包含TermCount个项 每一个词都有一项 因为每一个词都有自己的词位置倒排表 第 65 / 199 页

66 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 对于每一个词的都有一个DocFreq大小的数组 每项代表一篇文档 记录此文档中此词出现的位置 这 个文档数组也是和frq文件中的跳跃表有关系的 从上面我们知道 在frq的跳跃表节点中有ProxSkip 当SkipInterval为3的时候 frq的跳跃表节点指向prx文件中的此数组中的第1 第4 第7 第10 第 13 第16篇文档 对于每一篇文档 可能包含一个词多次 因而有一个Freq大小的数组 每一项代表此词在此文档中出现 一次 则有一个位置信息 每一个位置信息包含 PositionDelta(采用差值规则) 还可以保存payload 应用或然跟随规则 4.3. 其他信息 标准化因子文件(nrm) 为什么会有标准化因子呢 从第一章中的描述 我们知道 在搜索过程中 搜索出的文档要按与查询语句的相 关性排序 相关性大的打分(score)高 从而排在前面 相关性打分(score)使用向量空间模型(Vector Space Model) 在计算相关性之前 要计算Term Weight 也即某Term相对于某Document的重要性 在计算Term Weight时 主要有两个影响因素 一个是此Term在此文档中出现的次数 一个是此Term的普通程度 显然此 Term在此文档中出现的次数越多 此Term在此文档中越重要 这种Term Weight的计算方法是最普通的 然而存在以下几个问题 不同的文档重要性不同 有的文档重要些 有的文档相对不重要 比如对于做软件的 在索引书籍的时 候 我想让计算机方面的书更容易搜到 而文学方面的书籍搜索时排名靠后 不同的域重要性不同 有的域重要一些 如关键字 如标题 有的域不重要一些 如附件等 同样一个 词(Term) 出现在关键字中应该比出现在附件中打分要高 根据词(Term)在文档中出现的绝对次数来决定此词对文档的重要性 有不合理的地方 比如长的文档词 在文档中出现的次数相对较多 这样短的文档比较吃亏 比如一个词在一本砖头书中出现了10次 在另 外一篇不足100字的文章中出现了9次 就说明砖头书应该排在前面码 不应该 显然此词在不足100字 的文章中能出现9次 可见其对此文章的重要性 由于以上原因 Lucene在计算Term Weight时 都会乘上一个标准化因子(Normalization Factor) 来减少上 面三个问题的影响 标准化因子(Normalization Factor)是会影响随后打分(score)的计算的 Lucene的打分计算一部分发生在索引 过程中 一般是与查询语句无关的参数如标准化因子 大部分发生在搜索过程中 会在搜索过程的代码分析中 详述 标准化因子(Normalization Factor)在索引过程总的计算如下 第 66 / 199 页

67 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 它包括三个参数 Document boost 此值越大 说明此文档越重要 Field boost 此域越大 说明此域越重要 lengthnorm(field) = (1.0 / Math.sqrt(numTerms)) 一个域中包含的Term总数越多 也即文档越 长 此值越小 文档越短 此值越大 从上面的公式 我们知道 一个词(Term)出现在不同的文档或不同的域中 标准化因子不同 比如有两个文 档 每个文档有两个域 如果不考虑文档长度 就有四种排列组合 在重要文档的重要域中 在重要文档的非 重要域中 在非重要文档的重要域中 在非重要文档的非重要域中 四种组合 每种有不同的标准化因子 于是在Lucene中 标准化因子共保存了(文档数目乘以域数目)个 格式如下 标准化因子文件(Normalization Factor File: nrm) NormsHeader 字符串 NRM 外加Version 依Lucene的版本的不同而不同 接着是一个数组 大小为NumFields 每个Field一项 每一项为一个Norms Norms也是一个数组 大小为SegSize 即此段中文档的数量 每一项为一个Byte 表示一个 浮点数 其中0~2为尾数 3~8为指数 第 67 / 199 页

68 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 删除文档文件(del) 被删除文档文件(Deleted Document File:.del) Format 在此文件中 Bits和DGaps只能保存其中之一 -1表示保存DGaps 非负值表示保存 Bits ByteCount 此段中有多少文档 就有多少个bit被保存 但是以byte形式计数 也即Bits的大 小应该是byte的倍数 BitCount Bits中有多少位被至1 表示此文档已经被删除 Bits 一个数组的byte 大小为ByteCount 应用时被认为是byte*8个bit DGaps 如果删除的文档数量很小 则Bits大部分位为0 很浪费空间 DGaps采用以下的方式 来保存稀疏数组 比如第十 十二 三十二个文档被删除 于是第十 十二 三十二位设为1 DGaps也是以byte为单位的 仅保存不为0的byte 如第1个byte 第4个byte 第1个byte十 第 68 / 199 页

69 1.5 Lucene学习总结之三 Lucene的索引文件格式 (3) 进制为20 第4个byte十进制为1 于是保存成DGaps 第1个byte 位置1用不定长正整数保 存 值为20用二进制保存 第2个byte 位置4用不定长正整数保存 用差值为3 值为1用二进 制保存 二进制数据不用差值表示 五 总体结构 图示为Lucene索引文件的整体结构 属于整个索引(Index)的segment.gen segment_n 其保存的是段(segment)的元数据信息 然后分多个segment保存数据信息 同一个segment有相同的前缀文件名 对于每一个段 包含域信息 词信息 以及其他信息(标准化因子 删除文档) 域信息也包括域的元数据信息 在fnm中 域的数据信息 在fdx fdt中 词信息是反向信息 包括词典(tis, tii) 文档号及词频倒排表(frq) 词位置倒排表(prx) 大家可以通过看源代码 相应的Reader和Writer来了解文件结构 将更为透彻 第 69 / 199 页

70 1.6 Lucene学习总结之四 Lucene索引过程分析(1) 1.6 Lucene学习总结之四 Lucene索引过程分析(1) 发表时间: 对于Lucene的索引过程 除了将词(Term)写入倒排表并最终写入Lucene的索引文件外 还包括分词(Analyzer) 和合并段(merge segments)的过程 本次不包括这两部分 将在以后的文章中进行分析 Lucene的索引过程 很多的博客 文章都有介绍 推荐大家上网搜一篇文章 Annotated Lucene 好像 中文名称叫 Lucene源码剖析 是很不错的 想要真正了解Lucene索引文件过程 最好的办法是跟进代码调试 对着文章看代码 这样不但能够最详细准确 的掌握索引过程(描述都是有偏差的 而代码是不会骗你的) 而且还能够学习Lucene的一些优秀的实现 能够 在以后的工作中为我所用 毕竟Lucene是比较优秀的开源项目之一 由于Lucene已经升级到3.0.0了 本索引过程为Lucene 3.0.0的索引过程 一 索引过程体系结构 Lucene 3.0的搜索要经历一个十分复杂的过程 各种信息分散在不同的对象中分析 处理 写入 为了支持多 线程 每个线程都创建了一系列类似结构的对象集 为了提高效率 要复用一些对象集 这使得索引过程更加 复杂 其实索引过程 就是经历下图中所示的索引链的过程 索引链中的每个节点 负责索引文档的不同部分的信息 当经历完所有的索引链的时候 文档就处理完毕了 最初的索引链 我们称之基本索引链 为了支持多线程 使得多个线程能够并发处理文档 因而每个线程都要建立自己的索引链体系 使得每个线程 能够独立工作 在基本索引链基础上建立起来的每个线程独立的索引链体系 我们称之线程索引链 线程索引 链的每个节点是由基本索引链中的相应的节点调用函数addThreads创建的 为了提高效率 考虑到对相同域的处理有相似的过程 应用的缓存也大致相当 因而不必每个线程在处理每一 篇文档的时候都重新创建一系列对象 而是复用这些对象 所以对每个域也建立了自己的索引链体系 我们称 之域索引链 域索引链的每个节点是由线程索引链中的相应的节点调用addFields创建的 当完成对文档的处理后 各部分信息都要写到索引文件中 写入索引文件的过程是同步的 不是多线程的 也 是沿着基本索引链将各部分信息依次写入索引文件的 下面详细分析这一过程 第 70 / 199 页

71 二 详细索引过程 1 创建IndexWriter对象 代码 第 71 / 199 页 1.6 Lucene学习总结之四 Lucene索引过程分析(1)

72 1.6 Lucene学习总结之四 Lucene索引过程分析(1) IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); IndexWriter对象主要包含以下几方面的信息 用于索引文档 Directory directory; 指向索引文件夹 Analyzer analyzer; 分词器 Similarity similarity = Similarity.getDefault(); 影响打分的标准化因子(normalization factor) 部分 对文档的打分分两个部分 一部分是索引阶段计算的 与查询语句无关 一部分是搜索 阶段计算的 与查询语句相关 SegmentInfos segmentinfos = new SegmentInfos(); 保存段信息 大家会发现 和 segments_n中的信息几乎一一对应 IndexFileDeleter deleter; 此对象不是用来删除文档的 而是用来管理索引文件的 Lock writelock; 每一个索引文件夹只能打开一个IndexWriter 所以需要锁 Set segmentstooptimize = new HashSet(); 保存正在最优化(optimize)的段信息 当调用 optimize的时候 当前所有的段信息加入此Set 此后新生成的段并不参与此次最优化 用于合并段 在合并段的文章中将详细描述 SegmentInfos localrollbacksegmentinfos; HashSet mergingsegments = new HashSet(); MergePolicy mergepolicy = new LogByteSizeMergePolicy(this); MergeScheduler mergescheduler = new ConcurrentMergeScheduler(); LinkedList pendingmerges = new LinkedList(); Set runningmerges = new HashSet(); List mergeexceptions = new ArrayList(); long mergegen; 为保持索引完整性 一致性和事务性 SegmentInfos rollbacksegmentinfos; 当IndexWriter对索引进行了添加 删除文档操作后 可以调用commit将修改提交到文件中去 也可以调用rollback取消从上次commit到此时的修 改 SegmentInfos localrollbacksegmentinfos; 此段信息主要用于将其他的索引文件夹合并到此 索引文件夹的时候 为防止合并到一半出错可回滚所保存的原来的段信息 一些配置 long writelocktimeout; 获得锁的时间超时 当超时的时候 说明此索引文件夹已经被另一个 IndexWriter打开了 int termindexinterval; 同tii和tis文件中的indexInterval 第 72 / 199 页

73 1.6 Lucene学习总结之四 Lucene索引过程分析(1) 有关SegmentInfos对象所保存的信息 当索引文件夹如下的时候 SegmentInfos对象如下表 segmentinfos SegmentInfos (id=37) capacityincrement counter 3 elementcount 3 elementdata [0] 0 Object[10] (id=68) SegmentInfo (id=166) delcount delgen 0-1 diagnostics dir HashMap (id=170) SimpleFSDirectory (id=171) doccount 2 docstoreiscompoundfile docstoreoffset -1 docstoresegment files null ArrayList (id=173) hasprox true hassinglenormfile iscompoundfile name null prelockless false sizeinbytes 635 SegmentInfo (id=168) delcount 第 73 / 199 页 1 "_0" normgen [1] true 0 false

74 1.6 Lucene学习总结之四 Lucene索引过程分析(1) delgen -1 diagnostics dir HashMap (id=177) SimpleFSDirectory (id=171) doccount 2 docstoreiscompoundfile docstoreoffset -1 docstoresegment files null ArrayList (id=178) hasprox true hassinglenormfile iscompoundfile name true 1 "_1" normgen null prelockless false sizeinbytes 635 [2] SegmentInfo (id=169) delcount delgen 0-1 diagnostics dir HashMap (id=180) SimpleFSDirectory (id=171) doccount 2 docstoreiscompoundfile docstoreoffset -1 docstoresegment files null ArrayList (id=214) hasprox true hassinglenormfile iscompoundfile name 1 null prelockless false sizeinbytes 635 generation 4 lastgeneration modcount true "_2" normgen 4 3 pendingsegnoutput 第 74 / 199 页 false null false

75 userdata version 1.6 Lucene学习总结之四 Lucene索引过程分析(1) HashMap (id=146) 有关IndexFileDeleter 其不是用来删除文档的 而是用来管理索引文件的 在对文档的添加 删除 对段的合并的处理过程中 会生成很多新的文件 并需要删除老的文件 因而 需要管理 然而要被删除的文件又可能在被用 因而要保存一个引用计数 仅仅当引用计数为零的时候 才执行删 除 下面这个例子能很好的说明IndexFileDeleter如何对文件引用计数并进行添加和删除的 (1) 创建IndexWriter时 IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setmergefactor(3); 索引文件夹如下 引用计数如下 refcounts size HashMap (id=101) 1 table [8] HashMap$Entry[16] (id=105) HashMap$Entry (id=110) key value "segments_1" IndexFileDeleter$RefCount (id=38) count 1 (2) 添加第一个段时 第 75 / 199 页

76 indexdocs(writer, docdir); writer.commit(); 首先生成的不是compound文件 因而引用计数如下 refcounts size HashMap (id=101) 9 table [1] HashMap$Entry[16] (id=105) HashMap$Entry (id=129) key value "_0.tis" IndexFileDeleter$RefCount (id=138) count [3] HashMap$Entry (id=130) key value "_0.fnm" IndexFileDeleter$RefCount (id=141) count [4] value "_0.tii" IndexFileDeleter$RefCount (id=142) count 1 HashMap$Entry (id=135) key value "_0.frq" IndexFileDeleter$RefCount (id=143) count 第 76 / 199 页 1 HashMap$Entry (id=134) key [8] Lucene学习总结之四 Lucene索引过程分析(1)

77 [10] 1.6 Lucene学习总结之四 Lucene索引过程分析(1) HashMap$Entry (id=136) key "_0.fdx" value IndexFileDeleter$RefCount (id=144) count [13] 1 HashMap$Entry (id=139) key "_0.prx" value IndexFileDeleter$RefCount (id=145) count [14] 1 HashMap$Entry (id=140) key "_0.fdt" value IndexFileDeleter$RefCount (id=146) count 1 然后会合并成compound文件 并加入引用计数 refcounts size HashMap (id=101) 10 table [1] HashMap$Entry[16] (id=105) HashMap$Entry (id=129) key value "_0.tis" IndexFileDeleter$RefCount (id=138) count [2] HashMap$Entry (id=154) key value 第 77 / 199 页 1 "_0.cfs" IndexFileDeleter$RefCount (id=155)

78 count [3] 1.6 Lucene学习总结之四 Lucene索引过程分析(1) 1 HashMap$Entry (id=130) key "_0.fnm" value IndexFileDeleter$RefCount (id=141) count [4] 1 HashMap$Entry (id=134) key "_0.tii" value IndexFileDeleter$RefCount (id=142) count [8] 1 HashMap$Entry (id=135) key "_0.frq" value IndexFileDeleter$RefCount (id=143) count [10] 1 HashMap$Entry (id=136) key "_0.fdx" value IndexFileDeleter$RefCount (id=144) count [13] 1 HashMap$Entry (id=139) key "_0.prx" value IndexFileDeleter$RefCount (id=145) count [14] 1 HashMap$Entry (id=140) key "_0.fdt" value IndexFileDeleter$RefCount (id=146) count 1 然后会用IndexFileDeleter.decRef()来删除[_0.nrm, _0.tis, _0.fnm, _0.tii, _0.frq, _0.fdx, _0.prx, _0.fdt]文件 refcounts size 第 78 / 199 页 2 HashMap (id=101)

79 table HashMap$Entry[16] (id=105) [2] HashMap$Entry (id=154) key "_0.cfs" value IndexFileDeleter$RefCount (id=155) count [8] 1 HashMap$Entry (id=110) key "segments_1" value IndexFileDeleter$RefCount (id=38) count 1 然后为建立新的segments_2 refcounts size HashMap (id=77) 3 table [2] HashMap$Entry[16] (id=84) HashMap$Entry (id=87) key value "_0.cfs" IndexFileDeleter$RefCount (id=91) count [8] HashMap$Entry (id=89) key value "segments_1" IndexFileDeleter$RefCount (id=62) count [9] 3 0 HashMap$Entry (id=90) key next value "segments_2" null IndexFileDeleter$RefCount (id=93) count 1 然后IndexFileDeleter.decRef() 删除segments_1文件 第 79 / 199 页 1.6 Lucene学习总结之四 Lucene索引过程分析(1)

80 refcounts size 1.6 Lucene学习总结之四 Lucene索引过程分析(1) HashMap (id=77) 2 table [2] HashMap$Entry[16] (id=84) HashMap$Entry (id=87) key value "_0.cfs" IndexFileDeleter$RefCount (id=91) count [9] 2 HashMap$Entry (id=90) key value "segments_2" IndexFileDeleter$RefCount (id=93) count 1 (3) 添加第二个段 indexdocs(writer, docdir); writer.commit(); (4) 添加第三个段 由于MergeFactor为3 则会进行一次段合并 indexdocs(writer, docdir); writer.commit(); 首先和其他的段一样 生成_2.cfs以及segments_4 第 80 / 199 页

81 1.6 Lucene学习总结之四 Lucene索引过程分析(1) 同时创建了一个线程来进行背后进行段合并(ConcurrentMergeScheduler$MergeThread.run()) 这时候的引用计数如下 refcounts size HashMap (id=84) 5 table [2] HashMap$Entry[16] (id=98) HashMap$Entry (id=112) key "_0.cfs" value IndexFileDeleter$RefCount (id=117) count [4] HashMap$Entry (id=113) key "_3.cfs" value IndexFileDeleter$RefCount (id=118) count [12] "_1.cfs" value IndexFileDeleter$RefCount (id=119) count 第 81 / 199 页 1 HashMap$Entry (id=114) key [13] 1 1 HashMap$Entry (id=115)

82 key "_2.cfs" value IndexFileDeleter$RefCount (id=120) count [15] 1.6 Lucene学习总结之四 Lucene索引过程分析(1) 1 HashMap$Entry (id=116) key "segments_4" value IndexFileDeleter$RefCount (id=121) count 1 (5) 关闭writer writer.close(); 通过IndexFileDeleter.decRef()删除被合并的段 有关SimpleFSLock进行JVM之间的同步 有时候 我们写java程序的时候 也需要不同的JVM之间进行同步 来保护一个整个系统中唯一的资 源 如果唯一的资源仅仅在一个进程中 则可以使用线程同步的机制 然而如果唯一的资源要被多个进程进行访问 则需要进程间同步的机制 无论是Windows和Linux在操 作系统层面都有很多的进程间同步的机制 但进程间的同步却不是Java的特长 Lucene的SimpleFSLock给我们提供了一种方式 Lock的抽象类 public abstract class Lock { public static long LOCK_POLL_INTERVAL = 1000; public static final long LOCK_OBTAIN_WAIT_FOREVER = -1; public abstract boolean obtain() throws IOException; public boolean obtain(long lockwaittimeout) throws LockObtainFailedException, IOException { boolean locked = obtain(); 第 82 / 199 页

83 1.6 Lucene学习总结之四 Lucene索引过程分析(1) if (lockwaittimeout < 0 && lockwaittimeout!= LOCK_OBTAIN_WAIT_FOREVER) throw new IllegalArgumentException("..."); long maxsleepcount = lockwaittimeout / LOCK_POLL_INTERVAL; long sleepcount = 0; while (!locked) { if (lockwaittimeout!= LOCK_OBTAIN_WAIT_FOREVER && sleepcount++ >= maxsleepcount) { throw new LockObtainFailedException("Lock obtain timed out."); try { Thread.sleep(LOCK_POLL_INTERVAL); catch (InterruptedException ie) { throw new ThreadInterruptedException(ie); locked = obtain(); return locked; public abstract void release() throws IOException; public abstract boolean islocked() throws IOException; LockFactory的抽象类 public abstract class LockFactory { public abstract Lock makelock(string lockname); abstract public void clearlock(string lockname) throws IOException; SimpleFSLock的实现类 class SimpleFSLock extends Lock { 第 83 / 199 页

84 1.6 Lucene学习总结之四 Lucene索引过程分析(1) File lockfile; File lockdir; public SimpleFSLock(File lockdir, String lockfilename) { this.lockdir = lockdir; lockfile = new File(lockDir, public boolean obtain() throws IOException { if (!lockdir.exists()) { if (!lockdir.mkdirs()) throw new IOException("Cannot create directory: " + lockdir.getabsolutepath()); else if (!lockdir.isdirectory()) { throw new IOException("Found regular file where directory expected: " + lockdir.getabsolutepath()); return public void release() throws LockReleaseFailedException { if (lockfile.exists() &&!lockfile.delete()) throw new LockReleaseFailedException("failed to delete " + public boolean islocked() { return lockfile.exists(); 第 84 / 199 页

85 1.6 Lucene学习总结之四 Lucene索引过程分析(1) SimpleFSLockFactory的实现类 public class SimpleFSLockFactory extends FSLockFactory { public SimpleFSLockFactory(String lockdirname) throws IOException { setlockdir(new public Lock makelock(string lockname) { if (lockprefix!= null) { lockname = lockprefix + "-" + lockname; return new SimpleFSLock(lockDir, public void clearlock(string lockname) throws IOException { if (lockdir.exists()) { if (lockprefix!= null) { lockname = lockprefix + "-" + lockname; File lockfile = new File(lockDir, lockname); if (lockfile.exists() &&!lockfile.delete()) { throw new IOException("Cannot delete " + lockfile); 第 85 / 199 页

86 1.6 Lucene学习总结之四 Lucene索引过程分析(1) ; 2 创建文档Document对象 并加入域(Field) 代码 Document doc = new Document(); doc.add(new Field("path", f.getpath(), Field.Store.YES, Field.Index.NOT_ANALYZED)); doc.add(new Field("modified",DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.NOT_ANALYZED)); doc.add(new Field("contents", new FileReader(f))); Document对象主要包括以下部分 此文档的boost 默认为1 大于一说明比一般的文档更加重要 小于一说明更不重要 一个ArrayList保存此文档所有的域 每一个域包括域名 域值 和一些标志位 和fnm fdx fdt中的描述相对应 doc Document (id=42) boost 1.0 fields ArrayList (id=44) elementdata [0] Object[10] (id=46) Field (id=48) binarylength 0 binaryoffset boost 1.0 fieldsdata isbinary "exampledocs\\file01.txt" false isindexed isstored true true istokenized 第 86 / 199 页 0 false

87 1.6 Lucene学习总结之四 Lucene索引过程分析(1) lazy false name "path" omitnorms false omittermfreqandpositions false storeoffsetwithtermvector false storepositionwithtermvector storetermvector tokenstream [1] false false null Field (id=50) binarylength 0 binaryoffset boost fieldsdata isbinary " " false isindexed isstored true true istokenized lazy false false name "modified" omitnorms false omittermfreqandpositions false storeoffsetwithtermvector false storepositionwithtermvector storetermvector tokenstream [2] null Field (id=52) binarylength 0 binaryoffset boost isbinary FileReader (id=58) false isindexed isstored true false istokenized lazy name fieldsdata 第 87 / 199 页 false true false "contents" false

88 1.6 Lucene学习总结之四 Lucene索引过程分析(1) omitnorms false omittermfreqandpositions false storeoffsetwithtermvector false storepositionwithtermvector storetermvector tokenstream modcount size 第 88 / 199 页 3 3 null false false

89 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 发表时间: 将文档加入IndexWriter 代码 writer.adddocument(doc); -->IndexWriter.addDocument(Document doc, Analyzer analyzer) -->doflush = docwriter.adddocument(doc, analyzer); --> DocumentsWriter.updateDocument(Document, Analyzer, Term) 注 --> 代表一级函数调用 IndexWriter继而调用DocumentsWriter.addDocument 其又调用DocumentsWriter.updateDocument 4 将文档加入DocumentsWriter 代码 DocumentsWriter.updateDocument(Document doc, Analyzer analyzer, Term delterm) -->(1) DocumentsWriterThreadState state = getthreadstate(doc, delterm); -->(2) DocWriter perdoc = state.consumer.processdocument(); -->(3) finishdocument(state, perdoc); DocumentsWriter对象主要包含以下几部分 用于写索引文件 IndexWriter writer; Directory directory; Similarity similarity 分词器 String segment 当前的段名 每当flush的时候 将索引写入以此为名称的段 IndexWriter.doFlushInternal() --> String segment = docwriter.getsegment();//return segment --> newsegment = new SegmentInfo(segment, ); --> docwriter.createcompoundfile(segment);//根据segment创建cfs文件 String docstoresegment 存储域所要写入的目标段 (在索引文件格式一文中已经详细描述) int docstoreoffset 存储域在目标段中的偏移量 第 89 / 199 页

90 1.7 Lucene学习总结之四 Lucene索引过程分析(2) int nextdocid 下一篇添加到此索引的文档ID号 对于同一个索引文件夹 此变量唯一 且同 步访问 DocConsumer consumer; 这是整个索引过程的核心 是IndexChain整个索引链的源头 基本索引链 对于一篇文档的索引过程 不是由一个对象来完成的 而是用对象组合的方式形成的一个处理链 链上的每个对象仅 理索引过程的一部分 称为索引链 由于后面还有其他的索引链 所以此处的索引链我称为基本索引链 DocConsumer consumer 类型为DocFieldProcessor 是整个索引链的源头 包含如下部分 对索引域的处理 DocFieldConsumer consumer 类型为DocInverter 包含如下部分 InvertedDocConsumer consumer类型为termshash 包含如下部分 TermsHashConsumer consumer类型为freqproxtermswriter 负责写freq, prox TermsHash nexttermshash TermsHashConsumer consumer类型为termvectorstermswriter 负责 tvd, tvf信息 InvertedDocEndConsumer endconsumer 类型为NormsWriter 负责写nrm信息 对存储域的处理 FieldInfos fieldinfos = new FieldInfos(); StoredFieldsWriter fieldswriter负责写fnm, fdt, fdx信息 删除文档 BufferedDeletes deletesinram = new BufferedDeletes(); BufferedDeletes deletesflushed = new BufferedDeletes(); 类BufferedDeletes包含了一下的成员变量 HashMap terms = new HashMap();删除的词(Term) HashMap queries = new HashMap();删除的查询(Query) List docids = new ArrayList();删除的文档ID long bytesused 用于判断是否应该对删除的文档写入索引文件 由此可见 文档的删除主要有三种方式 IndexWriter.deleteDocuments(Term term) 所有包含此词的文档都会被删除 IndexWriter.deleteDocuments(Query query) 所有能满足此查询的文档都会被删除 IndexReader.deleteDocument(int docnum) 删除此文档ID 第 90 / 199 页

91 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 删除文档既可以用reader进行删除 也可以用writer进行删除 不同的是 reader进行删除后 此reader马上能够生 而用writer删除后 会被缓存在deletesInRAM及deletesFlushed中 只有写入到索引文件中 当reader再次打开的时 才能够看到 那deletesInRAM和deletesFlushed各有什么用处呢 此版本的Lucene对文档的删除是支持多线程的 当用IndexWriter删除文档的时候 都是缓存在deletesInRAM中的 flush 才将删除的文档写入到索引文件中去 我们知道flush是需要一段时间的 那么在flush的过程中 另一个线程 档删除怎么办呢 一般过程是这个样子的 当flush的时候 首先在同步(synchornized)的方法pushDeletes中 将deletesInRAM全部 deletesflushed中 然后将deletesInRAM清空 退出同步方法 于是flush的线程程就向索引文件写deletesFlushed 删除文档的过程 而与此同时其他线程新删除的文档则添加到新的deletesInRAM中去 直到下次flush才写入索引文 缓存管理 为了提高索引的速度 Lucene对很多的数据进行了缓存 使一起写入磁盘 然而缓存需要进行 管理 何时分配 何时回收 何时写入磁盘都需要考虑 ArrayList freecharblocks = new ArrayList();将用于缓存词(Term)信息的空闲块 ArrayList freebyteblocks = new ArrayList();将用于缓存文档号(doc id)及词频(freq) 位置 (prox)信息的空闲块 ArrayList freeintblocks = new ArrayList();将存储某词的词频(freq)和位置(prox)分别在 byteblocks中的偏移量 boolean bufferisfull;用来判断缓存是否满了 如果满了 则应该写入磁盘 long numbytesalloc;分配的内存数量 long numbytesused;使用的内存数量 long freetrigger;应该开始回收内存时的内存用量 long freelevel;回收内存应该回收到的内存用量 long rambuffersize;用户设定的内存用量 缓存用量之间的关系如下 DocumentsWriter.setRAMBufferSizeMB(double mb){ rambuffersize = (long) (mb*1024*1024);//用户设定的内存用量 当使用内存大于此时 开始写入磁盘 waitqueuepausebytes = (long) (rambuffersize*0.1); waitqueueresumebytes = (long) (rambuffersize*0.05); freetrigger = (long) (1.05 * rambuffersize);//当分配的内存到达105%的时候开始释放freeblocks中的内存 freelevel = (long) (0.95 * rambuffersize);//一直释放到95% 第 91 / 199 页

92 1.7 Lucene学习总结之四 Lucene索引过程分析(2) DocumentsWriter.balanceRAM(){ if (numbytesalloc+deletesramused > freetrigger) { //当分配的内存加删除文档所占用的内存大于105%的时候 开始释放内存 while(numbytesalloc+deletesramused > freelevel) { //一直进行释放 直到95% //释放free blocks byteblockallocator.freebyteblocks.remove(byteblockallocator.freebyteblocks.size()-1); numbytesalloc -= BYTE_BLOCK_SIZE; freecharblocks.remove(freecharblocks.size()-1); numbytesalloc -= CHAR_BLOCK_SIZE * CHAR_NUM_BYTE; freeintblocks.remove(freeintblocks.size()-1); numbytesalloc -= INT_BLOCK_SIZE * INT_NUM_BYTE; else { if (numbytesused+deletesramused > rambuffersize){ //当使用的内存加删除文档占有的内存大于用户指定的内存时 可以写入磁盘 bufferisfull = true; 当判断是否应该写入磁盘时: 如果使用的内存大于用户指定内存时 bufferisfull = true 当使用的内存加删除文档所占的内存加正在写入的删除文档所占的内存大于用户指定内存时 deletesinram.bytesused + deletesflushed.bytesused + numbytesused) >= rambuffersize 当删除的文档数目大于maxBufferedDeleteTerms时 DocumentsWriter.timeToFlushDeletes(){ return (bufferisfull deletesfull()) && setflushpending(); 第 92 / 199 页

93 1.7 Lucene学习总结之四 Lucene索引过程分析(2) DocumentsWriter.deletesFull(){ return (rambuffersize!= IndexWriter.DISABLE_AUTO_FLUSH && (deletesinram.bytesused + deletesflushed.bytesused + numbytesused) >= rambuffersize) (maxbuffereddeleteterms!= IndexWriter.DISABLE_AUTO_FLUSH && ((deletesinram.size() + deletesflushed.size()) >= maxbuffereddeleteterms)); 多线程并发索引 为了支持多线程并发索引 对每一个线程都有一个DocumentsWriterThreadState 其为每一 个线程根据DocConsumer consumer的索引链来创建每个线程的索引链(xxxperthread) 来 进行对文档的并发处理 DocumentsWriterThreadState[] threadstates = new DocumentsWriterThreadState[0]; HashMap threadbindings = new HashMap(); 虽然对文档的处理过程可以并行 但是将文档写入索引文件却必须串行进行 串行写入的代码 在DocumentsWriter.finishDocument中 WaitQueue waitqueue = new WaitQueue() long waitqueuepausebytes long waitqueueresumebytes 在Lucene中 文档是按添加的顺序编号的 DocumentsWriter中的nextDocID就是记录下一个添加的文档id 当Lu 持多线程的时候 就必须要有一个synchornized方法来付给文档id并且将nextDocID加一 这些是在 DocumentsWriter.getThreadState这个函数里面做的 虽然给文档付ID没有问题了 但是由Lucene索引文件格式我们知道 文档是要按照ID的顺序从小到大写到索引文件中 然而不同的文档处理速度不同 当一个先来的线程一处理一篇需要很长时间的大文档时 另一个后来的线程二可能已 了很多小的文档了 但是这些后来小文档的ID号都大于第一个线程所处理的大文档 因而不能马上写到索引文件中去 放到waitQueue中 仅仅当大文档处理完了之后才写入索引文件 waitqueue中有一个变量nextwritedocid表示下一个可以写入文件的id 当付给大文档ID=4时 则nextWriteDoc 为4 虽然后来的小文档 等都已处理结束 但是如下代码 WaitQueue.add(){ if (doc.docid == nextwritedocid){ else { waiting[loc] = doc; 第 93 / 199 页

94 1.7 Lucene学习总结之四 Lucene索引过程分析(2) waitingbytes += doc.sizeinbytes(); dopause() 则把5, 6, 7, 8放入waiting队列 并且记录当前等待的文档所占用的内存大小waitingBytes 当大文档4处理完毕后 不但写入文档4 把原来等待的文档5, 6, 7, 8也一起写入 WaitQueue.add(){ if (doc.docid == nextwritedocid) { writedocument(doc); while(true) { doc = waiting[nextwriteloc]; writedocument(doc); else { dopause() 但是这存在一个问题 当大文档很大很大 处理的很慢很慢的时候 后来的线程二可能已经处理了很多的小文档了 档都是在waitQueue中 则占有了越来越多的内存 长此以往 有内存不够的危险 因而在finishDocuments里面 在WaitQueue.add最后调用了doPause()函数 DocumentsWriter.finishDocument(){ dopause = waitqueue.add(docwriter); 第 94 / 199 页

95 1.7 Lucene学习总结之四 Lucene索引过程分析(2) if (dopause) waitforwaitqueue(); notifyall(); WaitQueue.doPause() { return waitingbytes > waitqueuepausebytes; 当waitingBytes足够大的时候(为用户指定的内存使用量的10%) dopause返回true 于是后来的线程二会进入wait 不再处理另外的文档 而是等待线程一处理大文档结束 当线程一处理大文档结束的时候 调用notifyAll唤醒等待他的线程 DocumentsWriter.waitForWaitQueue() { do { try { wait(); catch (InterruptedException ie) { throw new ThreadInterruptedException(ie); while (!waitqueue.doresume()); WaitQueue.doResume() { return waitingbytes <= waitqueueresumebytes; 当waitingBytes足够小的时候 doresume返回true, 则线程二不用再wait了 可以继续处理另外的文档 一些标志位 int maxfieldlength 一篇文档中 一个域内可索引的最大的词(Term)数 int maxbuffereddeleteterms 可缓存的最大的删除词(Term)数 当大于这个数的时候 就要 写到文件中了 此过程又包含如下三个子过程 第 95 / 199 页

96 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 4.1 得到当前线程对应的文档集处理对象(DocumentsWriterThreadState) 代码为 DocumentsWriterThreadState state = getthreadstate(doc, delterm); 在Lucene中 对于同一个索引文件夹 只能够有一个IndexWriter打开它 在打开后 在文件夹中 生成文件 write.lock 当其他IndexWriter再试图打开此索引文件夹的时候 则会报 org.apache.lucene.store.lockobtainfailedexception错误 这样就出现了这样一个问题 在同一个进程中 对同一个索引文件夹 只能有一个IndexWriter打开它 因而如 果想多线程向此索引文件夹中添加文档 则必须共享一个IndexWriter 而且在以往的实现中 adddocument 函数是同步的(synchronized) 也即多线程的索引并不能起到提高性能的效果 于是为了支持多线程索引 不使IndexWriter成为瓶颈 对于每一个线程都有一个相应的文档集处理对象 (DocumentsWriterThreadState) 这样对文档的索引过程可以多线程并行进行 从而增加索引的速度 getthreadstate函数是同步的(synchronized) DocumentsWriter有一个成员变量threadBindings 它是一 个HashMap 键为线程对象(Thread.currentThread()) 值为此线程对应的DocumentsWriterThreadState对 象 DocumentsWriterThreadState DocumentsWriter.getThreadState(Document doc, Term delterm)包含如 下几个过程 根据当前线程对象 从HashMap中查找相应的DocumentsWriterThreadState对象 如果没找到 则 生成一个新对象 并添加到HashMap中 DocumentsWriterThreadState state = (DocumentsWriterThreadState) threadbindings.get(thread.currentth if (state == null) { state = new DocumentsWriterThreadState(this); threadbindings.put(thread.currentthread(), state); 如果此线程对象正在用于处理上一篇文档 则等待 直到此线程的上一篇文档处理完 DocumentsWriter.getThreadState() { waitready(state); state.isidle = false; 第 96 / 199 页

97 1.7 Lucene学习总结之四 Lucene索引过程分析(2) waitready(state) { while (!state.isidle) {wait(); 显然如果state.isIdle为false 则此线程等待 在一篇文档处理之前 state.isidle = false会被设定 而在一篇文档处理完毕之后 DocumentsWriter.finishDocument(DocumentsWriterThreadState perthread, DocWriter docwriter)中 会首 perthread.isidle = true; 然后notifyAll()来唤醒等待此文档完成的线程 从而处理下一篇文档 如果IndexWriter刚刚commit过 则新添加的文档要加入到新的段中(segment) 则首先要生成新的段 名 initsegmentname(false); --> if (segment == null) segment = writer.newsegmentname(); 将此线程的文档处理对象设为忙碌 state.isidle = false; 4.2 用得到的文档集处理对象(DocumentsWriterThreadState)处理文档 代码为 DocWriter perdoc = state.consumer.processdocument(); 每一个文档集处理对象DocumentsWriterThreadState都有一个文档及域处理对象 DocFieldProcessorPerThread 它的成员函数processDocument()被调用来对文档及域进行处理 线程索引链(XXXPerThread): 由于要多线程进行索引 因而每个线程都要有自己的索引链 称为线程索引链 线程索引链同基本索引链有相似的树形结构 由基本索引链中每个层次的对象调用addThreads进行创建的 负责每个 文档的处理 DocFieldProcessorPerThread是线程索引链的源头 由DocFieldProcessor.addThreads( )创建 DocFieldProcessorPerThread对象结构如下 对索引域进行处理 DocFieldConsumerPerThread consumer 类型为 DocInverterPerThread 由DocInverter.addTh InvertedDocConsumerPerThread consumer 类型为TermsHashPerThread 由 TermsHash.addThreads创建 第 97 / 199 页

98 1.7 Lucene学习总结之四 Lucene索引过程分析(2) TermsHashConsumerPerThread consumer类型为freqproxtermswriterperthre FreqProxTermsWriter.addThreads创建 负责每个线程的freq prox信息处理 TermsHashPerThread nextperthread TermsHashConsumerPerThread consumer类型 TermVectorsTermsWriterPerThread 由TermVectorsTermsWriter创建 线程的tvx tvd tvf信息处理 InvertedDocEndConsumerPerThread endconsumer 类型为NormsWriterPerThread 由 NormsWriter.addThreads创建 负责nrm信息的处理 对存储域进行处理 StoredFieldsWriterPerThread fieldswriter由storedfieldswriter.addthreads创建 负责fnm fd 处理 FieldInfos fieldinfos; DocumentsWriter.DocWriter DocFieldProcessorPerThread.processDocument()包含以下几个过程 开始处理当前文档 consumer(docinverterperthread).startdocument(); fieldswriter(storedfieldswriterperthread).startdocument(); 在此版的Lucene中 几乎所有的XXXPerThread的类 都有startDocument和finishDocument两个函数 因 为对同一个线程 这些对象都是复用的 而非对每一篇新来的文档都创建一套 这样也提高了效率 也牵扯到 数据的清理问题 一般在startDocument函数中 清理处理上篇文档遗留的数据 在finishDocument中 收集 本次处理的结果数据 并返回 一直返回到DocumentsWriter.updateDocument(Document, Analyzer, Term) 然后根据条件判断是否将数据刷新到硬盘上 逐个处理文档的每一个域 由于一个线程可以连续处理多个文档 而在普通的应用中 几乎每篇文档的域都是大致相同的 为每篇文档的 每个域都创建一个处理对象非常低效 因而考虑到复用域处理对象DocFieldProcessorPerField 对于每一个域 都有一个此对象 那当来到一个新的域的时候 如何更快的找到此域的处理对象呢 Lucene创建了一个 DocFieldProcessorPerField[] fieldhash哈希表来方便更快查找域对应的处理对象 当处理各个域的时候 按什么顺序呢 其实是按照域名的字典顺序 因而Lucene创建了 DocFieldProcessorPerField[] fields的数组来方便按顺序处理域 因而一个域的处理对象被放在了两个地方 第 98 / 199 页

99 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 对于域的处理过程如下 首先 对于每一个域 按照域名 在fieldHash中查找域处理对象DocFieldProcessorPerField 代码 如下 final int hashpos = fieldname.hashcode() & hashmask;//计算哈希值 DocFieldProcessorPerField fp = fieldhash[hashpos];//找到哈希表中对应的位置 while(fp!= null &&!fp.fieldinfo.name.equals(fieldname)) fp = fp.next;//链式哈希表 如果能够找到 则更新DocFieldProcessorPerField中的域信息fp.fieldInfo.update(field.isIndexed() ) 如果没有找到 则添加域到DocFieldProcessorPerThread.fieldInfos中 并创建新的 DocFieldProcessorPerField 且将其加入哈希表 代码如下 fp = new DocFieldProcessorPerField(this, fi); fp.next = fieldhash[hashpos]; fieldhash[hashpos] = fp; 如果是一个新的field 则将其加入fields数组fields[fieldCount++] = fp; 并且如果是存储域的话 用StoredFieldsWriterPerThread将其写到索引中 if (field.isstored()) { fieldswriter.addfield(field, fp.fieldinfo); 处理存储域的过程如下 StoredFieldsWriterPerThread.addField(Fieldable field, FieldInfo fieldinfo) --> localfieldswriter.writefield(fieldinfo, field); FieldsWriter.writeField(FieldInfo fi, Fieldable field)代码如下 请参照fdt文件的格式 则一目了然 第 99 / 199 页

100 1.7 Lucene学习总结之四 Lucene索引过程分析(2) fieldsstream.writevint(fi.number);//文档号 byte bits = 0; if (field.istokenized()) bits = FieldsWriter.FIELD_IS_TOKENIZED; if (field.isbinary()) bits = FieldsWriter.FIELD_IS_BINARY; if (field.iscompressed()) bits = FieldsWriter.FIELD_IS_COMPRESSED; fieldsstream.writebyte(bits); //域的属性位 if (field.iscompressed()) {//对于压缩域 // compression is enabled for the current field final byte[] data; final int len; final int offset; // check if it is a binary field if (field.isbinary()) { data = CompressionTools.compress(field.getBinaryValue(), field.getbinaryoffset(), field.getbinarylengt else { byte x[] = field.stringvalue().getbytes("utf-8"); data = CompressionTools.compress(x, 0, x.length); len = data.length; offset = 0; fieldsstream.writevint(len);//写长度 fieldsstream.writebytes(data, offset, len);//写二进制内容 else {//对于非压缩域 // compression is disabled for the current field if (field.isbinary()) {//如果是二进制域 final byte[] data; final int len; final int offset; data = field.getbinaryvalue(); len = field.getbinarylength(); offset = field.getbinaryoffset(); 第 100 / 199 页

101 1.7 Lucene学习总结之四 Lucene索引过程分析(2) fieldsstream.writevint(len);//写长度 fieldsstream.writebytes(data, offset, len);//写二进制内容 else { fieldsstream.writestring(field.stringvalue());//写字符内容 然后 对fields数组进行排序 是域按照名称排序 quicksort(fields, 0, fieldcount-1); 最后 按照排序号的顺序 对域逐个处理 此处处理的仅仅是索引域 代码如下 for(int i=0;i fields[i].consumer.processfields(fields[i].fields, fields[i].fieldcount); 域处理对象(DocFieldProcessorPerField)结构如下 域索引链 每个域也有自己的索引链 称为域索引链 每个域的索引链也有同线程索引链有相似的树形结构 由线程索引链中每 个层次的对象调用addField进行创建 负责对此域的处理 和基本索引链及线程索引链不同的是 域索引链仅仅负责处理索引域 而不负责存储域的处理 DocFieldProcessorPerField是域索引链的源头 对象结构如下 DocFieldConsumerPerField consumer类型为docinverterperfield 由DocInverterPerThread.addField创 InvertedDocConsumerPerField consumer 类型为TermsHashPerField 由TermsHashPerThread 创建 TermsHashConsumerPerField consumer 类型为FreqProxTermsWriterPerField 由 FreqProxTermsWriterPerThread.addField创建 负责freq, prox信息的处理 TermsHashPerField nextperfield TermsHashConsumerPerField consumer 类型为TermVectorsTermsWriterPerFi TermVectorsTermsWriterPerThread.addField创建 负责tvx, tvd, tvf信息的处理 InvertedDocEndConsumerPerField endconsumer 类型为NormsWriterPerField 由 NormsWriterPerThread.addField创建 负责nrm信息的处理 处理索引域的过程如下 DocInverterPerField.processFields(Fieldable[], int) 过程如下 第 101 / 199 页

102 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 判断是否要形成倒排表 代码如下 boolean doinvert = consumer.start(fields, count); --> TermsHashPerField.start(Fieldable[], int) --> for(int i=0;i if (fields[i].isindexed()) return true; return false; 读到这里 大家可能会发生困惑 既然XXXPerField是对于每一个域有一个处理对象的 那为什么参数传进来的 是Fieldable[]数组, 并且还有域的数目count呢 其实这不经常用到 但必须得提一下 由上面的fieldHash的实现我们可以看到 是根据域名进行哈希的 所以 准确的讲 XXXPerField并非对于每一个域有一个处理对象 而是对每一组相同名字的域有相同的处理对象 对于同一篇文档 相同名称的域可以添加多个 代码如下 doc.add(new Field("contents", "the content of the file.", Field.Store.NO, Field.Index.NOT_ANALYZED)); doc.add(new Field("contents", new FileReader(f))); 则传进来的名为"contents"的域如下 fields [0] Fieldable[2] (id=52) Field (id=56) binarylength 0 binaryoffset boost fieldsdata isbinary "the content of the file." false iscompressed isindexed isstored true false istokenized lazy name false false false "contents" omitnorms false omittermfreqandpositions false storeoffsetwithtermvector false storepositionwithtermvector 第 102 / 199 页 false

103 1.7 Lucene学习总结之四 Lucene索引过程分析(2) storetermvector tokenstream [1] false null Field (id=58) binarylength 0 binaryoffset boost fieldsdata isbinary FileReader (id=131) false iscompressed isindexed isstored true false istokenized lazy false true false name "contents" omitnorms false omittermfreqandpositions false storeoffsetwithtermvector false storepositionwithtermvector storetermvector tokenstream false false null 对传进来的同名域逐一处理 代码如下 for(int i=0;i final Fieldable field = fields[i]; if (field.isindexed() && doinvert) { //仅仅对索引域进行处理 if (!field.istokenized()) { //如果此域不分词 见(1)对不分词的域的处理 else { //如果此域分词 见(2)对分词的域的处理 第 103 / 199 页

104 1.7 Lucene学习总结之四 Lucene索引过程分析(2) (1) 对不分词的域的处理 (1-1) 得到域的内容 并构建单个Token形成的SingleTokenAttributeSource 因为不进行分词 因而整个域 的内容算做一个Token. String stringvalue = field.stringvalue(); //stringvalue " " final int valuelength = stringvalue.length(); perthread.singletoken.reinit(stringvalue, 0, valuelength); 对于此域唯一的一个Token有以下的属性 Term 文字信息 在处理过程中 此值将保存在TermAttribute的实现类实例化的对象 TermAttributeImp里面 Offset 偏移量信息 是按字或字母的起始偏移量和终止偏移量 表明此Token在文章中的位置 多用 于加亮 在处理过程中 此值将保存在OffsetAttribute的实现类实例化的对象OffsetAttributeImp里 面 在SingleTokenAttributeSource里面 有一个HashMap来保存可能用于保存属性的类名(Key 准确的讲是接 口)以及保存属性信息的对象(Value) singletoken DocInverterPerThread$SingleTokenAttributeSource (id=150) attributeimpls attributes size LinkedHashMap (id=945) LinkedHashMap (id=946) 2 table [0] HashMap$Entry[16] (id=988) LinkedHashMap$Entry (id=991) key value Class (org.apache.lucene.analysis.tokenattributes.termattribute) (id=755) TermAttributeImpl (id=949) termbuffer termlength [7] //[2, 0, 0, 9, 1, 0, 2, 4, 0, 9, 5, 7] 12 LinkedHashMap$Entry (id=993) key value Class (org.apache.lucene.analysis.tokenattributes.offsetattribute) (id=274) OffsetAttributeImpl (id=948) endoffset 第 104 / 199 页 char[19] (id=954) 12

105 startoffset factory 1.7 Lucene学习总结之四 Lucene索引过程分析(2) 0 AttributeSource$AttributeFactory$DefaultAttributeFactory (id=947) offsetattribute termattribute OffsetAttributeImpl (id=948) TermAttributeImpl (id=949) (1-2) 得到Token的各种属性信息 为索引做准备 consumer.start(field)做的主要事情就是根据各种属性的类型来构造保存属性的对象(hashmap中有则取出 无 则构造) 为索引做准备 consumer(termshashperfield).start( ) --> termatt = fieldstate.attributesource.addattribute(termattribute.class);得到的就是上述hashmap中的term --> consumer(freqproxtermswriterperfield).start(f); --> if (fieldstate.attributesource.hasattribute(payloadattribute.class)) { payloadattribute = fieldstate.attributesource.getattribute(payloadattribute.class); 存储payload信息则得到payload的属 --> nextperfield(termshashperfield).start(f); --> termatt = fieldstate.attributesource.addattribute(termattribute.class);得到的还是上述hashmap中的 --> consumer(termvectorstermswriterperfield).start(f); --> if (dovectoroffsets) { offsetattribute = fieldstate.attributesource.addattribute(offsetattribute.class); 如果存储词向量则得到的是上述HashMap中的OffsetAttributeImp (1-3) 将Token加入倒排表 consumer(termshashperfield).add(); 加入倒排表的过程 无论对于分词的域和不分词的域 过程是一样的 因而放到对分词的域的解析中一起说 明 (2) 对分词的域的处理 第 105 / 199 页

106 1.7 Lucene学习总结之四 Lucene索引过程分析(2) (2-1) 构建域的TokenStream final TokenStream streamvalue = field.tokenstreamvalue(); //用户可以在添加域的时候 应用构造函数public Field(String name, TokenStream tokenstream) 直接传进一个T 就不用另外构建一个TokenStream了 if (streamvalue!= null) stream = streamvalue; else { stream = docstate.analyzer.reusabletokenstream(fieldinfo.name, reader); 此时TokenStream的各项属性值还都是空的 等待一个一个被分词后得到 此时的TokenStream对象如下 stream StopFilter (id=112) attributeimpls attributes size LinkedHashMap (id=121) LinkedHashMap (id=122) 4 table [2] HashMap$Entry[16] (id=146) LinkedHashMap$Entry (id=148) key Class (org.apache.lucene.analysis.tokenattributes.typeattribute) (id=154) value TypeAttributeImpl (id=157) type [8] "word" LinkedHashMap$Entry (id=150) after LinkedHashMap$Entry (id=156) key Class (org.apache.lucene.analysis.tokenattributes.offsetattribute) (id=163) value OffsetAttributeImpl (id=164) endoffset startoffset key value 0 Class (org.apache.lucene.analysis.tokenattributes.termattribute) (id=142) TermAttributeImpl (id=133) termbuffer 第 106 / 199 页 0 char[17] (id=173)

107 1.7 Lucene学习总结之四 Lucene索引过程分析(2) termlength [10] 0 LinkedHashMap$Entry (id=151) key Class (org.apache.lucene.analysis.tokenattributes.positionincrementattribute) (id=136) value PositionIncrementAttributeImpl (id=129) positionincrement currentstate AttributeSource$State (id=123) enablepositionincrements factory 1 true AttributeSource$AttributeFactory$DefaultAttributeFactory (id=125) input LowerCaseFilter (id=127) input StandardFilter (id=213) input StandardTokenizer (id=218) input stopwords termatt FileReader (id=93) //从文件中读出来的文本 将经过分词器分词 并一层层的Filter的处理 CharArraySet$UnmodifiableCharArraySet (id=131) TermAttributeImpl (id=133) (2-2) 得到第一个Token 并初始化此Token的各项属性信息 并为索引做准备(start) boolean hasmoretokens = stream.incrementtoken();//得到第一个token OffsetAttribute offsetattribute = fieldstate.attributesource.addattribute(offsetattribute.class);//得到偏 移量属性 offsetattribute endoffset startoffset OffsetAttributeImpl (id=164) 8 0 PositionIncrementAttribute posincrattribute = fieldstate.attributesource.addattribute(positionincrementattribute.class);//得到位置属性 posincrattribute PositionIncrementAttributeImpl (id=129) positionincrement 1 consumer.start(field);//其中得到了termattribute属性 如果存储payload则得到PayloadAttribute属性 如 果存储词向量则得到OffsetAttribute属性 (2-3) 进行循环 不断的取下一个Token 并添加到倒排表 第 107 / 199 页

108 1.7 Lucene学习总结之四 Lucene索引过程分析(2) for(;;) { if (!hasmoretokens) break; consumer.add(); hasmoretokens = stream.incrementtoken(); (2-4) 添加Token到倒排表的过程consumer(TermsHashPerField).add() TermsHashPerField对象主要包括以下部分 CharBlockPool charpool; 用于存储Token的文本信息 如果不足时 从DocumentsWriter中的 freecharblocks分配 ByteBlockPool bytepool;用于存储freq, prox信息 如果不足时 从DocumentsWriter中的 freebyteblocks分配 IntBlockPool intpool; 用于存储分别指向每个Token在bytePool中freq和prox信息的偏移量 如果不 足时 从DocumentsWriter的freeIntBlocks分配 TermsHashConsumerPerField consumer类型为freqproxtermswriterperfield 用于写freq, prox 信息到缓存中 RawPostingList[] postingshash = new RawPostingList[postingsHashSize];存储倒排表 每一个 Term都有一个RawPostingList (PostingList) 其中包含了int textstart 也即文本在charPool中的偏 移量 int bytestart 即此Term的freq和prox信息在bytePool中的起始偏移量 int intstart 即此 term的在intpool中的起始偏移量 形成倒排表的过程如下 //得到token的文本及文本长度 final char[] tokentext = termatt.termbuffer();//[s, t, u, d, e, n, t, s] final int tokentextlen = termatt.termlength();//tokentextlen 8 //按照token的文本计算哈希值 以便在postingsHash中找到此token对应的倒排表 第 108 / 199 页

109 1.7 Lucene学习总结之四 Lucene索引过程分析(2) int downto = tokentextlen; int code = 0; while (downto > 0) { char ch = tokentext[ downto]; code = (code*31) + ch; int hashpos = code & postingshashmask; //在倒排表哈希表中查找此Token 如果找到相应的位置 但是不是此Token 说明此位置存在哈希冲突 采取重新哈 p = postingshash[hashpos]; if (p!= null &&!postingequals(tokentext, tokentextlen)) { final int inc = ((code>>8)+code) 1; do { code += inc; hashpos = code & postingshashmask; p = postingshash[hashpos]; while (p!= null &&!postingequals(tokentext, tokentextlen)); //如果此Token之前从未出现过 if (p == null) { if (textlen1 + charpool.charupto > DocumentsWriter.CHAR_BLOCK_SIZE) { //当charPool不足的时候 在freeCharBlocks中分配新的buffer charpool.nextbuffer(); //从空闲的倒排表中分配新的倒排表 p = perthread.freepostings[--perthread.freepostingscount]; //将文本复制到charPool中 final char[] text = charpool.buffer; final int textupto = charpool.charupto; 第 109 / 199 页

110 1.7 Lucene学习总结之四 Lucene索引过程分析(2) p.textstart = textupto + charpool.charoffset; charpool.charupto += textlen1; System.arraycopy(tokenText, 0, text, textupto, tokentextlen); text[textupto+tokentextlen] = 0xffff; //将倒排表放入哈希表中 postingshash[hashpos] = p; numpostings++; if (numpostingint + intpool.intupto > DocumentsWriter.INT_BLOCK_SIZE) intpool.nextbuffer(); //当intPool不足的时候 在freeIntBlocks中分配新的buffer if (DocumentsWriter.BYTE_BLOCK_SIZE - bytepool.byteupto < numpostingint*byteblockpool.first_leve bytepool.nextbuffer(); //当bytePool不足的时候 在freeByteBlocks中分配新的buffer //此处streamCount为2 表明在intPool中 每两项表示一个词 一个是指向bytePool中freq信息偏移量的 一个 信息偏移量的 intuptos = intpool.buffer; intuptostart = intpool.intupto; intpool.intupto += streamcount; p.intstart = intuptostart + intpool.intoffset; //在bytePool中分配两个空间 一个放freq信息 一个放prox信息的 for(int i=0;i final int upto = bytepool.newslice(byteblockpool.first_level_size); intuptos[intuptostart+i] = upto + bytepool.byteoffset; p.bytestart = intuptos[intuptostart]; //当Term原来没有出现过的时候 调用newTerm consumer(freqproxtermswriterperfield).newterm(p); 第 110 / 199 页

111 1.7 Lucene学习总结之四 Lucene索引过程分析(2) //如果此Token之前曾经出现过 则调用addTerm else { intuptos = intpool.buffers[p.intstart >> DocumentsWriter.INT_BLOCK_SHIFT]; intuptostart = p.intstart & DocumentsWriter.INT_BLOCK_MASK; consumer(freqproxtermswriterperfield).addterm(p); (2-5) 添加新Term的过程 consumer(freqproxtermswriterperfield).newterm final void newterm(rawpostinglist p0) { FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; p.lastdocid = docstate.docid; //当一个新的term出现的时候 包含此Term的就只有本篇文档 记录其ID p.lastdoccode = docstate.docid << 1; //doccode是文档id左移一位 为什么左移 请参照索引文件格式(1)中 p.docfreq = 1; //docfreq这里用词可能容易引起误会 docfreq这里指的是此文档所包含的此term的次数 并非 数 writeprox(p, fieldstate.position); //写入prox信息到bytePool中 此时freq信息还不能写入 因为当前的文档还没 文档包含此Term的总数 writeprox(freqproxtermswriter.postinglist p, int proxcode) { termshashperfield.writevint(1, proxcode<<1);//第一个参数所谓1 也就是写入此文档在intPool中的第1项 一位呢 是因为后面可能跟着payload信息 参照索引文件格式(1)中或然跟随规则 p.lastposition = fieldstate.position;//总是要记录lastdocid, lastpostion 是因为要计算差值 参照索引文件格式 (2-6) 添加已有Term的过程 final void addterm(rawpostinglist p0) { FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0; if (docstate.docid!= p.lastdocid) { 第 111 / 199 页

112 1.7 Lucene学习总结之四 Lucene索引过程分析(2) //当文档ID变了的时候 说明上一篇文档已经处理完毕 可以写入freq信息了 //第一个参数所谓0 也就是写入上一篇文档在intPool中的第0项 freq信息 至于信息为何这样写 参照索引 随规则 及tis文件格式 if (1 == p.docfreq) termshashperfield.writevint(0, p.lastdoccode 1); else { termshashperfield.writevint(0, p.lastdoccode); termshashperfield.writevint(0, p.docfreq); p.docfreq = 1;//对于新的文档 freq还是为1. p.lastdoccode = (docstate.docid - p.lastdocid) << 1;//文档号存储差值 p.lastdocid = docstate.docid; writeprox(p, fieldstate.position); else { //当文档ID不变的时候 说明此文档中这个词又出现了一次 从而freq加一 写入再次出现的位置信息 用差值 p.docfreq++; writeprox(p, fieldstate.position-p.lastposition); (2-7) 结束处理当前域 consumer(termshashperfield).finish(); --> FreqProxTermsWriterPerField.finish() --> TermVectorsTermsWriterPerField.finish() endconsumer(normswriterperfield).finish(); --> norms[upto] = Similarity.encodeNorm(norm);//计算标准化因子的值 --> docids[upto] = docstate.docid; 结束处理当前文档 第 112 / 199 页

113 1.7 Lucene学习总结之四 Lucene索引过程分析(2) final DocumentsWriter.DocWriter one = fieldswriter(storedfieldswriterperthread).finishdocument(); 存储域返回结果 一个写成了二进制的存储域缓存 one StoredFieldsWriter$PerDoc (id=322) docid fdt 0 RAMOutputStream (id=325) bufferlength 1024 bufferposition 40 bufferstart 0 copybuffer null currentbuffer byte[1024] (id=332) currentbufferindex file RAMFile (id=333) utf8result next 0 UnicodeUtil$UTF8Result (id=335) null numstoredfields this$0 2 StoredFieldsWriter (id=327) final DocumentsWriter.DocWriter two = consumer(docinverterperthread).finishdocument(); --> NormsWriterPerThread.finishDocument() --> TermsHashPerThread.finishDocument() 索引域的返回结果为null 4.3 用DocumentsWriter.finishDocument结束本次文档添加 代码 DocumentsWriter.updateDocument(Document, Analyzer, Term) --> DocumentsWriter.finishDocument(DocumentsWriterThreadState, DocumentsWriter$DocWriter) --> dopause = waitqueue.add(docwriter);//有关waitqueue 在DocumentsWriter的缓存管理中已作解释 --> DocumentsWriter$WaitQueue.writeDocument(DocumentsWriter$DocWriter) 第 113 / 199 页

114 1.7 Lucene学习总结之四 Lucene索引过程分析(2) --> StoredFieldsWriter$PerDoc.finish() --> fieldswriter.flushdocument(perdoc.numstoredfields, perdoc.fdt);将存储域信息真正写入文 第 114 / 199 页

115 1.8 Lucene学习总结之四 Lucene索引过程分析(3) 1.8 Lucene学习总结之四 Lucene索引过程分析(3) 发表时间: DocumentsWriter对CharBlockPool ByteBlockPool IntBlockPool的缓存管 理 在索引的过程中 DocumentsWriter将词信息(term)存储在CharBlockPool中 将文档号(doc ID) 词 频(freq)和位置(prox)信息存储在ByteBlockPool中 在ByteBlockPool中 缓存是分块(slice)分配的 块(slice)是分层次的 层次越高 此层的块越大 每一 层的块大小事相同的 nextlevelarray表示的是当前层的下一层是第几层 可见第9层的下一层还是第9层 也就是说 最高有9层 levelsizearray表示每一层的块大小 第一层是5个byte 第二层是14个byte以此类推 ByteBlockPool类中有以下静态变量 final static int[] nextlevelarray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9; final static int[] levelsizearray = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200; 在ByteBlockPool中分配一个块的代码如下 //此函数仅仅在upto已经是当前块的结尾的时候方才调用来分配新块 public int allocslice(final byte[] slice, final int upto) { //可根据块的结束符来得到块所在的层次 从而我们可以推断 每个层次的块都有不同的结束符 第1层为16 第2 类推 final int level = slice[upto] & 15; //从数组总得到下一个层次及下一层块的大小 final int newlevel = nextlevelarray[level]; final int newsize = levelsizearray[newlevel]; // 如果当前缓存总量不够大 则从DocumentsWriter的freeByteBlocks中分配 if (byteupto > DocumentsWriter.BYTE_BLOCK_SIZE-newSize) 第 115 / 199 页

116 1.8 Lucene学习总结之四 Lucene索引过程分析(3) nextbuffer(); final int newupto = byteupto; final int offset = newupto + byteoffset; byteupto += newsize; //当分配了新的块的时候 需要有一个指针从本块指向下一个块 使得读取此信息的时候 能够在此块读取结束后 //这个指针需要4个byte 在本块中 除了结束符所占用的一个byte之外 之前的三个byte的数据都应该移到新的块 来形成一个指针 buffer[newupto] = slice[upto-3]; buffer[newupto+1] = slice[upto-2]; buffer[newupto+2] = slice[upto-1]; // 将偏移量(也即指针)写入到连同结束符在内的四个byte slice[upto-3] = (byte) (offset >>> 24); slice[upto-2] = (byte) (offset >>> 16); slice[upto-1] = (byte) (offset >>> 8); slice[upto] = (byte) offset; // 在新的块的结尾写入新的结束符 结束符和层次的关系就是(endbyte = 16 level) buffer[byteupto-1] = (byte) (16 newlevel); return newupto+3; 在ByteBlockPool中 文档号和词频(freq)信息是应用或然跟随原则写到一个块中去的 而位置信息 (prox)是写入到另一个块中去的 对于同一个词 这两块的偏移量保存在IntBlockPool中 因而在 IntBlockPool中 每一个词都有两个int 第0个表示docid + freq在byteblockpool中的偏移量 第1个 表示prox在ByteBlockPool中的偏移量 在写入docid + freq信息的时候 调用termsHashPerField.writeVInt(0, p.lastdoccode) 第一个参数 表示向此词的第0个偏移量写入 在写入prox信息的时候 调用termsHashPerField.writeVInt(1, (proxcode<<1) 1) 第一个参数表示向此词的第1个偏移量写入 第 116 / 199 页

117 1.8 Lucene学习总结之四 Lucene索引过程分析(3) CharBlockPool是按照出现的先后顺序保存词(term) 在TermsHashPerField中 有一个成员变量RawPostingList[] postingshash 为每一个term分配了一 个RawPostingList 将上述三个缓存关联起来 abstract class RawPostingList { final static int BYTES_SIZE = DocumentsWriter.OBJECT_HEADER_BYTES + 3*DocumentsWriter.INT_NUM_B int textstart; //此词在CharBlockPool中的偏移量 由此可以知道是哪个词 int intstart; //此词在IntBlockPool中的偏移量 在指向的位置有两个int 一个是docid + freq信息的偏移量 一个 int bytestart; //此词在ByteBlockPool中的起始偏移量 static final class PostingList extends RawPostingList { int docfreq; // 此词在此文档中出现的次数 int lastdocid; // 上次处理完的包含此词的文档号 int lastdoccode; int lastposition; // 文档号和词频按照或然跟随原则形成的编码 // 上次处理完的此词的位置 这里需要说明的是 在IntBlockPool中保存了两个在ByteBlockPool中的偏移量 而在RawPostingList的byteStart又 中的偏移量 这两者有什么区别呢 在IntBlockPool中保存的分别指向docid+freq及prox信息在ByteBlockPool中的偏移量是主要用来写入信息的 它记 入的docid+freq或者prox在ByteBlockPool中的位置 随着信息的不断写入 IntBlockPool中的两个偏移量是不断改 以写入的位置 RawPostingList中byteStart主要是用来读取docid及prox信息的 当索引过程基本结束 所有的信息都写入在缓存中 应的文档号偏移量及位置信息 然后写到索引文件中去呢 自然是通过RawPostingList找到byteStart 然后根据byt 中找到docid+freq及prox信息的起始位置 从起始位置开始的两个大小为5的块 第一个就是docid+freq信息的源头 的源头 如果源头的块中包含了所有的信息 读出来就可以了 如果源头的块中有指针 则沿着指针寻找到下一个块 息 下面举一个实例来表明如果进行缓存管理的 第 117 / 199 页

118 1.8 Lucene学习总结之四 Lucene索引过程分析(3) 此例子中 准备添加三个文件 file01: common common common common common term file02: common common common common common term term file03: term term term common common common common common file04: term (1) 添加第一篇文档第一个common 在CharBlockPool中分配6个char来存放"common"字符串 在ByteBlockPool中分配两个块 每个块大小为5 以16结束 第一个块用来存放docid+freq信息 第二个块 时docid+freq信息没有写入 docid+freq信息总是在下一篇文档的处理过程出现了"common"的时候方才写 处理完毕的时候 freq也即词频是无法知道的 而prox信息存放0 是因为第一个common的位置为0 但是 一位置0表示没有payload存储 因而0<<1 + 0 = 0 在IntBlockPool中分配两个int 一个指向第0个位置 是因为当前没有docid+freq信息写入 第二个指向第6 置写入了prox信息 所以IntBlockPool中存放的是下一个要写入的位置 (2) 添加第四个common 在ByteBlockPool中 prox信息已经存放了4个 第一个0是代表第一个位置为0 后面不跟随payload 第二 原则)为1 后面不跟随payload(或然跟随原则) 1<<1 + 0 =2 第三个第四个同第二个 第 118 / 199 页

119 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (3) 添加第五个common ByteBlockPool中 存放prox信息的块已经全部填满 必须重新分配新的块 新的块层次为2 大小为14 在缓存的最后追加分配 原块中连同结束位在内的四个byte作为指针(绿色部分) 指向新的块的其实地址 在此为10. 被指针占用的结束位之前的三位移到新的块中 也即6, 7, 8移到10, 11, 12处 13处是第五个common的pro 指针的值并不都是四个byte的最后一位 当缓存很大的时候 指针的值也会很大 比如指针出现[0, 0, 0, -56 示指向的负位置 而是最后一个byte的第一位为1 显示为有符号数为负 -56的二进制是 和前三 200也即指向第200个位置 比如指针出现[0, 0, 1, 2] 其转换为二进制的int为 大小为258 也 如指针出现 [0, 0, 1, -98] 转换为二进制的int为 大小为414 也即指向第414个位置 第 119 / 199 页

120 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (4) 添加第一篇文档 第一个term CharBlockPool中分配了5个char来存放"term" ByteBlockPool中分配了两个块来分别存放docid+freq信息和prox信息 第一个块没有信息写入 第二个块 息 即出现在第5个位置 并且后面没有payload 5<<1 + 0 = 10 IntBlockPool中分配了两个int来指向"term"的两个块中下一个写入的位置 第 120 / 199 页

121 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (5) 添加第二篇文档第一个common 第一篇文档的common的docid+freq信息写入 在第一篇文档中 "common"出现了5次 文档号为0 按照 信息为[docid<<1 + 0, 5] = [0, 5] docid左移一位 最后一位为0表示freq大于1 第二篇文档第一个common的位置信息也写入了 位置为0 0<<1 + 0 = 0 第 121 / 199 页

122 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (6) 添加第二篇文档第一个term 第一篇文档中的term的docid+freq信息写入 在第一篇文档中 "term"出现了1次 文档号为0 所以存储信 [1] 文档号左移一位 最后一位为1表示freq为1 第二篇文档第一个term的prox信息也写入了 在第5个位置 5<<1 + 0 = 10 第二篇文档中的5个common的prox信息也写入了 分别为从14到18的[0, 2, 2, 2, 2] 第 122 / 199 页

123 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (7) 添加第三篇文档的第一个term 第二篇文档的term的docid+freq信息写入 在第二篇文档中 文档号为1 "term"出现了2次 所以存储为[ [2, 2] 存储在25, 26两个位置 第二篇文档中两个term的位置信息也写入了 为30, 31的[10, 2] 也即出现在第5个 第6个位置 后面不跟 第三篇文档的第一个term的位置信息也写入了 在第0个位置 不跟payload 为32保存的[0] 第 123 / 199 页

124 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (8) 添加第三篇文档第二个term term的位置信息已经填满了 必须分配新的块 层次为2 大小为14 结束符为17 也即图中34到47的位置 30到33的四个byte组成一个int指针 指向第34个位置 原来30到32的三个prox信息移到34到36的位置 在37处保存第三篇文档第二个term的位置信息 位置为1 不跟随payload 1<<1 + 0 = 2 第 124 / 199 页

125 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (9) 添加第三篇文档第四个common 第二篇文档中"common"的docid+freq信息写入 文档号为1 出现了5次 存储为[docid << 1 + 0, freq] 存储为 [2, 5] 即2 3的位置 第三篇文档中前四个common的位置信息写入 即从19到22的[6, 2, 2, 2] 即出现在第3个 第4个 第5个 第三篇文档中的第三个"term"的位置信息也写入 为38处的[2] 第 125 / 199 页

126 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (10) 添加第三篇文档的第五个common 虽然common已经分配了层次为2 大小为14的第二个块(从10到23) 不过还是用完了 需要在缓存的最后分 小为20 结束符为18 也即从48到67的位置 从20到23的四个byte组成一个int指针指向新分配的块 原来20到22的数据移到48至50的位置 第三篇文档的第五个common的位置信息写入 为第51个位置的[2] 也即紧跟上个common 后面没有pay 第 126 / 199 页

127 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (11) 添加第四篇文档的第一个term 写入第三篇文档的term的docid+freq信息 文档号为2 出现了三次 存储为[docid<<1+0, freq] docid取 3] 然而存储term的docid+freq信息的块已经满了 需要在缓存的最后追加新的块 层次为2 大小为14 结束 置 从25到28的四个byte组成一个int指针指向新分配的块 原来25到26的信息移到68, 69处 在70, 71处写入第三篇文档的docid+freq信息[2, 3] 第 127 / 199 页

128 1.8 Lucene学习总结之四 Lucene索引过程分析(3) (12) 最终PostingList CharBlockPool IntBlockPool ByteBlockPool的关系如下图 第 128 / 199 页

129 第 129 / 199 页 1.8 Lucene学习总结之四 Lucene索引过程分析(3)

130 1.9 Lucene学习总结之四 Lucene索引过程分析(4) 1.9 Lucene学习总结之四 Lucene索引过程分析(4) 发表时间: 关闭IndexWriter对象 代码 writer.close(); --> IndexWriter.closeInternal(boolean) --> (1) 将索引信息由内存写入磁盘: flush(waitformerges, true, true); --> (2) 进行段合并: mergescheduler.merge(this); 对段的合并将在后面的章节进行讨论 此处仅仅讨论将索引信息由写入磁盘的过程 代码 IndexWriter.flush(boolean triggermerge, boolean flushdocstores, boolean flushdeletes) --> IndexWriter.doFlush(boolean flushdocstores, boolean flushdeletes) --> IndexWriter.doFlushInternal(boolean flushdocstores, boolean flushdeletes) 将索引写入磁盘包括以下几个过程 得到要写入的段名 String segment = docwriter.getsegment(); DocumentsWriter将缓存的信息写入段 docwriter.flush(flushdocstores); 生成新的段信息对象 newsegment = new SegmentInfo(segment, flusheddoccount, directory, false, true, docstoreoffset, docstoresegment, docstoreiscompoundfile, docwriter.hasprox()); 准备删除文档 docwriter.pushdeletes(); 生成cfs段 docwriter.createcompoundfile(segment); 删除文档 applydeletes(); 6.1 得到要写入的段名 代码 第 130 / 199 页

131 1.9 Lucene学习总结之四 Lucene索引过程分析(4) SegmentInfo newsegment = null; final int numdocs = docwriter.getnumdocsinram();//文档总数 String docstoresegment = docwriter.getdocstoresegment();//存储域和词向量所要要写入的段名 "_0" int docstoreoffset = docwriter.getdocstoreoffset();//存储域和词向量要写入的段中的偏移量 String segment = docwriter.getsegment();//段名 "_0" 在Lucene的索引文件结构一章做过详细介绍 存储域和词向量可以和索引域存储在不同的段中 6.2 将缓存的内容写入段 代码 flusheddoccount = docwriter.flush(flushdocstores); 此过程又包含以下两个阶段 按照基本索引链关闭存储域和词向量信息 按照基本索引链的结构将索引结果写入段 按照基本索引链关闭存储域和词向量信息 代码为 closedocstore(); flushstate.numdocsinstore = 0; 其主要是根据基本索引链结构 关闭存储域和词向量信息 consumer(docfieldprocessor).closedocstore(flushstate); consumer(docinverter).closedocstore(state); consumer(termshash).closedocstore(state); consumer(freqproxtermswriter).closedocstore(state); if (nexttermshash!= null) nexttermshash.closedocstore(state); consumer(termvectorstermswriter).closedocstore(state); 第 131 / 199 页

132 1.9 Lucene学习总结之四 Lucene索引过程分析(4) endconsumer(normswriter).closedocstore(state); fieldswriter(storedfieldswriter).closedocstore(state); 其中有实质意义的是以下两个closeDocStore: 词向量的关闭 TermVectorsTermsWriter.closeDocStore(SegmentWriteState) void closedocstore(final SegmentWriteState state) throws IOException { if (tvx!= null) { //为不保存词向量的文档在tvd文件中写入零 即便不保存词向量 在tvx, tvd中也保留一个位置 fill(state.numdocsinstore - docwriter.getdocstoreoffset()); //关闭tvx, tvf, tvd文件的写入流 tvx.close(); tvf.close(); tvd.close(); tvx = null; //记录写入的文件名 为以后生成cfs文件的时候 将这些写入的文件生成一个统一的cfs文件 state.flushedfiles.add(state.docstoresegmentname + "." + IndexFileNames.VECTORS_INDEX_EXTEN state.flushedfiles.add(state.docstoresegmentname + "." + IndexFileNames.VECTORS_FIELDS_EXTEN state.flushedfiles.add(state.docstoresegmentname + "." + IndexFileNames.VECTORS_DOCUMENTS //从DocumentsWriter的成员变量openFiles中删除 未来可能被IndexFileDeleter删除 docwriter.removeopenfile(state.docstoresegmentname + "." + IndexFileNames.VECTORS_INDEX_ docwriter.removeopenfile(state.docstoresegmentname + "." + IndexFileNames.VECTORS_FIELDS_ docwriter.removeopenfile(state.docstoresegmentname + "." + IndexFileNames.VECTORS_DOCUM lastdocid = 0; 存储域的关闭 StoredFieldsWriter.closeDocStore(SegmentWriteState) public void closedocstore(segmentwritestate state) throws IOException { //关闭fdx, fdt写入流 fieldswriter.close(); --> fieldsstream.close(); --> indexstream.close(); fieldswriter = null; lastdocid = 0; 第 132 / 199 页

133 1.9 Lucene学习总结之四 Lucene索引过程分析(4) //记录写入的文件名 state.flushedfiles.add(state.docstoresegmentname + "." + IndexFileNames.FIELDS_EXTENSION); state.flushedfiles.add(state.docstoresegmentname + "." + IndexFileNames.FIELDS_INDEX_EXTENSION); state.docwriter.removeopenfile(state.docstoresegmentname + "." + IndexFileNames.FIELDS_EXTENSIO state.docwriter.removeopenfile(state.docstoresegmentname + "." + IndexFileNames.FIELDS_INDEX_EX 按照基本索引链的结构将索引结果写入段 代码为 consumer(docfieldprocessor).flush(threads, flushstate); //回收fieldHash 以便用于下一轮的索引 为提高效率 索引链中的对象是被复用的 Map> childthreadsandfields = new HashMap>(); for ( DocConsumerPerThread thread : threads) { DocFieldProcessorPerThread perthread = (DocFieldProcessorPerThread) thread; childthreadsandfields.put(perthread.consumer, perthread.fields()); perthread.trimfields(state); //写入存储域 --> fieldswriter(storedfieldswriter).flush(state); //写入索引域 --> consumer(docinverter).flush(childthreadsandfields, state); //写入域元数据信息 并记录写入的文件名 以便以后生成cfs文件 --> final String filename = state.segmentfilename(indexfilenames.field_infos_extension); --> fieldinfos.write(state.directory, filename); --> state.flushedfiles.add(filename); 此过程也是按照基本索引链来的 第 133 / 199 页

134 1.9 Lucene学习总结之四 Lucene索引过程分析(4) consumer(docfieldprocessor).flush( ); consumer(docinverter).flush( ); consumer(termshash).flush( ); consumer(freqproxtermswriter).flush( ); if (nexttermshash!= null) nexttermshash.flush( ); consumer(termvectorstermswriter).flush( ); endconsumer(normswriter).flush( ); fieldswriter(storedfieldswriter).flush( ); 写入存储域 代码为 StoredFieldsWriter.flush(SegmentWriteState state) { if (state.numdocsinstore > 0) { initfieldswriter(); fill(state.numdocsinstore - docwriter.getdocstoreoffset()); if (fieldswriter!= null) fieldswriter.flush(); 从代码中可以看出 是写入fdx, fdt两个文件 但是在上述的closeDocStore已经写入了 并且把 state.numdocsinstore置零 fieldswriter设为null 在这里其实什么也不做 写入索引域 代码为 DocInverter.flush(Map>, SegmentWriteState) //写入倒排表及词向量信息 --> consumer(termshash).flush(childthreadsandfields, state); //写入标准化因子 --> endconsumer(normswriter).flush(endchildthreadsandfields, state); 第 134 / 199 页

135 1.9 Lucene学习总结之四 Lucene索引过程分析(4) 写入倒排表及词向量信息 代码为 TermsHash.flush(Map>, SegmentWriteState) //写入倒排表信息 --> consumer(freqproxtermswriter).flush(childthreadsandfields, state); //回收RawPostingList --> shrinkfreepostings(threadsandfields, state); //写入词向量信息 --> if (nexttermshash!= null) nexttermshash.flush(nextthreadsandfields, state); --> consumer(termvectorstermswriter).flush(childthreadsandfields, state); 写入倒排表信息 代码为 FreqProxTermsWriter.flush(Map Collection>, SegmentWriteState) (a) 所有域按名称排序 使得同名域能够一起处理 Collections.sort(allFields); final int numallfields = allfields.size(); (b) 生成倒排表的写对象 final FormatPostingsFieldsConsumer consumer = new FormatPostingsFieldsWriter(state, fieldinfos); int start = 0; (c) 对于每一个域 while(start < numallfields) { 第 135 / 199 页

136 1.9 Lucene学习总结之四 Lucene索引过程分析(4) (c-1) 找出所有的同名域 final FieldInfo fieldinfo = allfields.get(start).fieldinfo; final String fieldname = fieldinfo.name; int end = start+1; while(end < numallfields && allfields.get(end).fieldinfo.name.equals(fieldname)) end++; FreqProxTermsWriterPerField[] fields = new FreqProxTermsWriterPerField[end-start]; for(int i=start;i fields[i-start] = allfields.get(i); fieldinfo.storepayloads = fields[i-start].haspayloads; (c-2) 将同名域的倒排表添加到文件 appendpostings(fields, consumer); (c-3) 释放空间 for(int i=0;i TermsHashPerField perfield = fields[i].termshashperfield; int numpostings = perfield.numpostings; perfield.reset(); perfield.shrinkhash(numpostings); fields[i].reset(); start = end; 第 136 / 199 页

137 1.9 Lucene学习总结之四 Lucene索引过程分析(4) (d) 关闭倒排表的写对象 consumer.finish(); (b) 生成倒排表的写对象 代码为 public FormatPostingsFieldsWriter(SegmentWriteState state, FieldInfos fieldinfos) throws IOException { dir = state.directory; segment = state.segmentname; totalnumdocs = state.numdocs; this.fieldinfos = fieldinfos; //用于写tii,tis termsout = new TermInfosWriter(dir, segment, fieldinfos, state.termindexinterval); //用于写freq, prox的跳表 skiplistwriter = new DefaultSkipListWriter(termsOut.skipInterval, termsout.maxskiplevels, totalnumdoc //记录写入的文件名 state.flushedfiles.add(state.segmentfilename(indexfilenames.terms_extension)); state.flushedfiles.add(state.segmentfilename(indexfilenames.terms_index_extension)); //用以上两个写对象 按照一定的格式写入段 termswriter = new FormatPostingsTermsWriter(state, this); 对象结构如下 consumer dir FormatPostingsFieldsWriter (id=119) //用于处理一个域 SimpleFSDirectory (id=126) //目标索引文件夹 totalnumdocs 8 //文档总数 fieldinfos FieldInfos (id=70) //域元数据信息 segment "_0" //段名 skiplistwriter termsout termswriter DefaultSkipListWriter (id=133) //freq, prox中跳表的写对象 TermInfosWriter (id=125) //tii, tis文件的写对象 FormatPostingsTermsWriter (id=135) //用于添加词(Term) currentterm null currenttermstart fieldinfo null freqstart 0 第 137 / 199 页 0

138 1.9 Lucene学习总结之四 Lucene索引过程分析(4) proxstart 0 termbuffer null termsout TermInfosWriter (id=125) docswriter df FormatPostingsDocsWriter (id=139) //用于写入此词的docid, freq信息 0 fieldinfo null freqstart 0 lastdocid 0 omittermfreqandpositions out false SimpleFSDirectory$SimpleFSIndexOutput (id=144) skipinterval 16 skiplistwriter DefaultSkipListWriter (id=133) storepayloads false terminfo TermInfo (id=151) totalnumdocs poswriter 8 FormatPostingsPositionsWriter (id=146) //用于写入此词在此文档中的位置信息 lastpayloadlength lastposition -1 0 omittermfreqandpositions out false SimpleFSDirectory$SimpleFSIndexOutput (id=157) parent FormatPostingsDocsWriter (id=139) storepayloads false FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息 其返回 FormatPostingsTermsConsumer用于添加词信息 FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加词信息 其返回 FormatPostingsDocsConsumer用于添加freq信息 FormatPostingsDocsConsumer.addDoc(int docid, int termdocfreq)用于添加freq信息 其返回 FormatPostingsPositionsConsumer用于添加prox信息 FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadoffset, int payloadlength)用于添加prox信息 (c-2) 将同名域的倒排表添加到文件 代码为 FreqProxTermsWriter.appendPostings(FreqProxTermsWriterPerField[], FormatPostingsFieldsConsumer) { 第 138 / 199 页

139 1.9 Lucene学习总结之四 Lucene索引过程分析(4) int numfields = fields.length; final FreqProxFieldMergeState[] mergestates = new FreqProxFieldMergeState[numFields]; for(int i=0;i FreqProxFieldMergeState fms = mergestates[i] = new FreqProxFieldMergeState(fields[i]); boolean result = fms.nextterm(); //对所有的域 取第一个词(Term) (1) 添加此域 虽然有多个域 但是由于是同名域 只取第一个域的信息即可 返回的是用于添加此域中的词的对 final FormatPostingsTermsConsumer termsconsumer = consumer.addfield(fields[0].fieldinfo); FreqProxFieldMergeState[] termstates = new FreqProxFieldMergeState[numFields]; final boolean currentfieldomittermfreqandpositions = fields[0].fieldinfo.omittermfreqandpositions; (2) 此while循环是遍历每一个尚有未处理的词的域 依次按照词典顺序处理这些域所包含的词 当一个域中的所有 numfields减一 并从mergeStates数组中移除此域 直到所有的域的所有的词都处理完毕 方才退出此循环 while(numfields > 0) { (2-1) 找出所有域中按字典顺序的下一个词 可能多个同名域中 都包含同一个term 因而要遍历所有的numF 的下一个词 numtomerge即为有多少个域包含此词 termstates[0] = mergestates[0]; int numtomerge = 1; for(int i=1;i final char[] text = mergestates[i].text; final int textoffset = mergestates[i].textoffset; final int cmp = comparetext(text, textoffset, termstates[0].text, termstates[0].textoffset); if (cmp < 0) { termstates[0] = mergestates[i]; 第 139 / 199 页

140 1.9 Lucene学习总结之四 Lucene索引过程分析(4) numtomerge = 1; else if (cmp == 0) termstates[numtomerge++] = mergestates[i]; (2-2) 添加此词 返回FormatPostingsDocsConsumer用于添加文档号(doc ID)及词频信息(freq) final FormatPostingsDocsConsumer docconsumer = termsconsumer.addterm(termstates[0].text, term (2-3) 由于共numToMerge个域都包含此词 每个词都有一个链表的文档号表示包含这些词的文档 此循环遍历 依次按照从小到大的循序添加包含此词的文档号及词频信息 当一个域中对此词的所有文档号都处理过了 则numT termstates数组中移除此域 当所有包含此词的域的所有文档号都处理过了 则结束此循环 while(numtomerge > 0) { (2-3-1) 找出最小的文档号 FreqProxFieldMergeState minstate = termstates[0]; for(int i=1;i if (termstates[i].docid < minstate.docid) minstate = termstates[i]; final int termdocfreq = minstate.termfreq; (2-3-2) 添加文档号及词频信息 并形成跳表 返回FormatPostingsPositionsConsumer用于添加位置(pro final FormatPostingsPositionsConsumer posconsumer = docconsumer.adddoc(minstate.docid, termd //ByteSliceReader是用于读取bytepool中的prox信息的 final ByteSliceReader prox = minstate.prox; if (!currentfieldomittermfreqandpositions) { int position = 0; (2-3-3) 此循环对包含此词的文档 添加位置信息 第 140 / 199 页

141 1.9 Lucene学习总结之四 Lucene索引过程分析(4) for(int j=0;j final int code = prox.readvint(); position += code >> 1; final int payloadlength; // 如果此位置有payload信息 则从bytepool中读出 否则设为零 if ((code & 1)!= 0) { payloadlength = prox.readvint(); if (payloadbuffer == null payloadbuffer.length < payloadlength) payloadbuffer = new byte[payloadlength]; prox.readbytes(payloadbuffer, 0, payloadlength); else payloadlength = 0; //添加位置(prox)信息 posconsumer.addposition(position, payloadbuffer, 0, payloadlength); posconsumer.finish(); (2-3-4) 判断退出条件 上次选中的域取得下一个文档号 如果没有 则说明此域包含此词的文档已经处理完毕 除此域 并将numToMerge减一 然后此域取得下一个词 当循环到(2)的时候 表明此域已经开始处理下一个词 明此域中的所有的词都处理完毕 则从mergeStates中删除此域 并将numFields减一 当numFields为0的时候 if (!minstate.nextdoc()) {//获得下一个docid //如果此域包含此词的文档已经没有下一篇docid 则从数组termStates中移除 numtomerge减一 int upto = 0; 第 141 / 199 页

142 1.9 Lucene学习总结之四 Lucene索引过程分析(4) for(int i=0;i if (termstates[i]!= minstate) termstates[upto++] = termstates[i]; numtomerge--; //此域则取下一个词(term) 在循环(2)处来参与下一个词的合并 if (!minstate.nextterm()) { //如果此域没有下一个词了 则此域从数组mergeStates中移除 numfields减一 upto = 0; for(int i=0;i if (mergestates[i]!= minstate) mergestates[upto++] = mergestates[i]; numfields--; (2-4) 经过上面的过程 docid和freq信息虽已经写入段文件 而跳表信息并没有写到文件中 而是写入skip bu 写入文件 并且词典(tii, tis)也应该写入文件 docconsumer(formatpostingsdocswriter).finish(); termsconsumer.finish(); (2-3-4) 获得下一篇文档号代码如下 第 142 / 199 页

143 1.9 Lucene学习总结之四 Lucene索引过程分析(4) public boolean nextdoc() {//如何获取下一个docid if (freq.eof()) {//如果bytepool中的freq信息已经读完 if (p.lastdoccode!= -1) {//由上述缓存管理 PostingList里面还存着最后一篇文档的文档号及词频信息 则将最 docid = p.lastdocid; if (!field.omittermfreqandpositions) termfreq = p.docfreq; p.lastdoccode = -1; return true; else return false;//没有下一篇文档 final int code = freq.readvint();//如果bytepool中的freq信息尚未读完 if (field.omittermfreqandpositions) docid += code; else { //读出文档号及词频信息 docid += code >>> 1; if ((code & 1)!= 0) termfreq = 1; else termfreq = freq.readvint(); 第 143 / 199 页

144 1.9 Lucene学习总结之四 Lucene索引过程分析(4) return true; (2-3-2) 添加文档号及词频信息代码如下 FormatPostingsPositionsConsumer FormatPostingsDocsWriter.addDoc(int docid, int termdocfreq) { final int delta = docid - lastdocid; //当文档数量达到skipInterval倍数的时候 添加跳表项 if ((++df % skipinterval) == 0) { skiplistwriter.setskipdata(lastdocid, storepayloads, poswriter.lastpayloadlength); skiplistwriter.bufferskip(df); lastdocid = docid; if (omittermfreqandpositions) out.writevint(delta); else if (1 == termdocfreq) out.writevint((delta<<1) 1); else { //写入文档号及词频信息 out.writevint(delta<<1); out.writevint(termdocfreq); return poswriter; 第 144 / 199 页

145 1.9 Lucene学习总结之四 Lucene索引过程分析(4) (2-3-3) 添加位置信息 FormatPostingsPositionsWriter.addPosition(int position, byte[] payload, int payloadoffset, int payloadleng final int delta = position - lastposition; lastposition = position; if (storepayloads) { //保存位置及payload信息 if (payloadlength!= lastpayloadlength) { lastpayloadlength = payloadlength; out.writevint((delta<<1) 1); out.writevint(payloadlength); else out.writevint(delta << 1); if (payloadlength > 0) out.writebytes(payload, payloadlength); else out.writevint(delta); (2-4) 将跳表和词典(tii, tis)写入文件 FormatPostingsDocsWriter.finish() { 第 145 / 199 页

146 1.9 Lucene学习总结之四 Lucene索引过程分析(4) //将跳表缓存写入文件 long skippointer = skiplistwriter.writeskip(out); if (df > 0) { //将词典(terminfo)写入tii,tis文件 parent.termsout(terminfoswriter).add(fieldinfo.number, utf8.result, utf8.length, terminfo); 将跳表缓存写入文件 DefaultSkipListWriter(MultiLevelSkipListWriter).writeSkip(IndexOutput) { long skippointer = output.getfilepointer(); if (skipbuffer == null skipbuffer.length == 0) return skippointer; //正如我们在索引文件格式中分析的那样 高层在前 低层在后 除最低层外 其他的层都有长度保存 for (int level = numberofskiplevels - 1; level > 0; level--) { long length = skipbuffer[level].getfilepointer(); if (length > 0) { output.writevlong(length); skipbuffer[level].writeto(output); //写入最低层 skipbuffer[0].writeto(output); return skippointer; 第 146 / 199 页

147 1.9 Lucene学习总结之四 Lucene索引过程分析(4) 将词典(terminfo)写入tii,tis文件 tii文件是tis文件的类似跳表的东西 是在tis文件中每隔indexInterval个词提取出一个词放在tii文件中 以便很快的查找到词 因而TermInfosWriter类型中有一个成员变量other也是TermInfosWriter类型的 还有一个成员变量 isindex来表示此对象是用来写tii文件的还是用来写tis文件的 如果一个TermInfosWriter对象的isIndex=false则 它是用来写tis文件的 它的other指向的是用来写 tii文件的terminfoswriter对象 如果一个TermInfosWriter对象的isIndex=true则 它是用来写tii文件的 它的other指向的是用来写 tis文件的terminfoswriter对象 TermInfosWriter.add (int fieldnumber, byte[] termbytes, int termbyteslength, TermInfo ti) { //如果词的总数是indexInterval的倍数 则应该写入tii文件 if (!isindex && size % indexinterval == 0) other.add(lastfieldnumber, lasttermbytes, lasttermbyteslength, lastti); //将词写入tis文件 writeterm(fieldnumber, termbytes, termbyteslength); output.writevint(ti.docfreq); // write doc freq output.writevlong(ti.freqpointer - lastti.freqpointer); // write pointers output.writevlong(ti.proxpointer - lastti.proxpointer); if (ti.docfreq >= skipinterval) { output.writevint(ti.skipoffset); if (isindex) { output.writevlong(other.output.getfilepointer() - lastindexpointer); lastindexpointer = other.output.getfilepointer(); // write pointer 第 147 / 199 页

148 1.9 Lucene学习总结之四 Lucene索引过程分析(4) lastfieldnumber = fieldnumber; lastti.set(ti); size++; 写入词向量信息 代码为 TermVectorsTermsWriter.flush (Map> threadsandfields, final SegmentWriteState state) { if (tvx!= null) { if (state.numdocsinstore > 0) fill(state.numdocsinstore - docwriter.getdocstoreoffset()); tvx.flush(); tvd.flush(); tvf.flush(); for (Map.Entry> entry : threadsandfields.entrys for (final TermsHashConsumerPerField field : entry.getvalue() ) { TermVectorsTermsWriterPerField perfield = (TermVectorsTermsWriterPerField) field; perfield.termshashperfield.reset(); perfield.shrinkhash(); 第 148 / 199 页

149 1.9 Lucene学习总结之四 Lucene索引过程分析(4) TermVectorsTermsWriterPerThread perthread = (TermVectorsTermsWriterPerThread) entry.getkey(); perthread.termshashperthread.reset(true); 从代码中可以看出 是写入tvx, tvd, tvf三个文件 但是在上述的closeDocStore已经写入了 并且把tvx设为 null 在这里其实什么也不做 仅仅是清空postingsHash 以便进行下一轮索引时重用此对象 写入标准化因子 代码为 NormsWriter.flush(Map> threadsandfields, SegmentWriteState state) { final Map> byfield = new HashMap>(); for (final Map.Entry> entry : threadsandfields.entryset()) { //遍历所有的域 将同名域对应的NormsWriterPerField放到同一个链表中 final Collection fields = entry.getvalue(); final Iterator fieldsit = fields.iterator(); while (fieldsit.hasnext()) { final NormsWriterPerField perfield = (NormsWriterPerField) fieldsit.next(); List l = byfield.get(perfield.fieldinfo); if (l == null) { l = new ArrayList(); byfield.put(perfield.fieldinfo, l); 第 149 / 199 页

150 1.9 Lucene学习总结之四 Lucene索引过程分析(4) l.add(perfield); //记录写入的文件名 方便以后生成cfs文件 final String normsfilename = state.segmentname + "." + IndexFileNames.NORMS_EXTENSION; state.flushedfiles.add(normsfilename); IndexOutput normsout = state.directory.createoutput(normsfilename); try { //写入nrm文件头 normsout.writebytes(segmentmerger.norms_header, 0, SegmentMerger.NORMS_HEADER.length); final int numfield = fieldinfos.size(); int normcount = 0; //对每一个域进行处理 for(int fieldnumber=0;fieldnumber final FieldInfo fieldinfo = fieldinfos.fieldinfo(fieldnumber); //得到同名域的链表 List tomerge = byfield.get(fieldinfo); int upto = 0; if (tomerge!= null) { final int numfields = tomerge.size(); normcount++; final NormsWriterPerField[] fields = new NormsWriterPerField[numFields]; int[] uptos = new int[numfields]; 第 150 / 199 页

151 1.9 Lucene学习总结之四 Lucene索引过程分析(4) for(int j=0;j fields[j] = tomerge.get(j); int numleft = numfields; //处理同名的多个域 while(numleft > 0) { //得到所有的同名域中最小的文档号 int minloc = 0; int mindocid = fields[0].docids[uptos[0]]; for(int j=1;j final int docid = fields[j].docids[uptos[j]]; if (docid < mindocid) { mindocid = docid; minloc = j; // 在nrm文件中 每一个文件都有一个位置 没有设定的 放入默认值 for (;upto<mindocid;upto++) normsout.writebyte(defaultnorm); //写入当前的nrm值 normsout.writebyte(fields[minloc].norms[uptos[minloc]]); (uptos[minloc])++; upto++; //如果当前域的文档已经处理完毕 则numLeft减一 归零时推出循环 第 151 / 199 页

152 1.9 Lucene学习总结之四 Lucene索引过程分析(4) if (uptos[minloc] == fields[minloc].upto) { fields[minloc].reset(); if (minloc!= numleft-1) { fields[minloc] = fields[numleft-1]; uptos[minloc] = uptos[numleft-1]; numleft--; // 对所有的未设定nrm值的文档写入默认值 for(;upto normsout.writebyte(defaultnorm); else if (fieldinfo.isindexed &&!fieldinfo.omitnorms) { normcount++; // Fill entire field with default norm: for(;upto normsout.writebyte(defaultnorm); finally { normsout.close(); 第 152 / 199 页

153 1.9 Lucene学习总结之四 Lucene索引过程分析(4) 写入域元数据 代码为 FieldInfos.write(IndexOutput) { output.writevint(current_format); output.writevint(size()); for (int i = 0; i < size(); i++) { FieldInfo fi = fieldinfo(i); byte bits = 0x0; if (fi.isindexed) bits = IS_INDEXED; if (fi.storetermvector) bits = STORE_TERMVECTOR; if (fi.storepositionwithtermvector) bits = STORE_POSITIONS_WITH_TERMVECTOR; if (fi.storeoffsetwithtermvector) bits = STORE_OFFSET_WITH_TERMVECTOR; if (fi.omitnorms) bits = OMIT_NORMS; if (fi.storepayloads) bits = STORE_PAYLOADS; if (fi.omittermfreqandpositions) bits = OMIT_TERM_FREQ_AND_POSITIONS; output.writestring(fi.name); output.writebyte(bits); 此处基本就是按照fnm文件的格式写入的 6.3 生成新的段信息对象 代码 第 153 / 199 页

154 1.9 Lucene学习总结之四 Lucene索引过程分析(4) newsegment = new SegmentInfo(segment, flusheddoccount, directory, false, true, docstoreoffset, docsto docstoreiscompoundfile, docwriter.hasprox()); segmentinfos.add(newsegment); 6.4 准备删除文档 代码 docwriter.pushdeletes(); --> deletesflushed.update(deletesinram); 此处将deletesInRAM全部加到deletesFlushed中 并把deletesInRAM清空 原因上面已经阐明 6.5 生成cfs段 代码 docwriter.createcompoundfile(segment); newsegment.setusecompoundfile(true); 代码为 DocumentsWriter.createCompoundFile(String segment) { CompoundFileWriter cfswriter = new CompoundFileWriter(directory, segment + "." + IndexFileNames.COMPOUND_FILE_EXTENSION); //将上述中记录的文档名全部加入cfs段的写对象 for (final String flushedfile : flushstate.flushedfiles) cfswriter.addfile(flushedfile); 第 154 / 199 页

155 1.9 Lucene学习总结之四 Lucene索引过程分析(4) cfswriter.close(); 6.6 删除文档 代码 applydeletes(); 代码为 boolean applydeletes(segmentinfos infos) { if (!hasdeletes()) return false; final int infosend = infos.size(); int docstart = 0; boolean any = false; for (int i = 0; i < infosend; i++) { assert infos.info(i).dir == directory; SegmentReader reader = writer.readerpool.get(infos.info(i), false); try { any = applydeletes(reader, docstart); docstart += reader.maxdoc(); finally { writer.readerpool.release(reader); 第 155 / 199 页

156 1.9 Lucene学习总结之四 Lucene索引过程分析(4) deletesflushed.clear(); return any; Lucene删除文档可以用reader 也可以用writer 但是归根结底还是用reader来删除的 reader的删除有以下三种方式 按照词删除 删除所有包含此词的文档 按照文档号删除 按照查询对象删除 删除所有满足此查询的文档 但是这三种方式归根结底还是按照文档号删除 也就是写.del文件的过程 private final synchronized boolean applydeletes(indexreader reader, int docidstart) throws CorruptIndexException, IOException { final int docend = docidstart + reader.maxdoc(); boolean any = false; //按照词删除 删除所有包含此词的文档 TermDocs docs = reader.termdocs(); try { for (Entry entry: deletesflushed.terms.entryset()) { Term term = entry.getkey(); docs.seek(term); int limit = entry.getvalue().getnum(); while (docs.next()) { int docid = docs.doc(); 第 156 / 199 页

157 1.9 Lucene学习总结之四 Lucene索引过程分析(4) if (docidstart+docid >= limit) break; reader.deletedocument(docid); any = true; finally { docs.close(); //按照文档号删除 for (Integer docidint : deletesflushed.docids) { int docid = docidint.intvalue(); if (docid >= docidstart && docid < docend) { reader.deletedocument(docid-docidstart); any = true; //按照查询对象删除 删除所有满足此查询的文档 IndexSearcher searcher = new IndexSearcher(reader); for (Entry entry : deletesflushed.queries.entryset()) { Query query = entry.getkey(); int limit = entry.getvalue().intvalue(); Weight weight = query.weight(searcher); 第 157 / 199 页

158 Scorer scorer = weight.scorer(reader, true, false); if (scorer!= null) { while(true) { int doc = scorer.nextdoc(); if (((long) docidstart) + doc >= limit) break; reader.deletedocument(doc); any = true; searcher.close(); return any; 第 158 / 199 页 1.9 Lucene学习总结之四 Lucene索引过程分析(4)

159 2.1 有关Lucene的问题(1):为什么能搜的到 中华 AND 共和国 却搜不到 中华共和国? 2.1 有关Lucene的问题(1):为什么能搜的到 中华 AND 共和国 却搜不到 中华 共和国? 发表时间: 问题 使用中科院的中文分词对 中华人民共和国 进行索引 它被分词为"中华", "人民", "共和国" 用 人民共和 国 进行搜索 可以搜到 而搜索"中华共和国"却搜索不到 用 中华 AND 共和国 却可以搜出来 为什么 回答 我下载了 如果索引的时候 中华人民共和国 被分成了 中华 人民 共和国 而搜索的时候 搜 中华共和国 则被分为了 中华 共和国 然 而构建Query Parser构建Query Object的时候 却将它构建成了PhraseQuery contents:"中华 共和国" 而非BooleanQuery contents:中华 contents:共和国 根据PhraseQuery的解释 它有一个参数slop来 表示两个词之间的距离 默认为0 也即只有在文档不但包含 中华 而且包含 共和国 并且二者相邻的时候 才能返回 这就是为什么 人民共和国 可以搜出来(它构建的是PhraseQuery 但是相邻) 中华 AND 共和 国 能搜索出来(它构建的是BooleanQuery) 而 中华共和国 搜不出来的原因(它构建的是PhraseQuery 但不相邻) 尝试解析Query query = parser.parse("\"中华共和国\"~1") 或者用API设置Slop为1 就能搜索出结果了 Query query = parser.parse("中华共和国"); PhraseQuery pquery = (PhraseQuery)query; pquery.setslop(1); 实例 Analyzer ca = new ChineseAnalyzer(); QueryParser parser = new QueryParser(field, ca); Query query1 = parser.parse("人民共和国"); System.out.println("Searching for: " + query1.tostring(field)); 查询对象为 第 159 / 199 页

160 query1 2.1 有关Lucene的问题(1):为什么能搜的到 中华 AND 共和国 却搜不到 中华共和国? PhraseQuery (id=39) boost field 1.0 "contents" maxposition positions slop 1 ArrayList<E> (id=45) 0 terms ArrayList<E> (id=49) elementdata [0] Object[4] (id=74) Term (id=76) field "contents" text "人民" [1] Term (id=77) field "contents" text "共和国" 相当于查询语句 Searching for: "人民 共和国" Query query2 = parser.parse("中华 AND 共和国"); System.out.println("Searching for: " + query2.tostring(field)); 查询对象为 query2 BooleanQuery (id=43) boost 1.0 clauses ArrayList<E> (id=56) elementdata [0] Object[10] (id=57) BooleanClause (id=59) occur BooleanClause$Occur (id=62) name query "MUST" TermQuery (id=65) boost term 第 160 / 199 页 1.0 Term (id=70) field "contents" text "中华"

161 [1] 2.1 有关Lucene的问题(1):为什么能搜的到 中华 AND 共和国 却搜不到 中华共和国? BooleanClause (id=61) occur BooleanClause$Occur (id=62) name query "MUST" TermQuery (id=64) boost term 1.0 Term (id=68) field "contents" text "共和国" 相当于查询语句 Searching for: +中华 +共和国 Query query3 = parser.parse("\"中华共和国\"~1"); System.out.println("Searching for: " + query3.tostring(field)); 查询对象为 query3 PhraseQuery (id=54) boost field 1.0 "contents" maxposition positions slop 1 ArrayList<E> (id=93) 1 terms ArrayList<E> (id=94) elementdata [0] Object[4] (id=96) Term (id=97) field "contents" text "中华" [1] Term (id=98) field "contents" text "共和国" 相当于查询语句 Searching for: "中华 共和国"~1 第 161 / 199 页

162 2.1 有关Lucene的问题(1):为什么能搜的到 中华 AND 共和国 却搜不到 中华共和国? Query query4 = parser.parse("中华共和国"); PhraseQuery pquery = (PhraseQuery)query4; pquery.setslop(1); System.out.println("Searching for: " + query4.tostring(field)); 查询对象为 query4 PhraseQuery (id=55) boost field 1.0 "contents" maxposition positions slop 1 ArrayList<E> (id=102) 1 terms ArrayList<E> (id=103) elementdata [0] Object[4] (id=105) Term (id=107) field "contents" text "中华" [1] Term (id=108) field "contents" text "共和国" 相当于查询语句 Searching for: "中华 共和国"~1 第 162 / 199 页

163 2.2 有关Lucene的问题(2):stemming和lemmatization 2.2 有关Lucene的问题(2):stemming和lemmatization 发表时间: 问题 我试验了一下文章中提到的 stemming 和 lemmatization 将单词缩减为词根形式 如 cars 到 car 等 这种操作称为 stemming 将单词转变为词根形式 如 drove 到 drive 等 这种操作称为 lemmatization 试验没有成功 代码如下 public class TestNorms { public void createindex() throws IOException { Directory d = new SimpleFSDirectory(new File("d:/falconTest/lucene3/norms")); IndexWriter writer = new IndexWriter(d, new StandardAnalyzer(Version.LUCENE_30), true, IndexWriter.MaxFieldLength.UNLIMITED); Field field = new Field("desc", "", Field.Store.YES, Field.Index.ANALYZED); Document doc = new Document(); field.setvalue("hello students was drive"); doc.add(field); writer.adddocument(doc); writer.optimize(); writer.close(); public void search() throws IOException { Directory d = new SimpleFSDirectory(new File("d:/falconTest/lucene3/norms")); IndexReader reader = IndexReader.open(d); IndexSearcher searcher = new IndexSearcher(reader); TopDocs docs = searcher.search(new TermQuery(new Term("desc","drove")), 10); System.out.println(docs.totalHits); public static void main(string[] args) throws IOException { TestNorms test= new TestNorms(); 第 163 / 199 页

164 2.2 有关Lucene的问题(2):stemming和lemmatization test.createindex(); test.search(); 不管是单复数 还是单词的变化 都是没有体现的 不知道是不是分词器的原因 回答 的确是分词器的问题 StandardAnalyzer并不能进行stemming和lemmatization 因而不能够区分单复数和 词型 文章中讲述的是全文检索的基本原理 理解了他 有利于更好的理解Lucene 但不代表Lucene是完全按照此基 本流程进行的 (1) 有关stemming 作为stemming 一个著名的算法是The Porter Stemming Algorithm 其主页为 ~martin/porterstemmer/ 也可查看其论文 通过以下网页可以进行简单的测试 Porter's Stemming Algorithm Online[ mobasher/classes/csc575/porter.html] cars > car driving > drive tokenization > token 第 164 / 199 页

165 2.2 有关Lucene的问题(2):stemming和lemmatization 然而 drove > drove 可见stemming是通过规则缩减为词根的 而不能识别词型的变化 在最新的Lucene 3.0中 已经有了PorterStemFilter这个类来实现上述算法 只可惜没有Analyzer向匹配 不 过不要紧 我们可以简单实现 public class PorterStemAnalyzer extends Analyzer public TokenStream tokenstream(string fieldname, Reader reader) { return new PorterStemFilter(new LowerCaseTokenizer(reader)); 把此分词器用在你的程序中 就能够识别单复数和规则的词型变化了 public void createindex() throws IOException { Directory d = new SimpleFSDirectory(new File("d:/falconTest/lucene3/norms")); IndexWriter writer = new IndexWriter(d, new PorterStemAnalyzer(), true, IndexWriter.MaxFieldLength.UNLIMITED); Field field = new Field("desc", "", Field.Store.YES, Field.Index.ANALYZED); Document doc = new Document(); field.setvalue("hello students was driving cars professionally"); doc.add(field); writer.adddocument(doc); writer.optimize(); writer.close(); public void search() throws IOException { Directory d = new SimpleFSDirectory(new File("d:/falconTest/lucene3/norms")); IndexReader reader = IndexReader.open(d); IndexSearcher searcher = new IndexSearcher(reader); 第 165 / 199 页

166 2.2 有关Lucene的问题(2):stemming和lemmatization TopDocs docs = searcher.search(new TermQuery(new Term("desc", "car")), 10); System.out.println(docs.totalHits); docs = searcher.search(new TermQuery(new Term("desc", "drive")), 10); System.out.println(docs.totalHits); docs = searcher.search(new TermQuery(new Term("desc", "profession")), 10); System.out.println(docs.totalHits); (2) 有关lemmatization 至于lemmatization 一般是有字典的 方能够由"drove"对应到"drive". 在网上搜了一下 找到European languages lemmatizer[ 只不过是在linux下面 C++开发的 有兴趣可以试验一下 首先按照网站的说明下载 编译 安装 libmafsa is the core of the lemmatizer. All other libraries depend on it. Download the last version from the following page, unpack it and compile: # tar xzf libmafsa-0.2.tar.gz # cd libmafsa-0.2/ # cmake. # make # sudo make install After this you should install libturglem. You can download it at the same place. # tar xzf libturglem-0.2.tar.gz # cd libturglem-0.2 # cmake. # make # sudo make install Next you should install english dictionaries with some additional features to work with. 第 166 / 199 页

167 2.2 有关Lucene的问题(2):stemming和lemmatization # tar xzf turglem-english-0.2.tar.gz # cd turglem-english-0.2 # cmake. # make # sudo make install 安装完毕后 /usr/local/include/turglem是头文件 用于编译自己编写的代码 /usr/local/share/turglem/english是字典文件 其中lemmas.xml中我们可以看到"drove"和"drive"的 对应 "was"和"be"的对应 /usr/local/lib中的libmafsa.a libturglem.a libturglem-english.a libtxml.a是用于生成应用程序的 静态库 <l id="drive" p="6" /> <l id="drove" p="6" /> <l id="driving" p="6" /> 在turglem-english-0.2目录下有例子测试程序test_utf8.cpp 第 167 / 199 页

168 2.2 有关Lucene的问题(2):stemming和lemmatization #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <turglem/lemmatizer.h> #include <turglem/lemmatizer.hpp> #include <turglem/english/charset_adapters.hpp> int main(int argc, char **argv) { char in_s_buf[1024]; char *nl_ptr; tl::lemmatizer lem; if(argc!= 4) { printf("usage: %s words.dic predict.dic flexias.bin\n", argv[0]); return -1; lem.load_lemmatizer(argv[1], argv[3], argv[2]); while (!feof(stdin)) { fgets(in_s_buf, 1024, stdin); nl_ptr = strchr(in_s_buf, '\n'); if (nl_ptr) *nl_ptr = 0; nl_ptr = strchr(in_s_buf, '\r'); if (nl_ptr) *nl_ptr = 0; if (in_s_buf[0]) { printf("processing %s\n", in_s_buf); tl::lem_result pars; size_t pcnt = lem.lemmatize<english_utf8_adapter>(in_s_buf, pars); printf("%d\n", pcnt); for (size_t i = 0; i < pcnt; i++) { 第 168 / 199 页

169 2.2 有关Lucene的问题(2):stemming和lemmatization std::string s; u_int32_t src_form = lem.get_src_form(pars, i); s = lem.get_text<english_utf8_adapter>(pars, i, 0); printf("paradigm %d: normal form '%s'\n", (unsigned int)i, s.c_str()); printf("\tpart of speech:%d\n", lem.get_part_of_speech(pars, (unsigned int)i, src_form)); return 0; 编译此文件 并且链接静态库 注意链接顺序 否则可能出错 g++ -g -o output test_utf8.cpp -L/usr/local/lib/ -lturglem-english -lturglem -lmafsa ltxml 运行编译好的程序./output /usr/local/share/turglem/english/dict_english.auto /usr/local/share/turglem/english/prediction_english.auto /usr/local/share/turglem/english/paradigms_english.bin 做测试 虽然对其机制尚不甚了解 但是可以看到lemmatization的作用 第 169 / 199 页

170 drove processing drove 3 PARADIGM 0: normal form 'DROVE' part of speech:0 PARADIGM 1: normal form 'DROVE' part of speech:2 PARADIGM 2: normal form 'DRIVE' part of speech:2 was processing was 3 PARADIGM 0: normal form 'BE' part of speech:3 PARADIGM 1: normal form 'BE' part of speech:3 PARADIGM 2: normal form 'BE' part of speech:3 第 170 / 199 页 2.2 有关Lucene的问题(2):stemming和lemmatization

171 2.3 有关Lucene的问题(3): 向量空间模型与Lucene的打分机制 2.3 有关Lucene的问题(3): 向量空间模型与Lucene的打分机制 发表时间: 问题 在你的文章中提到了 于是我们把所有此文档中词(term)的权重(term weight) 看作一个向量 Document = {term1, term2,,term N Document Vector = {weight1, weight2,,weight N 同样我们把查询语句看作一个简单的文档 也用向量来表示 Query = {term1, term 2,, term N Query Vector = {weight1, weight2,, weight N 于是我们把所有此文档中词(term)的权重(term weight) 看作一个向量 Document = {term1, term2,,term NDocument Vector = {weight1, weight2,,weight N同样我们把查询语句看作一个简单的文档 也用向量来表示 Query = {term1, term 2,, term NQuery Vector = {weight1, weight2,, weight N 其中查询语句的term的权重如何定义的呢 在这里 既然要放到相同的向量空间 自然维数是相同的 不同时 取二者的并集 如果不含某个词(Term) 时 则权重(Term Weight)为0 为什么要取二者的并集 直接用查询语句向量的维度应该就可以吧 毕竟其他的字段也没有实际的用途哈 我 是这么想的 不知道行得通不 回答 对于第一个问题 文章中既然说 查询语句也看成一个很短很短的文档 则按理论来讲 term的权重也是应用 tf * idf进行计算的 但是由于query语句是一个特殊的文档 首先对于tf来讲 一般来说不会有人在查询中将一个词输入两次 因而 tf一般认为是1 而对于idf 是包含此term的文档总数 本应该包括query语句这篇很短的文档 但当文档数量达到一定的数目 的时候 这一篇对分数的影响也可忽略不计 因而idf为索引中包含此term的文档总数 第 171 / 199 页

172 2.3 有关Lucene的问题(3): 向量空间模型与Lucene的打分机制 所以对于score计算公式的分子部分来讲 设query向量为:<q(i), q(j)> 而document的向量<d(1),.., d(i),.., d(j),.., d(n)> 两者取点积后为q(i)*d(i) + q(j)*d(j) 其他项都是0. 到这里似乎看起来 和你感觉的一样 向量空间是n维同向量空间是2维是一样的 这里说一些题外话 有利于理解lucene score的公式的由来和推理 将其分解为df*idf后为 tf(i in q)*idf(i in q) * tf(i in d) * idf(i in d) + tf(j in q) * idf(j in q) * tf(j in d) * idf(j in d) 由上面的分析我们知道 对于query tf为1 所以tf(i in q) = tf(j in q) = 1 而idf对query和document都是一 样的 都是不计算query这个短文档的数目的 因而idf(i in q) = idf(i in d) idf(j in q) = idf(j in d) 于是上面的公式变成 tf(i in d)*idf(i)^2 + tf(j in d)* idf(j)^2 这就是为什么Lucene的score计算公式里有tf乘 以idf平方的样子: score(q,d) = coord(q,d) querynorm(q) ( tf(t in d) idf(t) 2 t.getboost() norm(t,d) ) t in q 然而在score的计算部分 除了分子部分 还有分母部分 也即两个向量都要除以自己的长度 从这个角度来 讲 向量空间是n维和2维算出来的向量长度差别就很大了 所以必须要取两者的并集 那么为什么要除以向量的长度呢 在这里叫做归一化处理 因为在索引中 不同的文档长度不一样 很显然 对于任意一个term 在长的文档中的tf要大的多 因而分数也越高 这样对小的文档不公平 举一个极端的例 子 在一篇1000万个词的鸿篇巨著中 "lucene"这个词出现了11次 而在一篇12个词的短小文档 中 "lucene"这个词出现了10次 如果不考虑长度在内 当然鸿篇巨著应该分数更高 然而显然这篇小文档才 是真正关注"lucene"的 在Lucene的score公式中 query语句长度的归一化是在querynorm中体现的 querynorm(q) = querynorm(sumofsquaredweights) = 1/sumOfSquaredWeights 第 172 / 199 页 ½

173 2.3 有关Lucene的问题(3): 向量空间模型与Lucene的打分机制 sumofsquaredweights = q.getboost() 2 ( idf(t) t.getboost() ) 2 t in q 除了boost之外 querynorm = 1/sqrt(q(i) ^ 2 + q(j) ^2) 而q(x) = df(x) * idf(x) 对于query来讲 df(x) = 1 因而queryNorm成了 查询词的idf值的平方和的开方后的值分之一 document的长度归一化是在 norm(t,d)中体现的 norm(t,d) = doc.getboost() lengthnorm(field) f.getboost() field f in d named as t 其中文档的boost和field的boost在nrm文件中的一节已经讲过 表示某篇文章的某个域可能更重要一些 也可 能更不重要一些 而lengthNorm(field)正是文档长度的归一化的体现 默认的DefaultSimilarity的实现中 lengthnorm如下计算 public float lengthnorm(string fieldname, int numterms) { return (float)(1.0 / Math.sqrt(numTerms)); 我们会发现 lengthnorm的计算并不是按照经典理论 是向量长度分之一 而是文档长度开方分之一 也即忽 略了每个term的权重 认为每个term的权重都是1 方才能够得出上述的公式 Lucene之所以这样做可能主要 考虑两点 首先 不想完全抛弃文件长度的影响 否则又对长文档不公平 毕竟它是包含了更多的信息 我们可以 简单的做个试验可以得知 即便是现在这个公式 lucene还是偏向于首先返回短小的文档的 这样在实 际应用中使得搜索结果很难看 其次 此接口是开放出来 在Similarity中的 用户可以根据自己应用的需要 改写lengthNorm的计算 公式 比如我想做一个经济学论文的搜索系统 经过一定时间的调研 发现大多数的经济学论文的长度 第 173 / 199 页

174 2.3 有关Lucene的问题(3): 向量空间模型与Lucene的打分机制 在8000到10000词 因而lengthNorm的公式应该是一个倒抛物线型的 8000到10000词的论文分数 最高 更短或更长的分数都应该偏低 方能够返回给用户最好的数据 第 174 / 199 页

175 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 发表时间: 在索引阶段设置Document Boost和Field Boost 存储在(.nrm)文 件中 如果希望某些文档和某些域比其他的域更重要 如果此文档和此域包含所要查询的词则应该得分较高 则可以 在索引阶段设定文档的boost和域的boost值 这些值是在索引阶段就写入索引文件的 存储在标准化因子(.nrm)文件中 一旦设定 除非删除此文档 否则无 法改变 如果不进行设定 则Document Boost和Field Boost默认为1 Document Boost及FieldBoost的设定方式如下 Document doc = new Document(); Field f = new Field("contents", "hello world", Field.Store.NO, Field.Index.ANALYZED); f.setboost(100); doc.add(f); doc.setboost(100); 两者是如何影响Lucene的文档打分的呢 让我们首先来看一下Lucene的文档打分的公式 score(q,d) = coord(q,d) querynorm(q) ( tf(t in d) idf(t) 2 t.getboost() norm(t,d) t in q Document Boost和Field Boost影响的是norm(t, d) 其公式如下 norm(t,d) = doc.getboost() lengthnorm(field) 第 175 / 199 页 f.getboost() )

176 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 field f in d named as t 它包括三个参数 Document boost 此值越大 说明此文档越重要 Field boost 此域越大 说明此域越重要 lengthnorm(field) = (1.0 / Math.sqrt(numTerms)) 一个域中包含的Term总数越多 也即文档越 长 此值越小 文档越短 此值越大 其中第三个参数可以在自己的Similarity中影响打分 下面会论述 当然 也可以在添加Field的时候 设置Field.Index.ANALYZED_NO_NORMS或 Field.Index.NOT_ANALYZED_NO_NORMS 完全不用norm 来节约空间 根据Lucene的注释 No norms means that index-time field and document boosting and field length normalization are disabled. The benefit is less memory usage as norms take up one byte of RAM per indexed field for every document in the index, during searching. Note that once you index a given field with norms enabled, disabling norms will have no effect. 没有norms意味着索引阶段禁用了文档 boost和域的boost及长度标准化 好处在于节省内存 不用在搜索阶段为索引中的每篇文档的每个域都占用一 个字节来保存norms信息了 但是对norms信息的禁用是必须全部域都禁用的 一旦有一个域不禁用 则其他 禁用的域也会存放默认的norms值 因为为了加快norms的搜索速度 Lucene是根据文档号乘以每篇文档的 norms信息所占用的大小来计算偏移量的 中间少一篇文档 偏移量将无法计算 也即norms信息要么都保 存 要么都不保存 下面几个试验可以验证norms信息的作用 试验一 Document Boost的作用 public void testnormsdocboost() throws Exception { File indexdir = new File("testNormsDocBoost"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setusecompoundfile(false); Document doc1 = new Document(); Field f1 = new Field("contents", "common hello hello", Field.Store.NO, Field.Index.ANALYZED); doc1.add(f1); doc1.setboost(100); writer.adddocument(doc1); 第 176 / 199 页

177 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 Document doc2 = new Document(); Field f2 = new Field("contents", "common common hello", Field.Store.NO, Field.Index.ANALYZED_NO_NO doc2.add(f2); writer.adddocument(doc2); Document doc3 = new Document(); Field f3 = new Field("contents", "common common common", Field.Store.NO, Field.Index.ANALYZED_NO doc3.add(f3); writer.adddocument(doc3); writer.close(); IndexReader reader = IndexReader.open(FSDirectory.open(indexDir)); IndexSearcher searcher = new IndexSearcher(reader); TopDocs docs = searcher.search(new TermQuery(new Term("contents", "common")), 10); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 如果第一篇文档的域f1也为Field.Index.ANALYZED_NO_NORMS的时候 搜索排名如下 docid : 2 score : docid : 1 score : docid : 0 score : 如果第一篇文档的域f1设为Field.Index.ANALYZED 则搜索排名如下 docid : 0 score : docid : 2 score : docid : 1 score : 试验二 Field Boost的作用 如果我们觉得title要比contents要重要 可以做一下设定 第 177 / 199 页

178 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 public void testnormsfieldboost() throws Exception { File indexdir = new File("testNormsFieldBoost"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setusecompoundfile(false); Document doc1 = new Document(); Field f1 = new Field("title", "common hello hello", Field.Store.NO, Field.Index.ANALYZED); f1.setboost(100); doc1.add(f1); writer.adddocument(doc1); Document doc2 = new Document(); Field f2 = new Field("contents", "common common hello", Field.Store.NO, Field.Index.ANALYZED_NO_NO doc2.add(f2); writer.adddocument(doc2); writer.close(); IndexReader reader = IndexReader.open(FSDirectory.open(indexDir)); IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "contents", new StandardAnalyzer(Version.LUCENE_CURRENT)); Query query = parser.parse("title:common contents:common"); TopDocs docs = searcher.search(query, 10); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 如果第一篇文档的域f1也为Field.Index.ANALYZED_NO_NORMS的时候 搜索排名如下 docid : 1 score : docid : 0 score : 如果第一篇文档的域f1设为Field.Index.ANALYZED 则搜索排名如下 第 178 / 199 页

179 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 docid : 0 score : docid : 1 score : 试验三 norms中文档长度对打分的影响 public void testnormslength() throws Exception { File indexdir = new File("testNormsLength"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setusecompoundfile(false); Document doc1 = new Document(); Field f1 = new Field("contents", "common hello hello", Field.Store.NO, Field.Index.ANALYZED_NO_NORMS doc1.add(f1); writer.adddocument(doc1); Document doc2 = new Document(); Field f2 = new Field("contents", "common common hello hello hello hello", Field.Store.NO, Field.Index.ANALYZED_NO_NORMS); doc2.add(f2); writer.adddocument(doc2); writer.close(); IndexReader reader = IndexReader.open(FSDirectory.open(indexDir)); IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "contents", new StandardAnalyzer(Version.LUCENE_CURRENT)); Query query = parser.parse("title:common contents:common"); TopDocs docs = searcher.search(query, 10); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 当norms被禁用的时候 包含两个common的第二篇文档打分较高 第 179 / 199 页

180 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 docid : 1 score : docid : 0 score : 当norms起作用的时候 虽然包含两个common的第二篇文档 由于长度较长 因而打分较低 docid : 0 score : docid : 1 score : 试验四 norms信息要么都保存 要么都不保存的特性 public void testomitnorms() throws Exception { File indexdir = new File("testOmitNorms"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); writer.setusecompoundfile(false); Document doc1 = new Document(); Field f1 = new Field("title", "common hello hello", Field.Store.NO, Field.Index.ANALYZED); doc1.add(f1); writer.adddocument(doc1); for (int i = 0; i < 10000; i++) { Document doc2 = new Document(); Field f2 = new Field("contents", "common common hello hello hello hello", Field.Store.NO, Field.Index.ANALYZED_NO_NORMS); doc2.add(f2); writer.adddocument(doc2); writer.close(); 当我们添加10001篇文档 所有的文档都设为Field.Index.ANALYZED_NO_NORMS的时候 我们看索引文 件 发现.nrm文件只有1K 也即其中除了保持一定的格式信息 并无其他数据 第 180 / 199 页

181 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 当我们把第一篇文档设为Field.Index.ANALYZED 而其他10000篇文档都设为 Field.Index.ANALYZED_NO_NORMS的时候 发现.nrm文件又10K 也即所有的文档都存储了norms信息 而非只有第一篇文档 在搜索语句中 设置Query Boost. 在搜索中 我们可以指定 某些词对我们来说更重要 我们可以设置这个词的boost common^4 hello 使得包含common的文档比包含hello的文档获得更高的分数 由于在Lucene中 一个Term定义为Field:Term 则也可以影响不同域的打分 title:common^4 content:common 使得title中包含common的文档比content中包含common的文档获得更高的分数 实例 第 181 / 199 页

182 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 public void testqueryboost() throws Exception { File indexdir = new File("TestQueryBoost"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED); Document doc1 = new Document(); Field f1 = new Field("contents", "common1 hello hello", Field.Store.NO, Field.Index.ANALYZED); doc1.add(f1); writer.adddocument(doc1); Document doc2 = new Document(); Field f2 = new Field("contents", "common2 common2 hello", Field.Store.NO, Field.Index.ANALYZED); doc2.add(f2); writer.adddocument(doc2); writer.close(); IndexReader reader = IndexReader.open(FSDirectory.open(indexDir)); IndexSearcher searcher = new IndexSearcher(reader); QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "contents", new StandardAnalyzer(Version.LUCENE_CURRENT)); Query query = parser.parse("common1 common2"); TopDocs docs = searcher.search(query, 10); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 根据tf/idf 包含两个common2的第二篇文档打分较高 docid : 1 score : docid : 0 score : 如果我们输入的查询语句为 "common1^100 common2" 则第一篇文档打分较高 docid : 0 score : docid : 1 score : 那Query Boost是如何影响文档打分的呢 第 182 / 199 页

183 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 根据Lucene的打分计算公式 score(q,d) = coord(q,d) querynorm(q) ( tf(t in d) idf(t) 2 t.getboost() norm(t,d) ) t in q 注 在queryNorm的部分 也有q.getBoost()的部分 但是对query向量的归一化(见向量空间模型与Lucene的 打分机制[/blog/588721]) 继承并实现自己的Similarity Similariy是计算Lucene打分的最主要的类 实现其中的很多借口可以干预打分的过程 (1) float computenorm(string field, FieldInvertState state) (2) float lengthnorm(string fieldname, int numtokens) (3) float querynorm(float sumofsquaredweights) (4) float tf(float freq) (5) float idf(int docfreq, int numdocs) (6) float coord(int overlap, int maxoverlap) (7) float scorepayload(int docid, String fieldname, int start, int end, byte [] payload, int offset, int length) 它们分别影响Lucene打分计算的如下部分 score(q,d) = (6)coord(q,d) (3)queryNorm(q) ( (4)tf(t in d) (5)idf(t) t in q norm(t,d) = doc.getboost() (2)lengthNorm(field) field f in d named as t 下面逐个进行解释 第 183 / 199 页 f.getboost() 2 t.getboost() (1)norm(t,d) )

184 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 (1) float computenorm(string field, FieldInvertState state) 影响标准化因子的计算 如上述 他主要包含了三部分 文档boost 域boost 以及文档长度归一化 此函数 一般按照上面norm(t, d)的公式进行计算 (2) float lengthnorm(string fieldname, int numtokens) 主要计算文档长度的归一化 默认是1.0 / Math.sqrt(numTerms) 因为在索引中 不同的文档长度不一样 很显然 对于任意一个term 在长的文档中的tf要大的多 因而分数 也越高 这样对小的文档不公平 举一个极端的例子 在一篇1000万个词的鸿篇巨著中 "lucene"这个词出现 了11次 而在一篇12个词的短小文档中 "lucene"这个词出现了10次 如果不考虑长度在内 当然鸿篇巨著应 该分数更高 然而显然这篇小文档才是真正关注"lucene"的 因而在此处是要除以文档的长度 从而减少因文档长度带来的打分不公 然而现在这个公式是偏向于首先返回短小的文档的 这样在实际应用中使得搜索结果也很难看 于是在实践中 要根据项目的需要 根据搜索的领域 改写lengthNorm的计算公式 比如我想做一个经济学论 文的搜索系统 经过一定时间的调研 发现大多数的经济学论文的长度在8000到10000词 因而lengthNorm 的公式应该是一个倒抛物线型的 8000到10000词的论文分数最高 更短或更长的分数都应该偏低 方能够返 回给用户最好的数据 (3) float querynorm(float sumofsquaredweights) 这是按照向量空间模型 对query向量的归一化 此值并不影响排序 而仅仅使得不同的query之间的分数可以 比较 (4) float tf(float freq) freq是指在一篇文档中包含的某个词的数目 tf是根据此数目给出的分数 默认为Math.sqrt(freq) 也即此项并 不是随着包含的数目的增多而线性增加的 (5) float idf(int docfreq, int numdocs) idf是根据包含某个词的文档数以及总文档数计算出的分数 默认为(Math.log(numDocs/ (double)(docfreq+1)) + 1.0) 由于此项计算涉及到总文档数和包含此词的文档数 因而需要全局的文档数信息 这给跨索引搜索造成麻烦 从下面的例子我们可以看出 用MultiSearcher来一起搜索两个索引和分别用IndexSearcher来搜索两个索引所 得出的分数是有很大差异的 第 184 / 199 页

185 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 究其原因是MultiSearcher的docFreq(Term term)函数计算了包含两个索引中包含此词的总文档数 而 IndexSearcher仅仅计算了每个索引中包含此词的文档数 当两个索引包含的文档总数是有很大不同的时候 分 数是无法比较的 public void testmultiindex() throws Exception{ MultiIndexSimilarity sim = new MultiIndexSimilarity(); File indexdir01 = new File("TestMultiIndex/TestMultiIndex01"); File indexdir02 = new File("TestMultiIndex/TestMultiIndex02"); IndexReader reader01 = IndexReader.open(FSDirectory.open(indexDir01)); IndexReader reader02 = IndexReader.open(FSDirectory.open(indexDir02)); IndexSearcher searcher01 = new IndexSearcher(reader01); searcher01.setsimilarity(sim); IndexSearcher searcher02 = new IndexSearcher(reader02); searcher02.setsimilarity(sim); MultiSearcher multiseacher = new MultiSearcher(searcher01, searcher02); multiseacher.setsimilarity(sim); QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "contents", new StandardAnalyzer(Version.LUCENE_CURRENT)); Query query = parser.parse("common"); TopDocs docs = searcher01.search(query, 10); System.out.println(" "); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); System.out.println(" "); docs = searcher02.search(query, 10); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); System.out.println(" "); docs = multiseacher.search(query, 20); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 结果为 第 185 / 199 页

186 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 docid : 0 score : docid : 1 score : docid : 2 score : docid : 3 score : docid : 4 score : docid : 5 score : docid : 6 score : docid : 7 score : docid : 0 score : docid : 1 score : docid : 2 score : docid : 3 score : docid : 4 score : docid : 0 score : docid : 1 score : docid : 2 score : docid : 3 score : docid : 4 score : docid : 5 score : docid : 6 score : docid : 7 score : docid : 8 score : docid : 9 score : docid : 10 score : docid : 11 score : docid : 12 score : 如果几个索引都是在一台机器上 则用MultiSearcher或者MultiReader就解决问题了 然而有时候索引是分布 在多台机器上的 虽然Lucene也提供了RMI 或用NFS保存索引的方法 然而效率和并行性一直是一个问题 一个可以尝试的办法是在Similarity中 idf返回1 然后多个机器上的索引并行搜索 在汇总结果的机器上 再 融入idf的计算 如下面的例子可以看出 当idf返回1的时候 打分可以比较了 第 186 / 199 页

187 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 class MultiIndexSimilarity extends Similarity public float idf(int docfreq, int numdocs) { return 1.0f; docid : 0 score : docid : 1 score : docid : 2 score : docid : 3 score : docid : 4 score : docid : 5 score : docid : 6 score : docid : 7 score : docid : 0 score : docid : 1 score : docid : 2 score : docid : 3 score : docid : 4 score : docid : 0 score : docid : 1 score : docid : 2 score : docid : 3 score : docid : 4 score : docid : 5 score : docid : 6 score : docid : 7 score : docid : 8 score : docid : 9 score : docid : 10 score : docid : 11 score : docid : 12 score : 第 187 / 199 页

188 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 (6) float coord(int overlap, int maxoverlap) 一次搜索可能包含多个搜索词 而一篇文档中也可能包含多个搜索词 此项表示 当一篇文档中包含的搜索词 越多 则此文档则打分越高 public void TestCoord() throws Exception { MySimilarity sim = new MySimilarity(); File indexdir = new File("TestCoord"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE true, IndexWriter.MaxFieldLength.LIMITED); Document doc1 = new Document(); Field f1 = new Field("contents", "common hello world", Field.Store.NO, Field.Index.ANALYZED); doc1.add(f1); writer.adddocument(doc1); Document doc2 = new Document(); Field f2 = new Field("contents", "common common common", Field.Store.NO, Field.Index.ANALYZED); doc2.add(f2); writer.adddocument(doc2); for(int i = 0; i < 10; i++){ Document doc3 = new Document(); Field f3 = new Field("contents", "world", Field.Store.NO, Field.Index.ANALYZED); doc3.add(f3); writer.adddocument(doc3); writer.close(); IndexReader reader = IndexReader.open(FSDirectory.open(indexDir)); IndexSearcher searcher = new IndexSearcher(reader); searcher.setsimilarity(sim); QueryParser parser = new QueryParser(Version.LUCENE_CURRENT, "contents", new StandardAnalyzer(Version.LUCENE_CURRENT)); Query query = parser.parse("common world"); TopDocs docs = searcher.search(query, 2); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 第 188 / 199 页

189 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 class MySimilarity extends Similarity public float coord(int overlap, int maxoverlap) { return 1; 如上面的实例 当coord返回1 不起作用的时候 文档一虽然包含了两个搜索词common和world 但由于 world的所在的文档数太多 而文档二包含common的次数比较多 因而文档二分数较高 docid : 1 score : docid : 0 score : 而当coord起作用的时候 文档一由于包含了两个搜索词而分数较高 class MySimilarity extends Similarity public float coord(int overlap, int maxoverlap) { return overlap / (float)maxoverlap; docid : 0 score : docid : 1 score : (7) float scorepayload(int docid, String fieldname, int start, int end, byte [] payload, int offset, int length) 由于Lucene引入了payload 因而可以存储一些自己的信息 用户可以根据自己存储的信息 来影响Lucene的 打分 payload的定义 第 189 / 199 页

190 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 我们知道 索引是以倒排表形式存储的 对于每一个词 都保存了包含这个词的一个链表 当然为了加快查询 速度 此链表多用跳跃表进行存储 Payload信息就是存储在倒排表中的 同文档号一起存放 多用于存储与每篇文档相关的一些信息 当然这部分 信息也可以存储域里(stored Field) 两者从功能上基本是一样的 然而当要存储的信息很多的时候 存放在倒 排表里 利用跳跃表 有利于大大提高搜索速度 Payload的存储方式如下图 由payload的定义 我们可以看出 payload可以存储一些不但与文档相关 而且与查询词也相关的信息 比如 某篇文档的某个词有特殊性 则可以在这个词的这个文档的position信息后存储payload信息 使得当搜索这个 词的时候 这篇文档获得较高的分数 要利用payload来影响查询需要做到以下几点 下面举例用<b></b>标记的词在payload中存储1 否则存储 0 首先要实现自己的Analyzer从而在Token中放入payload信息 class BoldAnalyzer extends Analyzer { 第 190 / 199 页

191 2.4 public TokenStream tokenstream(string fieldname, Reader reader) { TokenStream result = new WhitespaceTokenizer(reader); result = new BoldFilter(result); return result; class BoldFilter extends TokenFilter { public static int IS_NOT_BOLD = 0; public static int IS_BOLD = 1; private TermAttribute termatt; private PayloadAttribute payloadatt; protected BoldFilter(TokenStream input) { super(input); termatt = addattribute(termattribute.class); payloadatt = public boolean incrementtoken() throws IOException { if (input.incrementtoken()) { final char[] buffer = termatt.termbuffer(); final int length = termatt.termlength(); String tokenstring = new String(buffer, 0, length); if (tokenstring.startswith("<b>") && tokenstring.endswith("</b>")) { tokenstring = tokenstring.replace("<b>", ""); tokenstring = tokenstring.replace("</b>", ""); termatt.settermbuffer(tokenstring); payloadatt.setpayload(new Payload(int2bytes(IS_BOLD))); else { payloadatt.setpayload(new Payload(int2bytes(IS_NOT_BOLD))); return true; 第 191 / 199 页

192 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 else return false; public static int bytes2int(byte[] b) { int mask = 0xff; int temp = 0; int res = 0; for (int i = 0; i < 4; i++) { res <<= 8; temp = b[i] & mask; res = temp; return res; public static byte[] int2bytes(int num) { byte[] b = new byte[4]; for (int i = 0; i < 4; i++) { b[i] = (byte) (num >>> (24 - i * 8)); return b; 然后 实现自己的Similarity 从payload中读出信息 根据信息来打分 class PayloadSimilarity extends DefaultSimilarity public float scorepayload(int docid, String fieldname, int start, int end, byte[] payload, int offset, int length int isbold = BoldFilter.bytes2int(payload); if(isbold == BoldFilter.IS_BOLD){ System.out.println("It is a bold char."); else { System.out.println("It is not a bold char."); 第 192 / 199 页

193 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 return 1; 最后 查询的时候 一定要用PayloadXXXQuery(在此用PayloadTermQuery 在Lucene 2.4.1中 用 BoostingTermQuery) 否则scorePayload不起作用 public void testpayloadscore() throws Exception { PayloadSimilarity sim = new PayloadSimilarity(); File indexdir = new File("TestPayloadScore"); IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new BoldAnalyzer(), true, IndexWriter.MaxFieldLength.LIMITED); Document doc1 = new Document(); Field f1 = new Field("contents", "common hello world", Field.Store.NO, Field.Index.ANALYZED); doc1.add(f1); writer.adddocument(doc1); Document doc2 = new Document(); Field f2 = new Field("contents", "common <b>hello</b> world", Field.Store.NO, Field.Index.ANALYZED); doc2.add(f2); writer.adddocument(doc2); writer.close(); IndexReader reader = IndexReader.open(FSDirectory.open(indexDir)); IndexSearcher searcher = new IndexSearcher(reader); searcher.setsimilarity(sim); PayloadTermQuery query = new PayloadTermQuery(new Term("contents", "hello"), new MaxPayloadFunction()); TopDocs docs = searcher.search(query, 10); for (ScoreDoc doc : docs.scoredocs) { System.out.println("docid : " + doc.doc + " score : " + doc.score); 如果scorePayload函数始终是返回1 则结果如下 <b></b>不起作用 第 193 / 199 页

194 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 It is not a bold char. It is a bold char. docid : 0 score : docid : 1 score : 如果scorePayload函数如下 class PayloadSimilarity extends DefaultSimilarity public float scorepayload(int docid, String fieldname, int start, int end, byte[] payload, int offset, int length int isbold = BoldFilter.bytes2int(payload); if(isbold == BoldFilter.IS_BOLD){ System.out.println("It is a bold char."); return 10; else { System.out.println("It is not a bold char."); return 1; 则结果如下 同样是包含hello 包含加粗的文档获得较高分 It is not a bold char. It is a bold char. docid : 1 score : docid : 0 score : 继承并实现自己的collector 以上各种方法 已经把Lucene score计算公式的所有变量都涉及了 如果这还不能满足您的要求 还可以继承 实现自己的collector 在Lucene 2.4中 HitCollector有个函数public abstract void collect(int doc, float score) 用来收集搜索的 结果 第 194 / 199 页

195 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 其中TopDocCollector的实现如下 public void collect(int doc, float score) { if (score > 0.0f) { totalhits++; if (reusablesd == null) { reusablesd = new ScoreDoc(doc, score); else if (score >= reusablesd.score) { reusablesd.doc = doc; reusablesd.score = score; else { return; reusablesd = (ScoreDoc) hq.insertwithoverflow(reusablesd); 此函数将docid和score插入一个PriorityQueue中 使得得分最高的文档先返回 我们可以继承HitCollector 并在此函数中对score进行修改 然后再插入PriorityQueue 或者插入自己的数据 结构 比如我们在另外的地方存储docid和文档创建时间的对应 我们希望当文档时间是一天之内的分数最高 一周之 内的分数其次 一个月之外的分数很低 我们可以这样修改 public static long milisecondsoneday = 24L * 3600L * 1000L; public static long millisecondsoneweek = 7L * 24L * 3600L * 1000L; public static long millisecondsonemonth = 30L * 24L * 3600L * 1000L; public void collect(int doc, float score) { if (score > 0.0f) { long time = gettimebydocid(doc); 第 195 / 199 页

196 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式 if(time < milisecondsoneday) { score = score * 1.0; else if (time < millisecondsoneweek){ score = score * 0.8; else if (time < millisecondsonemonth) { score = score * 0.3; else { score = score * 0.1; totalhits++; if (reusablesd == null) { reusablesd = new ScoreDoc(doc, score); else if (score >= reusablesd.score) { reusablesd.doc = doc; reusablesd.score = score; else { return; reusablesd = (ScoreDoc) hq.insertwithoverflow(reusablesd); 在Lucene 3.0中 Collector接口为void collect(int doc) TopScoreDocCollector实现如下 public void collect(int doc) throws IOException { float score = scorer.score(); totalhits++; if (score <= pqtop.score) { return; 第 196 / 199 页

197 pqtop.doc = doc + docbase; pqtop.score = score; pqtop = pq.updatetop(); 同样可以用上面的方式影响其打分 第 197 / 199 页 2.4 有关Lucene的问题(4):影响Lucene对文档打分的四种方式

198 - 做最棒的软件开发交流社区 本系列文章将详细描述几乎最新版本的Lucene的基本原理 和代码分析 其中总体架构和索引文件格式是Lucene 2.9的 索引过程分 析是Lucene 3.0的 鉴于索引文件格式没有太大变化 因而原文没有更新 原理 和架构的文章中引用了前辈的一些图 可能属于早期的 Lucene 但不影响对原理和架构的理解 本系列文章尚在撰写之中 将会有分词器 段合并 QueryParser 查询语句与查询对象 搜索过程 打分公式 的推导等章节 提前给大家分享 希望大家批评指正 Lucene 3.0 原理与代码分析 作者: forfuture1978 第 198 / 199 页

199 - 做最棒的软件开发交流社区 本书由JavaEye提供电子书DIY功能制作并发行 更多精彩博客电子书 请访问 第 199 / 199 页

4.2 索引创建类 IndexWriter org.apache.lucene.index.indexwriter org.apache.lucene.index.documentswriter 索引创建过程

4.2 索引创建类 IndexWriter org.apache.lucene.index.indexwriter org.apache.lucene.index.documentswriter 索引创建过程 Annotated Lucene( 源码剖析中文版 ) Annotated Lucene 作者 :naven 1 目录 Annotated Lucene( 源码剖析中文版 )...- 1-1 目录...- 1-2 Lucene 是什么...- 3-2.1.1 强大特性...- 3-2.1.2 API 组成...- 4-2.1.3 Hello World!...- 5-2.1.4 Lucene roadmap...-

More information

C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 5 月 3 日 1

C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 5 月 3 日 1 C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 月 3 日 1 1 INPUTOUTPUT 1 InputOutput 题目描述 用 cin 输入你的姓名 ( 没有空格 ) 和年龄 ( 整数 ), 并用 cout 输出 输入输出符合以下范例 输入 master 999 输出 I am master, 999 years old. 注意 "," 后面有一个空格,"." 结束,

More information

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例 帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例 这篇文章主要介绍了帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例, 本文还详细介绍了帝国 CMS 数据库类中的一些常用方法, 需要的朋友可以参考下 例 1: 连接 MYSQL 数据库例子 (a.php)

More information

通过Hive将数据写入到ElasticSearch

通过Hive将数据写入到ElasticSearch 我在 使用 Hive 读取 ElasticSearch 中的数据 文章中介绍了如何使用 Hive 读取 ElasticSearch 中的数据, 本文将接着上文继续介绍如何使用 Hive 将数据写入到 ElasticSearch 中 在使用前同样需要加入 elasticsearch-hadoop-2.3.4.jar 依赖, 具体请参见前文介绍 我们先在 Hive 里面建个名为 iteblog 的表,

More information

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 odps-sdk 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基 开放数据处理服务 ODPS SDK SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基础功能的主体接口, 搜索关键词 "odpssdk-core" 一些

More information

OOP with Java 通知 Project 4: 4 月 18 日晚 9 点 关于抄袭 没有分数

OOP with Java 通知 Project 4: 4 月 18 日晚 9 点 关于抄袭 没有分数 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 4: 4 月 18 日晚 9 点 关于抄袭 没有分数 复习 类的复用 组合 (composition): has-a 关系 class MyType { public int i; public double d; public char c; public void set(double

More information

1.JasperReport ireport JasperReport ireport JDK JDK JDK JDK ant ant...6

1.JasperReport ireport JasperReport ireport JDK JDK JDK JDK ant ant...6 www.brainysoft.net 1.JasperReport ireport...4 1.1 JasperReport...4 1.2 ireport...4 2....4 2.1 JDK...4 2.1.1 JDK...4 2.1.2 JDK...5 2.1.3 JDK...5 2.2 ant...6 2.2.1 ant...6 2.2.2 ant...6 2.3 JasperReport...7

More information

Guava学习之Resources

Guava学习之Resources Resources 提供提供操作 classpath 路径下所有资源的方法 除非另有说明, 否则类中所有方法的参数都不能为 null 虽然有些方法的参数是 URL 类型的, 但是这些方法实现通常不是以 HTTP 完成的 ; 同时这些资源也非 classpath 路径下的 下面两个函数都是根据资源的名称得到其绝对路径, 从函数里面可以看出,Resources 类中的 getresource 函数都是基于

More information

主程式 : public class Main3Activity extends AppCompatActivity { ListView listview; // 先整理資料來源,listitem.xml 需要傳入三種資料 : 圖片 狗狗名字 狗狗生日 // 狗狗圖片 int[] pic =new

主程式 : public class Main3Activity extends AppCompatActivity { ListView listview; // 先整理資料來源,listitem.xml 需要傳入三種資料 : 圖片 狗狗名字 狗狗生日 // 狗狗圖片 int[] pic =new ListView 自訂排版 主程式 : public class Main3Activity extends AppCompatActivity { ListView listview; // 先整理資料來源,listitem.xml 需要傳入三種資料 : 圖片 狗狗名字 狗狗生日 // 狗狗圖片 int[] pic =new int[]{r.drawable.dog1, R.drawable.dog2,

More information

C/C++ - 文件IO

C/C++ - 文件IO C/C++ IO Table of contents 1. 2. 3. 4. 1 C ASCII ASCII ASCII 2 10000 00100111 00010000 31H, 30H, 30H, 30H, 30H 1, 0, 0, 0, 0 ASCII 3 4 5 UNIX ANSI C 5 FILE FILE 6 stdio.h typedef struct { int level ;

More information

第四章 102 图 4唱16 基于图像渲染的理论基础 三张拍摄图像以及它们投影到球面上生成的球面图像 拼图的圆心是相同的 而拼图是由球面图像上的弧线图像组成的 因此我 们称之为同心球拼图 如图 4唱18 所示 这些拼图中半径最大的是圆 Ck 最小的是圆 C0 设圆 Ck 的半径为 r 虚拟相机水平视域为 θ 有 r R sin θ 2 4畅11 由此可见 构造同心球拼图的过程实际上就是对投影图像中的弧线图像

More information

使用MapReduce读取XML文件

使用MapReduce读取XML文件 使用 MapReduce 读取 XML 文件 XML( 可扩展标记语言, 英语 :extensible Markup Language, 简称 : XML) 是一种标记语言, 也是行业标准数据交换交换格式, 它很适合在系统之间进行数据存储和交换 ( 话说 Hadoop H ive 等的配置文件就是 XML 格式的 ) 本文将介绍如何使用 MapReduce 来读取 XML 文件 但是 Had oop

More information

untitled

untitled 1 Outline 數 料 數 數 列 亂數 練 數 數 數 來 數 數 來 數 料 利 料 來 數 A-Z a-z _ () 不 數 0-9 數 不 數 SCHOOL School school 數 讀 school_name schoolname 易 不 C# my name 7_eleven B&Q new C# (1) public protected private params override

More information

IDEO_HCD_0716

IDEO_HCD_0716 IDEO HCD Toolkit Tencent CDC ...? Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC Tencent CDC

More information

59 1 CSpace 2 CSpace CSpace URL CSpace 1 CSpace URL 2 Lucene 3 ID 4 ID Web 1. 2 CSpace LireSolr 3 LireSolr 3 Web LireSolr ID

59 1 CSpace 2 CSpace CSpace URL CSpace 1 CSpace URL 2 Lucene 3 ID 4 ID Web 1. 2 CSpace LireSolr 3 LireSolr 3 Web LireSolr ID 58 2016. 14 * LireSolr LireSolr CEDD Ajax CSpace LireSolr CEDD Abstract In order to offer better image support services it is necessary to extend the image retrieval function of our institutional repository.

More information

1 1 大概思路 创建 WebAPI 创建 CrossMainController 并编写 Nuget 安装 microsoft.aspnet.webapi.cors 跨域设置路由 编写 Jquery EasyUI 界面 运行效果 2 创建 WebAPI 创建 WebAPI, 新建 -> 项目 ->

1 1 大概思路 创建 WebAPI 创建 CrossMainController 并编写 Nuget 安装 microsoft.aspnet.webapi.cors 跨域设置路由 编写 Jquery EasyUI 界面 运行效果 2 创建 WebAPI 创建 WebAPI, 新建 -> 项目 -> 目录 1 大概思路... 1 2 创建 WebAPI... 1 3 创建 CrossMainController 并编写... 1 4 Nuget 安装 microsoft.aspnet.webapi.cors... 4 5 跨域设置路由... 4 6 编写 Jquery EasyUI 界面... 5 7 运行效果... 7 8 总结... 7 1 1 大概思路 创建 WebAPI 创建 CrossMainController

More information

长 安 大 学 硕 士 学 位 论 文 基 于 数 据 仓 库 和 数 据 挖 掘 的 行 为 分 析 研 究 姓 名 : 杨 雅 薇 申 请 学 位 级 别 : 硕 士 专 业 : 计 算 机 软 件 与 理 论 指 导 教 师 : 张 卫 钢 20100530 长安大学硕士学位论文 3 1 3系统架构设计 行为分析数据仓库的应用模型由四部分组成 如图3 3所示

More information

Fun Time (1) What happens in memory? 1 i n t i ; 2 s h o r t j ; 3 double k ; 4 char c = a ; 5 i = 3; j = 2; 6 k = i j ; H.-T. Lin (NTU CSIE) Referenc

Fun Time (1) What happens in memory? 1 i n t i ; 2 s h o r t j ; 3 double k ; 4 char c = a ; 5 i = 3; j = 2; 6 k = i j ; H.-T. Lin (NTU CSIE) Referenc References (Section 5.2) Hsuan-Tien Lin Deptartment of CSIE, NTU OOP Class, March 15-16, 2010 H.-T. Lin (NTU CSIE) References OOP 03/15-16/2010 0 / 22 Fun Time (1) What happens in memory? 1 i n t i ; 2

More information

索 引 呢? 而 这 就 是 召 回 率 和 查 准 率 之 间 的 权 衡 了 2.2 Determining the vocabulary of terms 2.2.1 Tokenization 给 定 字 符 序 列,tokenization (koala++: 这 里 我 不 想 翻 译,

索 引 呢? 而 这 就 是 召 回 率 和 查 准 率 之 间 的 权 衡 了 2.2 Determining the vocabulary of terms 2.2.1 Tokenization 给 定 字 符 序 列,tokenization (koala++: 这 里 我 不 想 翻 译, The term vocabulary and postings lists Koala++ 整 理 倒 排 索 引 有 四 步 : 1. 收 集 要 索 引 的 文 档 2. 对 文 本 进 行 tokenize 3. 对 token 进 行 语 言 上 的 一 些 预 处 理 4. 为 每 个 term 索 引 文 档 2.1 Document delineation and character

More information

OOP with Java 通知 Project 4: 4 月 19 日晚 9 点

OOP with Java 通知 Project 4: 4 月 19 日晚 9 点 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 4: 4 月 19 日晚 9 点 复习 类的复用 组合 (composition): has-a 关系 class MyType { public int i; public double d; public char c; public void set(double x) { d

More information

水晶分析师

水晶分析师 大数据时代的挑战 产品定位 体系架构 功能特点 大数据处理平台 行业大数据应用 IT 基础设施 数据源 Hadoop Yarn 终端 统一管理和监控中心(Deploy,Configure,monitor,Manage) Master Servers TRS CRYSTAL MPP Flat Files Applications&DBs ETL&DI Products 技术指标 1 TRS

More information

3.1 num = 3 ch = 'C' 2

3.1 num = 3 ch = 'C' 2 Java 1 3.1 num = 3 ch = 'C' 2 final 3.1 final : final final double PI=3.1415926; 3 3.2 4 int 3.2 (long int) (int) (short int) (byte) short sum; // sum 5 3.2 Java int long num=32967359818l; C:\java\app3_2.java:6:

More information

res/layout 目录下的 main.xml 源码 : <?xml version="1.0" encoding="utf 8"?> <TabHost android:layout_height="fill_parent" xml

res/layout 目录下的 main.xml 源码 : <?xml version=1.0 encoding=utf 8?> <TabHost android:layout_height=fill_parent xml 拓展训练 1- 界面布局 1. 界面布局的重要性做应用程序, 界面是最基本的 Andorid 的界面, 需要写在 res/layout 的 xml 里面, 一般情况下一个 xml 对应一个界面 Android 界面布局有点像写 html( 连注释代码的方式都一样 ), 要先给 Android 定框架, 然后再在框架里面放控件,Android 提供了几种框架,AbsoluteLayout,LinearLayout,

More information

Microsoft Word - 01.DOC

Microsoft Word - 01.DOC 第 1 章 JavaScript 简 介 JavaScript 是 NetScape 公 司 为 Navigator 浏 览 器 开 发 的, 是 写 在 HTML 文 件 中 的 一 种 脚 本 语 言, 能 实 现 网 页 内 容 的 交 互 显 示 当 用 户 在 客 户 端 显 示 该 网 页 时, 浏 览 器 就 会 执 行 JavaScript 程 序, 用 户 通 过 交 互 式 的

More information

( Version 0.4 ) 1

( Version 0.4 ) 1 ( Version 0.4 ) 1 3 3.... 3 3 5.... 9 10 12 Entities-Relationship Model. 13 14 15.. 17 2 ( ) version 0.3 Int TextVarchar byte byte byte 3 Id Int 20 Name Surname Varchar 20 Forename Varchar 20 Alternate

More information

Chapter #

Chapter # 第三章 TCP/IP 协议栈 本章目标 通过本章的学习, 您应该掌握以下内容 : 掌握 TCP/IP 分层模型 掌握 IP 协议原理 理解 OSI 和 TCP/IP 模型的区别和联系 TCP/IP 介绍 主机 主机 Internet TCP/IP 早期的协议族 全球范围 TCP/IP 协议栈 7 6 5 4 3 应用层表示层会话层传输层网络层 应用层 主机到主机层 Internet 层 2 1 数据链路层

More information

Kubenetes 系列列公开课 2 每周四晚 8 点档 1. Kubernetes 初探 2. 上 手 Kubernetes 3. Kubernetes 的资源调度 4. Kubernetes 的运 行行时 5. Kubernetes 的 网络管理理 6. Kubernetes 的存储管理理 7.

Kubenetes 系列列公开课 2 每周四晚 8 点档 1. Kubernetes 初探 2. 上 手 Kubernetes 3. Kubernetes 的资源调度 4. Kubernetes 的运 行行时 5. Kubernetes 的 网络管理理 6. Kubernetes 的存储管理理 7. Kubernetes 包管理理 工具 Helm 蔺礼强 Kubenetes 系列列公开课 2 每周四晚 8 点档 1. Kubernetes 初探 2. 上 手 Kubernetes 3. Kubernetes 的资源调度 4. Kubernetes 的运 行行时 5. Kubernetes 的 网络管理理 6. Kubernetes 的存储管理理 7. Kubernetes

More information

JavaIO.PDF

JavaIO.PDF O u t p u t S t ream j a v a. i o. O u t p u t S t r e a m w r i t e () f l u s h () c l o s e () public abstract void write(int b) throws IOException public void write(byte[] data) throws IOException

More information

前言 C# C# C# C C# C# C# C# C# microservices C# More Effective C# More Effective C# C# C# C# Effective C# 50 C# C# 7 Effective vii

前言 C# C# C# C C# C# C# C# C# microservices C# More Effective C# More Effective C# C# C# C# Effective C# 50 C# C# 7 Effective vii 前言 C# C# C# C C# C# C# C# C# microservices C# More Effective C# More Effective C# C# C# C# Effective C# 50 C# C# 7 Effective vii C# 7 More Effective C# C# C# C# C# C# Common Language Runtime CLR just-in-time

More information

使用Cassandra和Spark 2.0实现Rest API服务

使用Cassandra和Spark 2.0实现Rest API服务 使用 Cassandra 和 Spark 2.0 实现 Rest API 服务 在这篇文章中, 我将介绍如何在 Spark 中使用 Akkahttp 并结合 Cassandra 实现 REST 服务, 在这个系统中 Cassandra 用于数据的存储 我们已经见识到 Spark 的威力, 如果和 Cassandra 正确地结合可以实现更强大的系统 我们先创建一个 build.sbt 文件, 内容如下

More information

概述

概述 OPC Version 1.6 build 0910 KOSRDK Knight OPC Server Rapid Development Toolkits Knight Workgroup, eehoo Technology 2002-9 OPC 1...4 2 API...5 2.1...5 2.2...5 2.2.1 KOS_Init...5 2.2.2 KOS_InitB...5 2.2.3

More information

「人名權威檔」資料庫欄位建置表

「人名權威檔」資料庫欄位建置表 ( version 0.2) 1 3 3 3 3 5 6 9.... 11 Entities - Relationship Model..... 12 13 14 16 2 ( ) Int Varchar Text byte byte byte Id Int 20 Name Surname Varchar 20 Forename Varchar 20 Alternate Type Varchar 10

More information

第八章 全球最大门户网站 雅虎 201 图 8 2 雅虎历年美国和美国以外地区收入比例情况 4畅 雅虎的品牌塑造 1996 年冬天 人们估计互联网上总共已经有了 9000 万个网页 这几乎与美国国会图书馆藏书的总页数相等 据研究 每天还有 17 万个新网页出现在因特网上 世界各地的电脑拥有者把各种各样的信 息制作成文字发送到网上 这些信息五花八门 包括公司 个人甚至 还有宠物的秘密生活等 搜索引擎的功能就是帮人们在茫茫的网中寻

More information

untitled

untitled 1 Outline 料 類 說 Tang, Shih-Hsuan 2006/07/26 ~ 2006/09/02 六 PM 7:00 ~ 9:30 聯 ives.net@gmail.com www.csie.ntu.edu.tw/~r93057/aspnet134 度 C# 力 度 C# Web SQL 料 DataGrid DataList 參 ASP.NET 1.0 C# 例 ASP.NET 立

More information

Chapter 9: Objects and Classes

Chapter 9: Objects and Classes Fortran Algol Pascal Modula-2 BCPL C Simula SmallTalk C++ Ada Java C# C Fortran 5.1 message A B 5.2 1 class Vehicle subclass Car object mycar public class Vehicle extends Object{ public int WheelNum

More information

1 4 1.1 4 1.2..4 2..4 2.1..4 3.4 3.1 Java.5 3.1.1..5 3.1.2 5 3.1.3 6 4.6 4.1 6 4.2.6 5 7 5.1..8 5.1.1 8 5.1.2..8 5.1.3..8 5.1.4..9 5.2..9 6.10 6.1.10

1 4 1.1 4 1.2..4 2..4 2.1..4 3.4 3.1 Java.5 3.1.1..5 3.1.2 5 3.1.3 6 4.6 4.1 6 4.2.6 5 7 5.1..8 5.1.1 8 5.1.2..8 5.1.3..8 5.1.4..9 5.2..9 6.10 6.1.10 Java V1.0.1 2007 4 10 1 4 1.1 4 1.2..4 2..4 2.1..4 3.4 3.1 Java.5 3.1.1..5 3.1.2 5 3.1.3 6 4.6 4.1 6 4.2.6 5 7 5.1..8 5.1.1 8 5.1.2..8 5.1.3..8 5.1.4..9 5.2..9 6.10 6.1.10 6.2.10 6.3..10 6.4 11 7.12 7.1

More information

OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢 学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料

OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢   学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢 Email: 51141201063@ecnu.cn 学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料 OOP with Java Java 类型 引用 不可变类型 对象存储位置 作用域 OOP

More information

Converting image (bmp/jpg) file into binary format

Converting image (bmp/jpg) file into binary format RAiO Image Tool 操作说明 Version 1.0 July 26, 2016 RAiO Technology Inc. Copyright RAiO Technology Inc. 2013 RAiO TECHNOLOGY INC. www.raio.com.tw Revise History Version Date Description 0.1 September 01, 2014

More information

ebook65-5

ebook65-5 5 P e r l P e r l I / O P e r l P e r l P e r l P e r l P e r l I / O P e r l P e r l 5.1 P e r l P e r ( ) S T D I N P e r l S T D I N 2 $ @ P e r l f o r e a c h e l s e i f P e r l p e r l f u n c o

More information

论文,,, ( &, ), 1 ( -, : - ), ; (, ), ; ;, ( &, ),,,,,, (, ),,,, (, ) (, ),,, :. : ( ), ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ), ( ),,,, 1 原译作 修补者, 但在英译版本中, 被译作

论文,,, ( &, ), 1 ( -, : - ), ; (, ), ; ;, ( &, ),,,,,, (, ),,,, (, ) (, ),,, :. : ( ), ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ), ( ),,,, 1 原译作 修补者, 但在英译版本中, 被译作 * 夏传玲 : 本文简要回顾了国内外定性研究在最近 多年的发展概况, 总结 了定性研究的六个发展趋势和分析策略上的三种流派 在上述两种背景下, 本文探讨了计算机辅助的定性分析给定性研究带来的机遇和挑战, 特别是它和手工操作对比时的优势和劣势, 以及应用这种定性分析技术所可能面临的困难 : 定性研究定性分析 文化差异,, (, ),,,, ( - ) ( - ) ( - ) ( - ) ( - ) (

More information

目 录(目录名)

目  录(目录名) 目录 1 域名解析配置命令... 1-1 1.1 域名解析配置命令...1-1 1.1.1 display dns domain... 1-1 1.1.2 display dns dynamic-host... 1-2 1.1.3 display dns proxy table... 1-2 1.1.4 display dns server... 1-3 1.1.5 display ip host...

More information

目 录(目录名)

目  录(目录名) 目录 目录...1-1 1.1 域名解析配置命令... 1-1 1.1.1 display dns domain... 1-1 1.1.2 display dns dynamic-host... 1-1 1.1.3 display dns server... 1-2 1.1.4 display ip host... 1-3 1.1.5 dns domain... 1-4 1.1.6 dns resolve...

More information

无类继承.key

无类继承.key 无类继承 JavaScript 面向对象的根基 周爱 民 / aimingoo aiming@gmail.com https://aimingoo.github.io https://github.com/aimingoo rand = new Person("Rand McKinnon",... https://docs.oracle.com/cd/e19957-01/816-6408-10/object.htm#1193255

More information

雲端 Cloud Computing 技術指南 運算 應用 平台與架構 10/04/15 11:55:46 INFO 10/04/15 11:55:53 INFO 10/04/15 11:55:56 INFO 10/04/15 11:56:05 INFO 10/04/15 11:56:07 INFO

雲端 Cloud Computing 技術指南 運算 應用 平台與架構 10/04/15 11:55:46 INFO 10/04/15 11:55:53 INFO 10/04/15 11:55:56 INFO 10/04/15 11:56:05 INFO 10/04/15 11:56:07 INFO CHAPTER 使用 Hadoop 打造自己的雲 8 8.3 測試 Hadoop 雲端系統 4 Nodes Hadoop Map Reduce Hadoop WordCount 4 Nodes Hadoop Map/Reduce $HADOOP_HOME /home/ hadoop/hadoop-0.20.2 wordcount echo $ mkdir wordcount $ cd wordcount

More information

9, : Java 19., [4 ]. 3 Apla2Java Apla PAR,Apla2Java Apla Java.,Apla,,, 1. 1 Apla Apla A[J ] Get elem (set A) A J A B Intersection(set A,set B) A B A B

9, : Java 19., [4 ]. 3 Apla2Java Apla PAR,Apla2Java Apla Java.,Apla,,, 1. 1 Apla Apla A[J ] Get elem (set A) A J A B Intersection(set A,set B) A B A B 25 9 2008 9 M ICROEL ECTRON ICS & COMPU TER Vol. 25 No. 9 September 2008 J ava 1,2, 1,2, 1,2 (1, 330022 ; 2, 330022) :,. Apla - Java,,.. : PAR ;Apla - Java ; ;CMP ; : TP311 : A : 1000-7180 (2008) 09-0018

More information

Office Office Office Microsoft Word Office Office Azure Office One Drive 2 app 3 : [5] 3, :, [6]; [5], ; [8], [1], ICTCLAS(Institute of Computing Tech

Office Office Office Microsoft Word Office Office Azure Office One Drive 2 app 3 : [5] 3, :, [6]; [5], ; [8], [1], ICTCLAS(Institute of Computing Tech - OfficeCoder 1 2 3 4 1,2,3,4 xingjiarong@mail.sdu.edu.cn 1 xuchongyang@mail.sdu.edu.cn 2 sun.mc@outlook.com 3 luoyuanhang@mail.sdu.edu.cn 4 Abstract. Microsoft Word 2013 Word 2013 Office Keywords:,, HTML5,

More information

RxJava

RxJava RxJava By 侦跃 & @hi 头 hi RxJava 扩展的观察者模式 处 观察者模式 Observable 发出事件 Subscriber 订阅事件 bus.post(new AnswerEvent(42)); @Subscribe public void onanswer(answerevent event) {! }! Observable observable = Observable.create(new

More information

C/C++ 语言 - 循环

C/C++ 语言 - 循环 C/C++ Table of contents 7. 1. 2. while 3. 4. 5. for 6. 8. (do while) 9. 10. (nested loop) 11. 12. 13. 1 // summing.c: # include int main ( void ) { long num ; long sum = 0L; int status ; printf

More information

C/C++程序设计 - 字符串与格式化输入/输出

C/C++程序设计 - 字符串与格式化输入/输出 C/C++ / Table of contents 1. 2. 3. 4. 1 i # include # include // density of human body : 1. 04 e3 kg / m ^3 # define DENSITY 1. 04 e3 int main ( void ) { float weight, volume ; int

More information

赵燕菁 #!!!

赵燕菁 #!!! 赵燕菁 城市规划在灾后重建中对于工程技术的关注 很容易掩盖城市灾后重建中看不见的制度因素!!! 产权 城市最基本的制度 原型 # 就是公共产品交易的存在 城市 发达 # 与否 取决于公共产品提供的范围和水平 现代城市和传统城市的最大差别 就是可以以信用的方式 抵押未来的收益 获得公共产品建设所需要的原始资本 市场经济与计划经济最大的差别 就在于高度复杂的产权制度 因此 未来灾区规划中 产权的恢复和重建

More information

epub83-1

epub83-1 C++Builder 1 C + + B u i l d e r C + + B u i l d e r C + + B u i l d e r C + + B u i l d e r 1.1 1.1.1 1-1 1. 1-1 1 2. 1-1 2 A c c e s s P a r a d o x Visual FoxPro 3. / C / S 2 C + + B u i l d e r / C

More information

Microsoft Word - (web)_F.1_Notes_&_Application_Form(Chi)(non-SPCCPS)_16-17.doc

Microsoft Word - (web)_F.1_Notes_&_Application_Form(Chi)(non-SPCCPS)_16-17.doc 聖 保 羅 男 女 中 學 學 年 中 一 入 學 申 請 申 請 須 知 申 請 程 序 : 請 將 下 列 文 件 交 回 本 校 ( 麥 當 勞 道 33 號 ( 請 以 A4 紙 張 雙 面 影 印, 並 用 魚 尾 夾 夾 起 : 填 妥 申 請 表 並 貼 上 近 照 小 學 五 年 級 上 下 學 期 成 績 表 影 印 本 課 外 活 動 表 現 及 服 務 的 證 明 文 件 及

More information

}; "P2VTKNvTAnYNwBrqXbgxRSFQs6FTEhNJ", " " string imagedata; if(0!= read_image("a.jpg",imagedata)) { return -1; } string rsp; ytopen_sdk m_sd

}; P2VTKNvTAnYNwBrqXbgxRSFQs6FTEhNJ,   string imagedata; if(0!= read_image(a.jpg,imagedata)) { return -1; } string rsp; ytopen_sdk m_sd tencentyun-youtu c++ sdk for 腾讯云智能优图服务 & 腾讯优图开放平台 安装 运行环境 Linux 依赖项 - curl-7.40.0, 获取更新版本 https://github.com/bagder/curl - openssl-1.0.1k, 获取更新版本 https://github.com/openssl/openssl 构建工程 工程采用 CMake 构建 1.

More information

untitled

untitled 1 行 行 行 行.NET 行 行 類 來 行 行 Thread 類 行 System.Threading 來 類 Thread 類 (1) public Thread(ThreadStart start ); Name 行 IsAlive 行 行狀 Start 行 行 Suspend 行 Resume 行 行 Thread 類 (2) Sleep 行 CurrentThread 行 ThreadStart

More information

PowerPoint Presentation

PowerPoint Presentation Skill-building Courses Intro to SQL Lesson 2 More Functions in SQL 通配符 :LIKE SELECT * FROM Products WHERE PName LIKE %gizmo% PName Price Category Manufacturer Gizmo $19.99 Gadgets GizmoWorks Powergizmo

More information

HP and Canon 单色通用芯片表 SCC 芯片 图片 HP 700 M712, 700 M725 CF214X (14X) 17.5 HP 5200 Q7516A U16-2CHIP SSS 846 芯片记号 (U16-2) Canon LBP-3500, LBP-3900, LBP-392

HP and Canon 单色通用芯片表 SCC 芯片 图片 HP 700 M712, 700 M725 CF214X (14X) 17.5 HP 5200 Q7516A U16-2CHIP SSS 846 芯片记号 (U16-2) Canon LBP-3500, LBP-3900, LBP-392 HP and Canon 单色通用芯片表在线访问我们的网站, 可以得到更多的信息 : www.scc-inc.com/chipcenter 全部开始都是专利通用芯片一个芯片, 多个不同型号的硒鼓 注意 : 当在这个文档上要寻找一个特殊的 或打印机的型号时, 在你的键盘上同时按 CTRL 键和 F 键就能搜索到 HP and Canon 单色通用芯片表 SCC 芯片 图片 HP 700 M712, 700

More information

EJB-Programming-3.PDF

EJB-Programming-3.PDF :, JBuilder EJB 2.x CMP EJB Relationships JBuilder EJB Test Client EJB EJB Seminar CMP Entity Beans Value Object Design Pattern J2EE Design Patterns Value Object Value Object Factory J2EE EJB Test Client

More information

一 登录 crm Mobile 系统 : 输入 ShijiCare 用户名和密码, 登录系统, 如图所示 : 第 2 页共 32 页

一 登录 crm Mobile 系统 : 输入 ShijiCare 用户名和密码, 登录系统, 如图所示 : 第 2 页共 32 页 第 1 页共 32 页 crm Mobile V1.0 for IOS 用户手册 一 登录 crm Mobile 系统 : 输入 ShijiCare 用户名和密码, 登录系统, 如图所示 : 第 2 页共 32 页 二 crm Mobile 界面介绍 : 第 3 页共 32 页 三 新建 (New) 功能使用说明 1 选择产品 第 4 页共 32 页 2 填写问题的简要描述和详细描述 第 5 页共

More information

Microsoft PowerPoint - 4. 数组和字符串Arrays and Strings.ppt [兼容模式]

Microsoft PowerPoint - 4. 数组和字符串Arrays and Strings.ppt [兼容模式] Arrays and Strings 存储同类型的多个元素 Store multi elements of the same type 数组 (array) 存储固定数目的同类型元素 如整型数组存储的是一组整数, 字符数组存储的是一组字符 数组的大小称为数组的尺度 (dimension). 定义格式 : type arrayname[dimension]; 如声明 4 个元素的整型数组 :intarr[4];

More information

第 15 章 程 式 編 写 語 言 15.1 程 式 編 写 語 言 的 角 色 程 式 編 寫 語 言 是 程 式 編 寫 員 與 電 腦 溝 通 的 界 面 語 法 是 一 組 規 則 讓 程 式 編 寫 員 將 字 詞 集 合 起 來 電 腦 是 處 理 位 元 和 字 節 的 機 器, 與

第 15 章 程 式 編 写 語 言 15.1 程 式 編 写 語 言 的 角 色 程 式 編 寫 語 言 是 程 式 編 寫 員 與 電 腦 溝 通 的 界 面 語 法 是 一 組 規 則 讓 程 式 編 寫 員 將 字 詞 集 合 起 來 電 腦 是 處 理 位 元 和 字 節 的 機 器, 與 程 式 編 写 語 言 在 完 成 這 章 後, 你 將 能 夠 了 解 程 式 編 写 語 言 的 功 能 了 解 高 階 語 言 和 低 階 語 言 之 間 的 分 別 知 道 翻 譯 程 式 的 意 義 和 能 夠 把 翻 譯 程 式 分 類 為 : 匯 編 程 式 編 譯 程 式 和 解 譯 程 式 認 識 不 同 翻 譯 程 式 的 優 點 和 缺 點 程 式 是 指 揮 電 腦 的 指

More information

绘制OpenCascade中的曲线

绘制OpenCascade中的曲线 在 OpenSceneGraph 中绘制 OpenCascade 的曲线 Draw OpenCascade Geometry Curves in OpenSceneGraph eryar@163.com 摘要 Abstract: 本文简要说明 OpenCascade 中几何曲线的数据, 并将这些几何曲线在 OpenSceneGraph 中绘制出来 关键字 KeyWords:OpenCascade Geometry

More information

KV-cache 1 KV-cache Fig.1 WorkflowofKV-cache 2.2 Key-value Key ; Key Mem-cache (FIFO) Value Value Key Mem-cache ( Value 256B 100 MB 20%

KV-cache 1 KV-cache Fig.1 WorkflowofKV-cache 2.2 Key-value Key ; Key Mem-cache (FIFO) Value Value Key Mem-cache ( Value 256B 100 MB 20% 38 11 2013 11 GeomaticsandInformationScienceofWuhanUniversity Vol.38No.11 Nov.2013 :1671-8860(2013)11-1339-05 :A GIS Key-value 1 1 1 1 (1 129 430079) : 设计了一种基于 Key-value 结构的缓存 KV-cache 旨在简化数据结构 高效管理缓存数据

More information

ABOUT ME AGENDA 唐建法 / TJ MongoDB 高级方案架构师 MongoDB 中文社区联合发起人 Spark 介绍 Spark 和 MongoDB 案例演示

ABOUT ME AGENDA 唐建法 / TJ MongoDB 高级方案架构师 MongoDB 中文社区联合发起人 Spark 介绍 Spark 和 MongoDB 案例演示 完整的大数据解決方案 ABOUT ME AGENDA 唐建法 / TJ MongoDB 高级方案架构师 MongoDB 中文社区联合发起人 Spark 介绍 Spark 和 MongoDB 案例演示 Dataframe Pig YARN Spark Stand Alone HDFS Spark Stand Alone Mesos Mesos Spark Streaming Hive Hadoop

More information

提问袁小兵:

提问袁小兵: C++ 面 试 试 题 汇 总 柯 贤 富 管 理 软 件 需 求 分 析 篇 1. STL 类 模 板 标 准 库 中 容 器 和 算 法 这 部 分 一 般 称 为 标 准 模 板 库 2. 为 什 么 定 义 虚 的 析 构 函 数? 避 免 内 存 问 题, 当 你 可 能 通 过 基 类 指 针 删 除 派 生 类 对 象 时 必 须 保 证 基 类 析 构 函 数 为 虚 函 数 3.

More information

本章学习目标 小风 Java 实战系列教程 SpringMVC 简介 SpringMVC 的入门案例 SpringMVC 流程分析 配置注解映射器和适配器 注解的使用 使用不同方式的跳转页面 1. SpringMVC 简介 Spring web mvc

本章学习目标 小风 Java 实战系列教程 SpringMVC 简介 SpringMVC 的入门案例 SpringMVC 流程分析 配置注解映射器和适配器 注解的使用 使用不同方式的跳转页面 1. SpringMVC 简介 Spring web mvc 本章学习目标 SpringMVC 简介 SpringMVC 的入门案例 SpringMVC 流程分析 配置注解映射器和适配器 配置视图解析器 @RequestMapping 注解的使用 使用不同方式的跳转页面 1. SpringMVC 简介 Spring web mvc 和 Struts2 都属于表现层的框架, 它是 Spring 框架的一部分, 我们可 以从 Spring 的整体结构中看得出来 :

More information

ChinaBI企业会员服务- BI企业

ChinaBI企业会员服务- BI企业 商业智能 (BI) 开源工具 Pentaho BisDemo 介绍及操作说明 联系人 : 杜号权苏州百咨信息技术有限公司电话 : 0512-62861389 手机 :18616571230 QQ:37971343 E-mail:du.haoquan@bizintelsolutions.com 权限控制管理 : 权限控制管理包括 : 浏览权限和数据权限 ( 权限部分两个角色 :ceo,usa; 两个用户

More information

(TestFailure) JUnit Framework AssertionFailedError JUnit Composite TestSuite Test TestSuite run() run() JUnit

(TestFailure) JUnit Framework AssertionFailedError JUnit Composite TestSuite Test TestSuite run() run() JUnit Tomcat Web JUnit Cactus JUnit Java Cactus JUnit 26.1 JUnit Java JUnit JUnit Java JSP Servlet JUnit Java Erich Gamma Kent Beck xunit JUnit boolean JUnit Java JUnit Java JUnit Java 26.1.1 JUnit JUnit How

More information

全国计算机技术与软件专业技术资格(水平)考试

全国计算机技术与软件专业技术资格(水平)考试 全 国 计 算 机 技 术 与 软 件 专 业 技 术 资 格 ( 水 平 ) 考 试 2009 年 下 半 年 程 序 员 下 午 试 卷 ( 考 试 时 间 14:00~16:30 共 150 分 钟 ) 请 按 下 述 要 求 正 确 填 写 答 题 纸 1. 在 答 题 纸 的 指 定 位 置 填 写 你 所 在 的 省 自 治 区 直 辖 市 计 划 单 列 市 的 名 称 2. 在 答

More information

例 度 讀 讀 不 不 來 念 來 了 讀 不 不 讀 不 讀行 利 了 說 更 了 讀

例 度 讀 讀 不 不 來 念 來 了 讀 不 不 讀 不 讀行 利 了 說 更 了 讀 讀 爛 來 都 力 讀 不 讀 了 讀 來 讀 了 更 不 都 六年 類 更 錄 不 都 便 路 不 不 了 讀 來不 讀 讀 刺 數 不 刺 讀 索 料 易 力 練 讀 易 料 了 讀 力 讀便不 讀 例 度 讀 讀 不 不 來 念 來 了 讀 不 不 讀 不 讀行 利 了 說 更 了 讀 年 來 句 易 說 說 易 說 讀 識 識 力 句 老 錄 朗讀 讀 了 易 臨 說讀 力 識 樂 參 練

More information

OOP with Java 通知 Project 4: 5 月 2 日晚 9 点

OOP with Java 通知 Project 4: 5 月 2 日晚 9 点 OOP with Java Yuanbin Wu cs@ecnu OOP with Java 通知 Project 4: 5 月 2 日晚 9 点 复习 类的复用 组合 (composition): has-a 关系 class MyType { public int i; public double d; public char c; public void set(double x) { d =

More information

RUN_PC連載_8_.doc

RUN_PC連載_8_.doc PowerBuilder 8 (8) Web DataWindow ( ) DataWindow Web DataWindow Web DataWindow Web DataWindow PowerDynamo Web DataWindow / Web DataWindow Web DataWindow Wizard Web DataWindow Web DataWindow DataWindow

More information

软件工程文档编制

软件工程文档编制 实训抽象类 一 实训目标 掌握抽象类的定义 使用 掌握运行时多态 二 知识点 抽象类的语法格式如下 : public abstract class ClassName abstract void 方法名称 ( 参数 ); // 非抽象方法的实现代码 在使用抽象类时需要注意如下几点 : 1 抽象类不能被实例化, 实例化的工作应该交由它的子类来完成 2 抽象方法必须由子类来进行重写 3 只要包含一个抽象方法的抽象类,

More information

Spark读取Hbase中的数据

Spark读取Hbase中的数据 Spark 读取 Hbase 中的数据 Spark 和 Flume-ng 整合, 可以参见本博客 : Spark 和 Flume-ng 整合 使用 Spark 读取 HBase 中的数据 如果想及时了解 Spark Hadoop 或者 Hbase 相关的文章, 欢迎关注微信公共帐号 :iteblog_hadoop 大家可能都知道很熟悉 Spark 的两种常见的数据读取方式 ( 存放到 RDD 中 ):(1)

More information

Apache CarbonData集群模式使用指南

Apache CarbonData集群模式使用指南 我们在 Apache CarbonData 快速入门编程指南 文章中介绍了如何快速使用 Apache CarbonData, 为了简单起见, 我们展示了如何在单机模式下使用 Apache CarbonData 但是生产环境下一般都是使用集群模式, 本文主要介绍如何在集群模式下使用 Apache CarbonData 启动 Spark shell 这里以 Spark shell 模式进行介绍,master

More information

Important Notice SUNPLUS TECHNOLOGY CO. reserves the right to change this documentation without prior notice. Information provided by SUNPLUS TECHNOLO

Important Notice SUNPLUS TECHNOLOGY CO. reserves the right to change this documentation without prior notice. Information provided by SUNPLUS TECHNOLO Car DVD New GUI IR Flow User Manual V0.1 Jan 25, 2008 19, Innovation First Road Science Park Hsin-Chu Taiwan 300 R.O.C. Tel: 886-3-578-6005 Fax: 886-3-578-4418 Web: www.sunplus.com Important Notice SUNPLUS

More information

Chapter 9: Objects and Classes

Chapter 9: Objects and Classes Java application Java main applet Web applet Runnable Thread CPU Thread 1 Thread 2 Thread 3 CUP Thread 1 Thread 2 Thread 3 ,,. (new) Thread (runnable) start( ) CPU (running) run ( ) blocked CPU sleep(

More information

Fuzzy Highlight.ppt

Fuzzy Highlight.ppt Fuzzy Highlight high light Openfind O(kn) n k O(nm) m Knuth O(n) m Knuth Unix grep regular expression exact match Yahoo agrep fuzzy match Gais agrep Openfind gais exact match fuzzy match fuzzy match O(kn)

More information

静态分析 投放文件 行为分析 互斥量 (Mutexes) 执行的命令 创建的服务 启动的服务 进程 cmd.exe PID: 2520, 上一级进程 PID: 2556 cmd.exe PID: 2604, 上一级进程 PID: 2520 访问的文件 C:\Users\test\AppData\Lo

静态分析 投放文件 行为分析 互斥量 (Mutexes) 执行的命令 创建的服务 启动的服务 进程 cmd.exe PID: 2520, 上一级进程 PID: 2556 cmd.exe PID: 2604, 上一级进程 PID: 2520 访问的文件 C:\Users\test\AppData\Lo 魔盾安全分析报告 分析类型 开始时间 结束时间 持续时间 分析引擎版本 FILE 2016-11-25 00:20:03 2016-11-25 00:22:18 135 秒 1.4-Maldun 虚拟机机器名 标签 虚拟机管理 开机时间 关机时间 win7-sp1-x64 win7-sp1-x64 KVM 2016-11-25 00:20:03 2016-11-25 00:22:18 魔盾分数 0.0

More information

Microsoft Word - PHP7Ch01.docx

Microsoft Word - PHP7Ch01.docx PHP 01 1-6 PHP PHP HTML HTML PHP CSSJavaScript PHP PHP 1-6-1 PHP HTML PHP HTML 1. Notepad++ \ch01\hello.php 01: 02: 03: 04: 05: PHP 06:

More information

EJB-Programming-4-cn.doc

EJB-Programming-4-cn.doc EJB (4) : (Entity Bean Value Object ) JBuilder EJB 2.x CMP EJB Relationships JBuilder EJB Test Client EJB EJB Seminar CMP Entity Beans Session Bean J2EE Session Façade Design Pattern Session Bean Session

More information

C H A P T E R 7 Windows Vista Windows Vista Windows Vista FAT16 FAT32 NTFS NTFS New Technology File System NTFS

C H A P T E R 7 Windows Vista Windows Vista Windows Vista FAT16 FAT32 NTFS NTFS New Technology File System NTFS C H P T E R 7 Windows Vista Windows Vista Windows VistaFT16 FT32NTFS NTFSNew Technology File System NTFS 247 6 7-1 Windows VistaTransactional NTFS TxFTxF Windows Vista MicrosoftTxF CIDatomicity - Consistency

More information

// HDevelopTemplateWPF projects located under %HALCONEXAMPLES%\c# using System; using HalconDotNet; public partial class HDevelopExport public HTuple

// HDevelopTemplateWPF projects located under %HALCONEXAMPLES%\c# using System; using HalconDotNet; public partial class HDevelopExport public HTuple halcon 与 C# 混合编程之 Halcon 代码调用 写在前面 完成 halcon 与 C# 混合编程的环境配置后, 进行界面布局设计构思每一个按钮所需要实现 的功能, 将 Halcon 导出的代码复制至相应的 C# 模块下即可 halcon 源程序 : dev_open_window(0, 0, 512, 512, 'black', WindowHandle) read_image (Image,

More information

目錄

目錄 資 訊 素 養 線 上 教 材 單 元 五 資 料 庫 概 論 及 Access 5.1 資 料 庫 概 論 5.1.1 為 什 麼 需 要 資 料 庫? 日 常 生 活 裡 我 們 常 常 需 要 記 錄 一 些 事 物, 以 便 有 朝 一 日 所 記 錄 的 事 物 能 夠 派 得 上 用 場 我 們 能 藉 由 記 錄 每 天 的 生 活 開 銷, 就 可 以 在 每 個 月 的 月 底 知

More information

chp6.ppt

chp6.ppt Java 软 件 设 计 基 础 6. 异 常 处 理 编 程 时 会 遇 到 如 下 三 种 错 误 : 语 法 错 误 (syntax error) 没 有 遵 循 语 言 的 规 则, 出 现 语 法 格 式 上 的 错 误, 可 被 编 译 器 发 现 并 易 于 纠 正 ; 逻 辑 错 误 (logic error) 即 我 们 常 说 的 bug, 意 指 编 写 的 代 码 在 执 行

More information

CC213

CC213 : (Ken-Yi Lee), E-mail: feis.tw@gmail.com 49 [P.51] C/C++ [P.52] [P.53] [P.55] (int) [P.57] (float/double) [P.58] printf scanf [P.59] [P.61] ( / ) [P.62] (char) [P.65] : +-*/% [P.67] : = [P.68] : ,

More information

untitled

untitled JavaEE+Android - 6 1.5-2 JavaEE web MIS OA ERP BOSS Android Android Google Map office HTML CSS,java Android + SQL Sever JavaWeb JavaScript/AJAX jquery Java Oracle SSH SSH EJB+JBOSS Android + 1. 2. IDE

More information

Windows XP

Windows XP Windows XP What is Windows XP Windows is an Operating System An Operating System is the program that controls the hardware of your computer, and gives you an interface that allows you and other programs

More information

2 SGML, XML Document Traditional WYSIWYG Document Content Presentation Content Presentation Structure Structure? XML/SGML 3 2 SGML SGML Standard Gener

2 SGML, XML Document Traditional WYSIWYG Document Content Presentation Content Presentation Structure Structure? XML/SGML 3 2 SGML SGML Standard Gener SGML HTML XML 1 SGML XML Extensible Markup Language XML SGML Standard Generalized Markup Language, ISO 8879, SGML HTML ( Hypertext Markup Language HTML) (Markup Language) (Tag) < > Markup (ISO) 1986 SGML

More information

基于ECO的UML模型驱动的数据库应用开发1.doc

基于ECO的UML模型驱动的数据库应用开发1.doc ECO UML () Object RDBMS Mapping.Net Framework Java C# RAD DataSetOleDbConnection DataGrod RAD Client/Server RAD RAD DataReader["Spell"].ToString() AObj.XXX bug sql UML OR Mapping RAD Lazy load round trip

More information

Go构建日请求千亿微服务最佳实践的副本

Go构建日请求千亿微服务最佳实践的副本 Go 构建 请求千亿级微服务实践 项超 100+ 700 万 3000 亿 Goroutine & Channel Goroutine Channel Goroutine func gen() chan int { out := make(chan int) go func(){ for i:=0; i

More information

Java Access 5-1 Server Client Client Server Server Client 5-2 DataInputStream Class java.io.datainptstream (extends) FilterInputStream InputStream Obj

Java Access 5-1 Server Client Client Server Server Client 5-2 DataInputStream Class java.io.datainptstream (extends) FilterInputStream InputStream Obj Message Transition 5-1 5-2 DataInputStream Class 5-3 DataOutputStream Class 5-4 PrintStream Class 5-5 (Message Transition) (Exercises) Java Access 5-1 Server Client Client Server Server Client 5-2 DataInputStream

More information

WinMDI 28

WinMDI 28 WinMDI WinMDI 2 Region Gate Marker Quadrant Excel FACScan IBM-PC MO WinMDI WinMDI IBM-PC Dr. Joseph Trotter the Scripps Research Institute WinMDI HP PC WinMDI WinMDI PC MS WORD, PowerPoint, Excel, LOTUS

More information

基于UML建模的管理管理信息系统项目案例导航——VB篇

基于UML建模的管理管理信息系统项目案例导航——VB篇 PowerBuilder 8.0 PowerBuilder 8.0 12 PowerBuilder 8.0 PowerScript PowerBuilder CIP PowerBuilder 8.0 /. 2004 21 ISBN 7-03-014600-X.P.. -,PowerBuilder 8.0 - -.TP311.56 CIP 2004 117494 / / 16 100717 http://www.sciencep.com

More information

视频情感计算研究小组 本学年研究工作汇报

视频情感计算研究小组  本学年研究工作汇报 Outline 利用开源工具构建小型搜索引擎 主讲 : 于俊清 搜索引擎体系结构小型搜索引擎 - 目标与功能采集工具 Larbin 简介数据分析与预处理工具 Lucene 简介 Solr 简介 实用化问题 搜索引擎的结构与组成 典型的全文搜索引擎 网页 抓取 预处理 分 词 文档服务器 建立倒 排索引 倒排索引排序 采集器 分析器 索引器 检索器 人机接口 索引数据库 华中科技大学 检索服务器 (

More information

三种方法实现Hadoop(MapReduce)全局排序(1)

三种方法实现Hadoop(MapReduce)全局排序(1) 三种方法实现 Hadoop(MapReduce) 全局排序 () 三种方法实现 Hadoop(MapReduce) 全局排序 () 我们可能会有些需求要求 MapReduce 的输出全局有序, 这里说的有序是指 Key 全局有序 但是我们知道,MapReduce 默认只是保证同一个分区内的 Key 是有序的, 但是不保证全局有序 基于此, 本文提供三种方法来对 MapReduce 的输出进行全局排序

More information

(, : )?,,,,, (, : ),,,, (, ;, ;, : ),,, (, : - ),,, (, : ),,,,,,,,,,,,, -,,,, -,,,, -,,,,,,, ( ), ;, ( ) -,,,,,,

(, : )?,,,,, (, : ),,,, (, ;, ;, : ),,, (, : - ),,, (, : ),,,,,,,,,,,,, -,,,, -,,,, -,,,,,,, ( ), ;, ( ) -,,,,,, : 曹正汉 :, '.,,,., -..,.,,,.,, -., -,,,,,,,,,,,,,,, ( ),,,,,,,?,,?,, ( ), :? (. ) (, ),?, (, : )?,,,,, (, : ),,,, (, ;, ;, : ),,, (, : - ),,, (, : ),,,,,,,,,,,,, -,,,, -,,,, -,,,,,,, ( ), ;, ( ) -,,,,,,

More information

尽 管 Java 语 言 是 在 C++ 语 言 基 础 上 发 展 起 来 的, 但 是 有 别 于 C++,Java 是 一 种 纯 粹 的 面 向 对 象 语 言 (Object-oriented language) 在 像 Java 这 样 纯 粹 的 面 向 对 象 语 言 中, 所 有

尽 管 Java 语 言 是 在 C++ 语 言 基 础 上 发 展 起 来 的, 但 是 有 别 于 C++,Java 是 一 种 纯 粹 的 面 向 对 象 语 言 (Object-oriented language) 在 像 Java 这 样 纯 粹 的 面 向 对 象 语 言 中, 所 有 玩 转 Object 不 理 解, 就 无 法 真 正 拥 有 歌 德 按 其 实 而 审 其 名, 以 求 其 情 ; 听 其 言 而 查 其 累, 无 使 放 悖 ( 根 据 实 际 明 辨 名 称, 以 便 求 得 真 实 情 况 ; 听 取 言 辞 后 弄 明 它 的 类 别, 不 让 它 混 淆 错 乱 ) 三 玩 转 Object 大 围 山 人 玩 转 Object...1 1. 通

More information

學 科 100% ( 為 單 複 選 題, 每 題 2.5 分, 共 100 分 ) 1. 請 參 閱 附 圖 作 答 : (A) 選 項 A (B) 選 項 B (C) 選 項 C (D) 選 項 D Ans:D 2. 下 列 對 於 資 料 庫 正 規 化 (Normalization) 的 敘

學 科 100% ( 為 單 複 選 題, 每 題 2.5 分, 共 100 分 ) 1. 請 參 閱 附 圖 作 答 : (A) 選 項 A (B) 選 項 B (C) 選 項 C (D) 選 項 D Ans:D 2. 下 列 對 於 資 料 庫 正 規 化 (Normalization) 的 敘 ITE 資 訊 專 業 人 員 鑑 定 資 料 庫 系 統 開 發 與 設 計 實 務 試 卷 編 號 :IDS101 注 意 事 項 一 本 測 驗 為 單 面 印 刷 試 題, 共 計 十 三 頁 第 二 至 十 三 頁 為 四 十 道 學 科 試 題, 測 驗 時 間 90 分 鐘 : 每 題 2.5 分, 總 測 驗 時 間 為 90 分 鐘 二 執 行 CSF 測 驗 系 統 -Client

More information

TC35短信发送程序设计

TC35短信发送程序设计 http://www.dragonsoft.net.cn/down/project/tc35_sms.rar TC35 AT /down/book/tc35_at.pdf TC35/TC35i GSM Modem TC35 GSM POS COM SIM DOWN COM E, vbcr AT VB6.0 1)C# http://www.yesky.com/softchannel/72342380468109312/20040523/1800310.shtml,

More information