1、tf = sqrt(phraseFreq)。所以,第一个结果里面的第二个doc的tf = sqrt(1.5)=1.2247。
2、idf = 所有term的idf的和。而一个term的idf = 1 - log((docFreq + 1)/numOfDocs)。也就是说,对于第一个结果来说,idf = 2 * (1 - log(5/4)) = 1.5537。
3、fieldNorm是事先计算好了的,它等于1/sqrt(wordsNum - 1)。比如,文档2的name属性有4个词语,所以,fieldNorm = 1/sqrt(4-1)= 0.57735。可是,Lucene使用8位来表示这个值,所以,会有精度的损失,最后得到的结果是0.5。
TermQuery
TermQuery 最简单(上文提到过), 用Term t=new Term("contents","junit"); new TermQuery(t)就可以构造
TermQuery把查询条件视为一个keyword, 要求和查询内容完全匹配,比如Field.Keyword类型就可以使用TermQuery
RangeQuery
RangeQuery 看名字就知道是表示一个范围的搜索条件,RangeQuery query = new RangeQuery(begin, end, included);
boolean参数表示是否包含边界条件本身, 用字符表示为"[begin TO end]"()包含边界值 或者"{begin TO end}"(不包含边界值)
PrefixQuery
顾名思义,就是表示以XX开头的查询, 字符表示为"something*"
BooleanQuery
逻辑组合的Query,你可以把各种Query添加进去并标明他们的逻辑关系,添加条件用如下方法
public void add(Query query, boolean required, boolean prohibited)
后两个boolean变量是标示AND OR NOT三种关系(如果同时取true的话是不和逻辑的哦 ) 字符表示为" AND OR NOT" 或 "+ -" ,一个BooleanQuery中可以添加多个Query, 如果超过setMaxClauseCount(int)的值(默认1024个)的话,会抛出TooManyClauses错误.
表3:两个参数的组合
| required | |||
false | true | |||
prohibited | false | Clause is optional | Clause must match | |
true | Clause must not | match | Invalid | |
|
|
|
|
|
PhraseQuery
表示不严格语句的查询,比如"quick fox"要匹配"quick brown fox","quick brown high fox"等,PhraseQuery所以提供了一个setSlop()参数,在查询中,lucene会尝试调整单词的距离和位置,这个参数表示可以接受调整次数限制,如果实际的内容可以在这么多步内调整为完全匹配,那么就被视为匹配.在默认情况下slop的值是0, 所以默认是不支持非严格匹配的, 通过设置slop参数(比如"quick fox"匹配"quick brown fox"就需要1个slop来把fox后移动1位),我们可以让lucene来模糊查询. 值得注意的是,PhraseQuery不保证前后单词的次序,在上面的例子中,"fox quick"需要2个slop,也就是如果slop如果大于等于2,那么"fox quick"也会被认为是匹配的.如果是多个Term的搜索,slop指最大的所以的用到次数.看个例子就更明白了:
Lucene系统的排序是根据信息检索的向量空间模型来计算的。文档(d)和查询条件(q)之间越接近,那么文档(d)的得分就越高。计算公司如下:
score(q,d) = coord(q,d) · queryNorm(q) · | ∑ | ( tf(t in d) · idf(t)2 · t.getBoost() · norm(t,d) ) |
| t in q |
|
其中:
1.
tf(t in d)
表示的是查询条件中,每个(t:term)在本文档(d)中的出现频率。查询关键词出现的频率越高,文档的得分就越高。这个部分的默认计算公式是:
frequency½ |
2. idf(t)
表示的是反转文档频率( Inverse Document Frequency).这个函数表示的是(t:term)在所有文档中一共在多少个文档中出现过。因为文档出现的次数越少就越容易定位,所以文档数越少,得分就越高。这个函数的默认计算公式如下:
idf(t) = | 1 + log ( |
| ) |
3.coord(q,d)
这个函数表示的是在这个文档(d)中Term(t)出现的百分比,也就是文档中出现的不同Term数量和查询条件(q)中的不同Term(t)的数量之比。所以,文档中出现的Term种类越多,分值就高。
4.queryNorm(q) 这个函数是一个调节因子,不影响具体的排序情况。主要是用来让排序结果在不同的查询条件(或者不同的索引)之间可以比较。这个条件是在搜索的时候计算的。它的计算公式如下: The sum of squared weights (查询条件的terms)是由查询的权重对象计算的。不同的查询方式,有不同的计算方法。例如:Boolean query的计算公式如下:
queryNorm(q) = queryNorm(sumOfSquaredWeights) = |
| ||||
∑ | ( idf(t) · t.getBoost() ) 2 | ||||
| t in q |
|
5.norm(t,d)
这个函数得到的是一些建索引的时候得到的一些参数计算值:encapsulates a few (indexing time) boost and length factors:
Document boost – 文档的boost,是建索引的时候设置的文档得分。
o Field boost – 在将一个字段加入到文档中去的时候加入的字段得分。(不同的字段得分不同有利于排序,例如标题的得分应该比内容的得分要高等)
o lengthNorm(field) – 文档在建立索引的时候加入的一个参数,根据文档的某个字段含有的Term数量来计算的。Term数量比较少的字段将得到更多的得分。这个函数是由Similarity类在建立索引的时候计算的。 (1/numTerms*numTerms)
当一片文档加入到索引中去的时候,所有的上面因子将乘起来。如果一个文档含有多个同名的当一个文档有多个字段的名字也叫做这个名字的时候,这些字段的boost值也乘一块儿:
norm(t,d)=doc.getBoost()·lengthNorm(field) · | ∏ | |
| field f in d named as t |
|
最 后,norm的结果是经过加密,形成一个单字节的形式保存的。在搜索的时候,这个字节从索引目录中读取,然后解密成float型的norm值。这种压缩/ 解压缩的过程可以减少索引的大小,但是会有一定精度上的损失。也就是说decode(encode(x))并不一定等于x,比如说decode (encode(0.89))=0.75。
架构概览
图一显示了 Lucene 的索引机制的架构。Lucene 使用各种解析器对各种不同类型的文档进行解析。比如对于 HTML 文档,HTML 解析器会做一些预处理的工作,比如过滤文档中的 HTML 标签等等。HTML 解析器的输出的是文本内容,接着 Lucene 的分词器(Analyzer)从文本内容中提取出索引项以及相关信息,比如索引项的出现频率。接着 Lucene 的分词器把这些信息写到索引文件中。
图一:Lucene 索引机制架构
用Lucene索引文档
接下来我将一步一步的来演示如何利用 Lucene 为你的文档创建索引。只要你能将要索引的文件转化成文本格式,Lucene 就能为你的文档建立索引。比如,如果你想为 HTML 文档或者 PDF 文档建立索引,那么首先你就需要从这些文档中提取出文本信息,然后把文本信息交给 Lucene 建立索引。我们接下来的例子用来演示如何利用 Lucene 为后缀名为 txt 的文件建立索引。
1. 准备文本文件
首先把一些以 txt 为后缀名的文本文件放到一个目录中,比如在 Windows 平台上,你可以放到 C://files_to_index 下面。
package lucene.index;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import java.util.Date;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
/**
* This class demonstrates the process of creating an index with Lucene
* for text files in a directory.
*/
public class TextFileIndexer {
public static void main(String[] args) throws Exception{
//fileDir is the directory that contains the text files to be indexed
File fileDir = new File("C://files_to_index ");
//indexDir is the directory that hosts Lucene's index files
File indexDir = new File("C://luceneIndex");
Analyzer luceneAnalyzer = new StandardAnalyzer();
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true);
File[] textFiles = fileDir.listFiles();
long startTime = new Date().getTime();
//Add documents to the index
for(int i = 0; i < textFiles.length; i++){
if(textFiles[i].isFile() >> textFiles[i].getName().endsWith(".txt")){
System.out.println("File " + textFiles[i].getCanonicalPath()
+ " is being indexed");
Reader textReader = new FileReader(textFiles[i]);
Document document = new Document();
document.add(Field.Text("content",textReader));
document.add(Field.Text("path",textFiles[i].getPath()));
indexWriter.addDocument(document);
}
}
indexWriter.optimize();
indexWriter.close();
long endTime = new Date().getTime();
System.out.println("It took " + (endTime - startTime)
+ " milliseconds to create an index for the files in the directory "
+ fileDir.getPath());
}
}
正如清单1所示,你可以利用 Lucene 非常方便的为文档创建索引。接下来我们分析一下清单1中的比较关键的代码,我们先从下面的一条语句开始看起。
Analyzer luceneAnalyzer = new StandardAnalyzer(); |
这条语句创建了类 StandardAnalyzer 的一个实例,这个类是用来从文本中提取出索引项的。它只是抽象类 Analyzer 的其中一个实现。Analyzer 也有一些其它的子类,比如 SimpleAnalyzer 等。
我们接着看另外一条语句:
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true); |
这条语句创建了类 IndexWriter 的一个实例,该类也是 Lucene 索引机制里面的一个关键类。这个类能创建一个新的索引或者打开一个已存在的索引并为该所引添加文档。我们注意到该类的构造函数接受三个参数,第一个参数指 定了存储索引文件的路径。第二个参数指定了在索引过程中使用什么样的分词器。最后一个参数是个布尔变量,如果值为真,那么就表示要创建一个新的索引,如果 值为假,就表示打开一个已经存在的索引。
接下来的代码演示了如何添加一个文档到索引文件中。
Document document = new Document(); document.add(Field.Text("content",textReader)); document.add(Field.Text("path",textFiles[i].getPath())); indexWriter.addDocument(document); |
首先第一行创建了类 Document 的一个实例,它由一个或者多个的域(Field)组成。你可以把这个类想象成代表了一个实际的文档,比如一个 HTML 页面,一个 PDF 文档,或者一个文本文件。而类 Document 中的域一般就是实际文档的一些属性。比如对于一个 HTML 页面,它的域可能包括标题,内容,URL 等。我们可以用不同类型的 Field 来控制文档的哪些内容应该索引,哪些内容应该存储。如果想获取更多的关于 Lucene 的域的信息,可以参考 Lucene 的帮助文档。代码的第二行和第三行为文档添加了两个域,每个域包含两个属性,分别是域的名字和域的内容。在我们的例子中两个域的名字分别是 "content"和"path"。分别存储了我们需要索引的文本文件的内容和路径。最后一行把准备好的文档添加到了索引当中。
当我们把文档添加到索引中后,不要忘记关闭索引,这样才保证 Lucene 把添加的文档写回到硬盘上。下面的一句代码演示了如何关闭索引。
indexWriter.close(); |
利用清单1中的代码,你就可以成功的将文本文档添加到索引中去。接下来我们看看对索引进行的另外一种重要的操作,从索引中删除文档。
从索引中删除文档
类IndexReader负责从一个已经存在的索引中删除文档,如清单2所示。
清单2:从索引中删除文档
File indexDir = new File("C://luceneIndex");IndexReader ir = IndexReader.open(indexDir);ir.delete(1);ir.delete(new Term("path","C://file_to_index/lucene.txt")); ir.close(); |
在清单2中,第二行用静态方法 IndexReader.open(indexDir) 初始化了类 IndexReader 的一个实例,这个方法的参数指定了索引的存储路径。类 IndexReader 提供了两种方法去删除一个文档,如程序中的第三行和第四行所示。第三行利用文档的编号来删除文档。每个文档都有一个系统自动生成的编号。第四行删除了路径 为"C://file_to_index/lucene.txt"的文档。你可以通过指定文件路径来方便的删除一个文档。值得注意的是虽然利用上述代码删 除文档使得该文档不能被检索到,但是并没有物理上删除该文档。Lucene 只是通过一个后缀名为 .delete 的文件来标记哪些文档已经被删除。既然没有物理上删除,我们可以方便的把这些标记为删除的文档恢复过来,如清单 3 所示,首先打开一个索引,然后调用方法 ir.undeleteAll() 来完成恢复工作。
清单3:恢复已删除文档
File indexDir = new File("C://luceneIndex");IndexReader ir = IndexReader.open(indexDir);ir.undeleteAll();ir.close(); |
你现在也许想知道如何物理上删除索引中的文档,方法也非常简单。清单 4 演示了这个过程。
清单4:如何物理上删除文档
File indexDir = new File("C://luceneIndex");Analyzer luceneAnalyzer = new StandardAnalyzer();IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,false);indexWriter.optimize();indexWriter.close(); |
在清单 4 中,第三行创建了类 IndexWriter 的一个实例,并且打开了一个已经存在的索引。第 4 行对索引进行清理,清理过程中将把所有标记为删除的文档物理删除。
Lucene 没有直接提供方法对文档进行更新,如果你需要更新一个文档,那么你首先需要把这个文档从索引中删除,然后把新版本的文档加入到索引中去。
提高索引性能
利用 Lucene,在创建索引的工程中你可以充分利用机器的硬件资源来提高索引的效率。当你需要索引大量的文件时,你会注意到索引过程的瓶颈是在往磁盘上写索 引文件的过程中。为了解决这个问题, Lucene 在内存中持有一块缓冲区。但我们如何控制 Lucene 的缓冲区呢?幸运的是,Lucene 的类 IndexWriter 提供了三个参数用来调整缓冲区的大小以及往磁盘上写索引文件的频率。
1.合并因子(mergeFactor)
这个参数决定了在 Lucene 的一个索引块中可以存放多少文档以及把磁盘上的索引块合并成一个大的索引块的频率。比如,如果合并因子的值是 10,那么当内存中的文档数达到 10 的时候所有的文档都必须写到磁盘上的一个新的索引块中。并且,如果磁盘上的索引块的隔数达到 10 的话,这 10 个索引块会被合并成一个新的索引块。这个参数的默认值是 10,如果需要索引的文档数非常多的话这个值将是非常不合适的。对批处理的索引来讲,为这个参数赋一个比较大的值会得到比较好的索引效果。
2.最小合并文档数
这个参数也会影响索引的性能。它决定了内存中的文档数至少达到多少才能将它们写回磁盘。这个参数的默认值是10,如果你有足够的内存,那么将这个值尽量设的比较大一些将会显著的提高索引性能。
3.最大合并文档数
这个参数决定了一个索引块中的最大的文档数。它的默认值是 Integer.MAX_VALUE,将这个参数设置为比较大的值可以提高索引效率和检索速度,由于该参数的默认值是整型的最大值,所以我们一般不需要改动这个参数。
清单 5 列出了这个三个参数用法,清单 5 和清单 1 非常相似,除了清单 5 中会设置刚才提到的三个参数。
清单5:提高索引性能
/**
* This class demonstrates how to improve the indexing performance
* by adjusting the parameters provided by IndexWriter.
*/
public class AdvancedTextFileIndexer {
public static void main(String[] args) throws Exception{
//fileDir is the directory that contains the text files to be indexed
File fileDir = new File("C://files_to_index");
//indexDir is the directory that hosts Lucene's index files
File indexDir = new File("C://luceneIndex");
Analyzer luceneAnalyzer = new StandardAnalyzer();
File[] textFiles = fileDir.listFiles();
long startTime = new Date().getTime();
int mergeFactor = 10;
int minMergeDocs = 10;
int maxMergeDocs = Integer.MAX_VALUE;
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true);
indexWriter.mergeFactor = mergeFactor;
indexWriter.minMergeDocs = minMergeDocs;
indexWriter.maxMergeDocs = maxMergeDocs;
//Add documents to the index
for(int i = 0; i < textFiles.length; i++){
if(textFiles[i].isFile() >> textFiles[i].getName().endsWith(".txt")){
Reader textReader = new FileReader(textFiles[i]);
Document document = new Document();
document.add(Field.Text("content",textReader));
document.add(Field.Keyword("path",textFiles[i].getPath()));
indexWriter.addDocument(document);
}
}
indexWriter.optimize();
indexWriter.close();
long endTime = new Date().getTime();
System.out.println("MergeFactor: " + indexWriter.mergeFactor);
System.out.println("MinMergeDocs: " + indexWriter.minMergeDocs);
System.out.println("MaxMergeDocs: " + indexWriter.maxMergeDocs);
System.out.println("Document number: " + textFiles.length);
System.out.println("Time consumed: " + (endTime - startTime) + " milliseconds");
}
}
通过这个例子,我们注意到在调整缓冲区的大小以及写磁盘的频率上面 Lucene 给我们提供了非常大的灵活性。现在我们来看一下代码中的关键语句。如下的代码首先创建了类 IndexWriter 的一个实例,然后对它的三个参数进行赋值。
int mergeFactor = 10; int minMergeDocs = 10; int maxMergeDocs = Integer.MAX_VALUE; IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true); indexWriter.mergeFactor = mergeFactor; indexWriter.minMergeDocs = minMergeDocs; indexWriter.maxMergeDocs = maxMergeDocs; |
下面我们来看一下这三个参数取不同的值对索引时间的影响,注意参数值的不同和索引之间的关系。我们为这个实验准备了 10000 个测试文档。表 1 显示了测试结果。表1:测试结果
通过表 1,你可以清楚地看到三个参数对索引时间的影响。在实践中,你会经常的改变合并因子和最小合并文档数的值来提高索引性能。只要你有足够大的内存,你可以为 合并因子和最小合并文档数这两个参数赋尽量大的值以提高索引效率,另外我们一般无需更改最大合并文档数这个参数的值,因为系统已经默认将它设置成了最大。
Lucene 索引文件结构分析
在分析 Lucene 的索引文件结构之前,我们先要理解反向索引(Inverted index)这个概念,反向索引是一种以索引项为中心来组织文档的方式,每个索引项指向一个文档序列,这个序列中的文档都包含该索引项。相反,在正向索引 中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。你可以利用反向索引轻松的找到那些文档包含了特定的索引项。Lucene正是使用 了反向索引作为其基本的索引结构。
在Lucene 中有索引块的概念,每个索引块包含了一定数目的文档。我们能够对单独的索引块进行检索。图 2 显示了 Lucene 索引结构的逻辑视图。索引块的个数由索引的文档的总数以及每个索引块所能包含的最大文档数来决定。
图2:索引文件的逻辑视图
Lucene 中的关键索引文件
下面的部分将会分析Lucene中的主要的索引文件,可能分析有些索引文件的时候没有包含文件的所有的字段,但不会影响到对索引文件的理解。
1.索引块文件
这个文件包含了索引中的索引块信息,这个文件包含了每个索引块的名字以及大小等信息。表 2 显示了这个文件的结构信息。
表2:索引块文件结构
2.域信息文件
我们知道,索引中的文档由一个或者多个域组成,这个文件包含了每个索引块中的域的信息。表 3 显示了这个文件的结构。
表3:域信息文件结构
3.索引项信息文件
这是索引文件里面最核心的一个文件,它存储了所有的索引项的值以及相关信息,并且以索引项来排序。表 4 显示了这个文件的结构。
表4:索引项信息文件结构
4.频率文件
这个文件包含了包含索引项的文档的列表,以及索引项在每个文档中出现的频率信息。如果Lucene在索引项信息文件中发现有索引项和搜索词相匹配。 那么 Lucene 就会在频率文件中找有哪些文件包含了该索引项。表5显示了这个文件的一个大致的结构,并没有包含这个文件的所有字段。
表5:频率文件的结构
5.位置文件
这个文件包含了索引项在每个文档中出现的位置信息,你可以利用这些信息来参与对索引结果的排序。表 6 显示了这个文件的结构
表6:位置文件的结构
到目前为止我们介绍了 Lucene 中的主要的索引文件结构,希望能对你理解 Lucene 的物理的存储结构有所帮助。