package org.apache.lucene.demo;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.IndexWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Date;
//为指定目录下的所有文件建立索引
public class IndexFiles {
private IndexFiles() {}
static final File INDEX_DIR = new File("index");
//存放建立索引的目录
public static void main(String[] args) {
String usage = "java org.apache.lucene.demo.IndexFiles <root_directory>";
// 如果在DOS下直接输入命令java org.apache.lucene.demo.IndexFiles,而没有指定目录名。
if (args.length == 0) {
//args没有接收到任何输入
System.err.println("Usage: " + usage);
System.exit(1);
}
// 如果在DOS下输入命令java org.apache.lucene.demo.IndexFiles myDir,而myDir=index目录已经存在。
if (INDEX_DIR.exists()) {
System.out.println("Cannot save index to '" +INDEX_DIR+ "' directory, please delete it first");
System.exit(1);
}
// 如果在DOS下输入命令java org.apache.lucene.demo.IndexFiles myDir,而myDir目录不存在,则无法创建索引,退出。
final File docDir = new File(args[0]);
// 通过输入的第一个参数构造一个File
if (!docDir.exists() || !docDir.canRead()) {
System.out.println("Document directory '" +docDir.getAbsolutePath()+ "' does not exist or is not readable, please check the path");
System.exit(1);
}
// 如果不存在以上问题,按如下流程执行:
Date start = new Date();
try {
// 通过目录INDEX_DIR构造一个IndexWriter对象
IndexWriter writer = new IndexWriter(INDEX_DIR, new StandardAnalyzer(), true);
System.out.println("Indexing to directory '" +INDEX_DIR+ "'...");
indexDocs(writer, docDir);
System.out.println("Optimizing...");
writer.optimize();
writer.close();
// 计算创建索引文件所需要的时间
Date end = new Date();
System.out.println(end.getTime() - start.getTime() + " total milliseconds");
} catch (IOException e) {
System.out.println(" caught a " + e.getClass() +
"\n with message: " + e.getMessage());
}
}
static void indexDocs(IndexWriter writer, File file)
throws IOException {
// file可以读取
if (file.canRead()) {
if (file.isDirectory()) {
// 如果file是一个目录(该目录下面可能有文件、目录文件、空文件三种情况)
String[] files = file.list(); // 获取file目录下的所有文件(包括目录文件)File对象,放到数组files里
//如果files!=null
if (files != null) {
for (int i = 0; i < files.length; i++) { // 对files数组里面的File对象递归索引,通过广度遍历
indexDocs(writer, new File(file, files[i]));
}
}
} else {
// 到达叶节点时,说明是一个File,而不是目录,则建立索引
System.out.println("adding " + file);
try {
writer.addDocument(FileDocument.Document(file)); // 通过writer,使用file对象构造一个Document对象,添加到writer中,以便能够通过建立的索引查找到该文件
}
catch (FileNotFoundException fnfe) {
;
}
}
}
}
}
上面是一个简单的Demo,主要使用了org.apache.lucene.index包里面的IndexWriter类。IndexWriter有很多构造方法,这个Demo使用了它的如下的构造方法,使用String类型的目录名作为参数之一构造一个索引器:
public IndexWriter(String path, Analyzer a, boolean create)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(FSDirectory.getDirectory(path), a, create, true, null, true);
}
这里,FSDirectory是文件系统目录,该类的方法都是static的,可以直接方便地获取与文件系统目录相关的一些参数,以及对文件系统目录的操作。FSDirectory类继承自抽象类Directory。
如果想要建立索引,需要从IndexWriter的构造方法开始入手:
可以使用一个File对象构造一个索引器:
public IndexWriter(File path, Analyzer a, boolean create)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(FSDirectory.getDirectory(path), a, create, true, null, true);
}
可以使用一个Directory对象构造:
public IndexWriter(Directory d, Analyzer a, boolean create)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(d, a, create, false, null, true);
}
使用具有两个参数的构造函数老构造索引器,指定一个与文件系统目录有关的参数,和一个分词工具,IndexWriter类提供了3个:
public IndexWriter(String path, Analyzer a)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(FSDirectory.getDirectory(path), a, true, null, true);
}
public IndexWriter(File path, Analyzer a)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(FSDirectory.getDirectory(path), a, true, null, true);
}
public IndexWriter(Directory d, Analyzer a)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(d, a, false, null, true);
}
另外,还有5个构造方法,可以参考源文件IndexWriter类。
Analyzer是一个抽象类,能够对数据源进行分析,过滤,主要功能是进行分词:
package org.apache.lucene.analysis;
java.io.Reader;
public abstract class Analyzer {
public abstract TokenStream tokenStream(String fieldName, Reader reader);
public int getPositionIncrementGap(String fieldName)
{
return 0;
}
}
通过使用StandardAnalyzer类(继承自Analyzer抽象类),构造一个索引器IndexWriter。StandardAnalyzer类,对进行检索的word进行了过滤,因为在检索的过程中,有很多对检索需求没有用处的单词。比如一些英文介词:at、with等等,StandardAnalyzer类对其进行了过滤。看下StandardAnalyzer类的源代码:
package org.apache.lucene.analysis.standard;
import org.apache.lucene.analysis.*;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.util.Set;
public class StandardAnalyzer extends Analyzer {
private Set stopSet;
// StopAnalyzer类对检索的关键字进行过滤,这些关键字如果以STOP_WORDS数组中指定的word结尾
public static final String[] STOP_WORDS = StopAnalyzer.ENGLISH_STOP_WORDS;
// 构造一个StandardAnalyzer分析器,下面的几个构造函数都是以不同的方式构造一个限制检索关键字结尾字符串的StandardAnalyzer分析器,可以使用默认的,也可以根据自己的需要设置
public StandardAnalyzer() {
this(STOP_WORDS);
}
public StandardAnalyzer(Set stopWords) {
stopSet = stopWords;
}
public StandardAnalyzer(String[] stopWords) {
stopSet = StopFilter.makeStopSet(stopWords);
}
public StandardAnalyzer(File stopwords) throws IOException {
stopSet = WordlistLoader.getWordSet(stopwords);
}
public StandardAnalyzer(Reader stopwords) throws IOException {
stopSet = WordlistLoader.getWordSet(stopwords);
}
看看StopAnalyzer类,它的构造方法和StandardAnalyzer类的很相似,其中默认的ENGLISH_STOP_WORDS指定了下面这些:
public static final String[] ENGLISH_STOP_WORDS = {
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "such",
"that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"
};
也可以使用带参数的构造函数,根据需要自己指定。
IndexWriter是一个非常重要的工具。建立索引必须从它开始。而且,从它的构造函数开始。
Document和Field是Lucene中两个最重要的概念。在建立索引的时候,也就是实例化一个索引器IndexWriter的之前,必须通过已经建立好的Document逻辑文件,将Document的对象添加到IndexWriter实例中,才能算是建立索引。
Document汇集数据源,这个数据源是通过Field来构造的。构造好Field之后,将每个Field对象加入到Document之中,可以通过Document来管理Field,然后将聚集的Document加入到IndexWriter中,建立索引,写入指定的Directory,为检索做准备。
写一个建立索引,然后读取索引的例子,代码如下:
package org.shirdrn.lucene;
import java.io.File;
import java.io.IOException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
public class MyIndexWriter {
public static void main(String[] args){
File myFile = new File("E:\\Lucene\\myindex");
try {
Document myDoc = new Document();
Field myFieldNo = new Field("myNo","SN-BH-19830119",Field.Store.YES,Field.Index.UN_TOKENIZED);
Field myFieldName = new Field("myName","异域王者",Field.Store.YES,Field.Index.UN_TOKENIZED);
Field myFieldGender = new Field("myGender","男",Field.Store.YES,Field.Index.UN_TOKENIZED);
Field myFieldDescb = new Field("myDescb","2003年,数学系,信息与计算科学;2007年,计算机系,软件与理论",Field.Store.YES,Field.Index.UN_TOKENIZED);
myDoc.add(myFieldNo);
myDoc.add(myFieldName);
myDoc.add(myFieldGender);
myDoc.add(myFieldDescb);
Field hisFieldNo = new Field("hisNo","SN-BH-19860101",Field.Store.YES,Field.Index.UN_TOKENIZED);
Field hisFieldName = new Field("hisName","风平浪静",Field.Store.YES,Field.Index.UN_TOKENIZED);
Field hisFieldGender = new Field("hisGender","男",Field.Store.YES,Field.Index.UN_TOKENIZED);
Field hisFieldDescb = new Field("hisDescb","2003年,历史系,世界史;2007年,计算机系,人工智能",Field.Store.YES,Field.Index.UN_TOKENIZED);
myDoc.add(hisFieldNo);
myDoc.add(hisFieldName);
myDoc.add(hisFieldGender);
myDoc.add(hisFieldDescb);
IndexWriter myWriter = new IndexWriter(myFile,new StandardAnalyzer(),true);
// 构造一个索引器,true指定了:向已经存在的索引中追加索引
myWriter.addDocument(myDoc);
myWriter.close(); // 关闭索引器,将追加的索引文件写入到索引目录中
IndexReader myReader = IndexReader.open(myFile);
// 读取索引
for(int i=0;i<myReader.numDocs();i++){
System.out.println(myReader.document(i)); // 输出索引文件的信息
}
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里,使用File myFile = new File("E:\\Lucene\\myindex");作为一个参数来构造一个索引器。
运行程序,可以在目录E:\Lucene\myindex下看到生成的索引文件,一共有三个文件:
_0.cfs
segments.gen
segments_3
第一个是.cfs格式的,成为复合索引格式;第三个segments_3是一个索引段。
同时,可以在控制台上看到输出结果:
Document<stored/uncompressed,indexed<myNo:SN-BH-19830119> stored/uncompressed,indexed<myName:异域王者> stored/uncompressed,indexed<myGender:男> stored/uncompressed,indexed<myDescb:2003年,数学系,信息与计算科学;2007年,计算机系,软件与理论> stored/uncompressed,indexed<hisNo:SN-BH-19860101> stored/uncompressed,indexed<hisName:风平浪静> stored/uncompressed,indexed<hisGender:男> stored/uncompressed,indexed<hisDescb:2003年,历史系,世界史;2007年,计算机系,人工智能>>
因为上面程序中使用了IndexReader,先打开索引文件,然后通过IndexReader的document()来读取索引文件的内容。
org.apache.lucene.demo.IndexFiles类中,使用递归的方式去索引文件。在构造了一个IndexWriter索引器之后,就可以向索引器中添加Doucument了,执行真正地建立索引的过程。遍历每个目录,因为每个目录中可能还存在目录,进行深度遍历,采用递归技术找到处于叶节点处的文件(普通的具有扩展名的文件,比如my.txt文件),然后调用如下代码中红色部分:
static void indexDocs(IndexWriter writer, File file) throws IOException { // file可以读取 if (file.canRead()) { if (file.isDirectory()) {
// 如果file是一个目录(该目录下面可能有文件、目录文件、空文件三种情况) String[] files = file.list();
// 获取file目录下的所有文件(包括目录文件)File对象,放到数组files里
// 如果files!=null if (files != null) { for (int i = 0; i < files.length; i++) {
// 对files数组里面的File对象递归索引,通过广度遍历 indexDocs(writer, new File(file, files[i])); } } } else {
// 到达叶节点时,说明是一个File,而不是目录,则建立索引 System.out.println("adding " + file); try { writer.addDocument(FileDocument.Document(file)); } catch (FileNotFoundException fnfe) { ; } } } }
上面红色标注的这一句:
writer.addDocument(FileDocument.Document(file));
其实做了很多工作。每当递归到叶子节点,获得一个文件,而非目录文件,比如文件myWorld.txt。然后对这个文件进行了复杂的操作:
先根据由myWorld.txt构造的File对象f,通过f获取myWorld.txt的具体信息,比如存储路径、修改时间等等,构造多个Field对象,再由这些不同Field的聚合,构建出一个Document对象,最后把Document对象加入索引器IndexWriter对象中,通过索引器可以对这些聚合的Document的Field中信息进行分词、过滤处理,方便检索。
org.apache.lucene.demo.FileDocument类的源代码如下所示:
package org.apache.lucene.demo;
import java.io.File; import java.io.FileReader;
import org.apache.lucene.document.DateTools; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field;
public class FileDocument { public static Document Document(File f) throws java.io.FileNotFoundException {
// 实例化一个Document Document doc = new Document(); // 根据传进来的File f,构造多个Field对象,然后把他们都添加到Document中
// 通过f的所在路径构造一个Field对象,并设定该Field对象的一些属性: // “path”是构造的Field的名字,通过该名字可以找到该Field // Field.Store.YES表示存储该Field;Field.Index.UN_TOKENIZED表示不对该Field进行分词,但是对其进行索引,以便检索
doc.add(new Field("path", f.getPath(), Field.Store.YES, Field.Index.UN_TOKENIZED));
// 构造一个具有最近修改修改时间信息的Field doc.add(new Field("modified", DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.UN_TOKENIZED));
// 构造一个Field,这个Field可以从一个文件流中读取,必须保证由f所构造的文件流是打开的 doc.add(new Field("contents", new FileReader(f))); return doc; }
private FileDocument() {} }
通过上面的代码,可以看出Field是何其的重要,必须把Field完全掌握了。
Field类定义了两个很有用的内部静态类:Store和Index,用它们来设置对Field进行索引时的一些属性。
// Store是一个内部类,它是static的,主要为了设置Field的存储属性
public static final class Store extends Parameter implements Serializable {
private Store(String name) { super(name); }
// 在索引中压缩存储Field的值 public static final Store COMPRESS = new Store("COMPRESS");
// 在索引中存储Field的值 public static final Store YES = new Store("YES");
// 在索引中不存储Field的值 public static final Store NO = new Store("NO"); }
//通过Index设置索引方式 public static final class Index extends Parameter implements Serializable {
private Index(String name) { super(name); }
// 不对Field进行索引,所以这个Field就不能被检索到(一般来说,建立索引而使它不被检索,这是没有意义的) // 如果对该Field还设置了Field.Store为Field.Store.YES或Field.Store.COMPRESS,则可以检索
public static final Index NO = new Index("NO");
// 对Field进行索引,同时还要对其进行分词(由Analyzer来管理如何分词) public static final Index TOKENIZED = new Index("TOKENIZED");
// 对Field进行索引,但不对其进行分词 public static final Index UN_TOKENIZED = new Index("UN_TOKENIZED");
// 对Field进行索引,但是不使用Analyzer public static final Index NO_NORMS = new Index("NO_NORMS");
}
Field类中还有一个内部类,它的声明如下:
public static final class TermVector extends Parameter implements Serializable
这是一个与词条有关的类。因为在检索的时候需要指定检索关键字,通过为一个Field添加一个TermVector,就可以在检索中把该Field检索到。它的构造函数:
private TermVector(String name) { super(name); }
通过指定一个字符串,来构造一个Field的TermVector,指定该Field的对词条的设置方式,如下:
// 不存储 public static final TermVector NO = new TermVector("NO");
// 为每个Document都存储一个TermVector public static final TermVector YES = new TermVector("YES");
// 存储,同时存储位置信息 public static final TermVector WITH_POSITIONS = new TermVector("WITH_POSITIONS"); // 存储,同时存储偏移量信息 public static final TermVector WITH_OFFSETS = new TermVector("WITH_OFFSETS"); // 存储,同时存储位置、偏移量信息 public static final TermVector WITH_POSITIONS_OFFSETS = new TermVector("WITH_POSITIONS_OFFSETS"); }
同时,Field的值可以构造成很多类型,Field类中定义了4种:String、Reader、byte[]、TokenStream。
然后就是Field对象的构造,应该看它的构造方法,它有7种构造方法:
public Field(String name, byte[] value, Store store) public Field(String name, Reader reader) public Field(String name, Reader reader, TermVector termVector) public Field(String name, String value, Store store, Index index) public Field(String name, String value, Store store, Index index, TermVector termVector) public Field(String name, TokenStream tokenStream) public Field(String name, TokenStream tokenStream, TermVector termVector)
还要注意了,通过Field类的声明:
public final class Field extends AbstractField implements Fieldable, Serializable
可以看出,应该对它继承的父类AbstractField类有一个了解,下面的是AbstractField类的属性:
protected String name = "body"; protected boolean storeTermVector = false; protected boolean storeOffsetWithTermVector = false; protected boolean storePositionWithTermVector = false; protected boolean omitNorms = false; protected boolean isStored = false; protected boolean isIndexed = true; protected boolean isTokenized = true; protected boolean isBinary = false; protected boolean isCompressed = false; protected boolean lazy = false; protected float boost = 1.0f; protected Object fieldsData = null;
还有Field实现了Fieldable接口,添加了一些对对应的Document中的Field进行管理判断的方法信息。
|
建立索引,通过已经生成的索引文件,实现通过关键字检索。
写了一个类MySearchEngine,根据上述思想实现,把Lucene自带的递归建立索引的方法提取出来,加了一个搜索的方法:
package org.shirdrn.lucene;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Date;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.demo.FileDocument;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class MySearchEngine {
private File file;
private String indexPath;
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public String getIndexPath() {
return indexPath;
}
public void setIndexPath(String indexPath) {
this.indexPath = indexPath;
}
public void createIndex(IndexWriter writer, File file) throws IOException {
// file可以读取
if (file.canRead()) {
if (file.isDirectory()) {
// 如果file是一个目录(该目录下面可能有文件、目录文件、空文件三种情况)
String[] files = file.list();
// 获取file目录下的所有文件(包括目录文件)File对象,放到数组files里
// 如果files!=null
if (files != null) {
for (int i = 0; i < files.length; i++) {
// 对files数组里面的File对象递归索引,通过广度遍历
createIndex(writer, new File(file, files[i]));
}
}
}
else { // 到达叶节点时,说明是一个File,而不是目录,则为该文件
建立索引
try {
writer.addDocument(FileDocument.Document(file));
}
catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
}
}
}
public void searchContent(String type,String keyword){ // 根据指定的检索内容类型type,以及检索关键字keyword进行检索操作
try {
IndexSearcher searcher = new IndexSearcher(this.indexPath); // 根据指定路径,构造一个IndexSearcher检索器
Term term = new Term(type,keyword); // 创建词条
Query query = new TermQuery(term);
// 创建查询
Date startTime = new Date();
TermDocs termDocs = searcher.getIndexReader().termDocs(term); // 执行检索操作
while(termDocs.next()){
// 遍历输出根据指定词条检索的结果信息
System.out.println("搜索的该关键字【"+keyword+"】在文件\n"+searcher.getIndexReader().document(termDocs.doc()));
System.out.println("中,出现过 "+termDocs.freq()+" 次");
}
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime(); // 计算检索花费时间
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里引用了import org.apache.lucene.demo.FileDocument,在创建Field的时候,为每个Field都设置了三种属性:path、modified、contents。在检索的时候,只要指定其中的一个就可以从索引中检索出来。
public static void main(String[] args){
MySearchEngine mySearcher = new MySearchEngine();
String indexPath = "E:\\Lucene\\myindex";
File file = new File("E:\\Lucene\\txt");
mySearcher.setIndexPath(indexPath);
mySearcher.setFile(file);
IndexWriter writer;
try {
writer = new IndexWriter(mySearcher.getIndexPath(),new StandardAnalyzer(),true);
mySearcher.createIndex(writer, mySearcher.getFile());
mySearcher.searchContent("contents","server");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
在构造IndexWriter的时候,选择分词器不同,对检索的结果有很大的影响。
我感觉,这里自带的StandardAnalyzer和SimpleAnalyzer对中文的支持不是很好,因为它是对像英文这样的,以空格作为分隔符的,中文不同英文的习惯,可能有时候根本检索不出中文。
使用StandardAnalyzer和SimpleAnalyzer的时候,检索关键字“server“,结果是相同的,结果如下所示:
搜索的该关键字【server】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\license.txt> stored/uncompressed,indexed<modified:200106301125>>
中,出现过 2 次
搜索的该关键字【server】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\Patch.txt> stored/uncompressed,indexed<modified:200112160745>>
中,出现过 8 次
搜索的该关键字【server】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\REDIST.TXT> stored/uncompressed,indexed<modified:200511120152>>
中,出现过 1 次
搜索的该关键字【server】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\新建 文本文档.txt> stored/uncompressed,indexed<modified:200710270258>>
中,出现过 27 次
本次搜索所用的时间为 0 ms
但是,如果使用StandardAnalyzer检索中文,mySearcher.searchContent("contents","的");,结果可以看出来:
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\120E升级包安装说明.txt> stored/uncompressed,indexed<modified:200803101357>>
中,出现过 2 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\1实验题目.txt> stored/uncompressed,indexed<modified:200710160733>>
中,出现过 1 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\3实验题目.txt> stored/uncompressed,indexed<modified:200710300744>>
中,出现过 2 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\CustomKeyInfo.txt> stored/uncompressed,indexed<modified:200406041814>>
中,出现过 80 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\CustomKeysSample.txt> stored/uncompressed,indexed<modified:200610100451>>
中,出现过 8 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\FAQ.txt> stored/uncompressed,indexed<modified:200604130754>>
中,出现过 348 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\MyEclipse 注册码.txt> stored/uncompressed,indexed<modified:200712061059>>
中,出现过 5 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\readme.txt> stored/uncompressed,indexed<modified:200803101314>>
中,出现过 17 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\Struts之AddressBooks学习笔记.txt> stored/uncompressed,indexed<modified:200710131711>>
中,出现过 8 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\Update.txt> stored/uncompressed,indexed<modified:200707050028>>
中,出现过 177 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\Visual Studio 2005注册升级.txt> stored/uncompressed,indexed<modified:200801300512>>
中,出现过 3 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\书籍网站.txt> stored/uncompressed,indexed<modified:200708071255>>
中,出现过 3 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\使用技巧集萃.txt> stored/uncompressed,indexed<modified:200511210413>>
中,出现过 153 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\关系记录.txt> stored/uncompressed,indexed<modified:200802201145>>
中,出现过 24 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\剑心补丁使用说明(readme).txt> stored/uncompressed,indexed<modified:200803101357>>
中,出现过 17 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\史上最强天籁之声及欧美流行曲超级精选【 FLAC分轨】.txt> stored/uncompressed,indexed<modified:200712231241>>
中,出现过 1 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\密码强度检验.txt> stored/uncompressed,indexed<modified:200712010901>>
中,出现过 1 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\新1建 文本文档.txt> stored/uncompressed,indexed<modified:200710311142>>
中,出现过 39 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\新建 文本文档.txt> stored/uncompressed,indexed<modified:200710270258>>
中,出现过 4 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\汉化说明.txt> stored/uncompressed,indexed<modified:200708210247>>
中,出现过 16 次
本次搜索所用的时间为 16 ms
使用SimpleAnalyzer的时候,检索中文关键字,结果很不准确。mySearcher.searchContent("contents","的");,结果可以看出来:
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\Update.txt> stored/uncompressed,indexed<modified:200707050028>>
中,出现过 1 次
搜索的该关键字【的】在文件
Document<stored/uncompressed,indexed<path:E:\Lucene\txt\使用技巧集萃.txt> stored/uncompressed,indexed<modified:200511210413>>
中,出现过 1 次
本次搜索所用的时间为 0 ms
现在感觉到了,分词在建立索引的时候是一件多么重要的事情啊。
研究Lucene分析器的实现。
Analyzer抽象类
所有的分析器的实现,都是继承自抽象类Analyzer,它的源代码如下所示:
package org.apache.lucene.analysis;
import java.io.Reader;
public abstract class Analyzer { // 通过Field的名称,和一个Reader对象,创建一个分词流,该方法是抽象方法 public abstract TokenStream tokenStream(String fieldName, Reader reader);
//个人理解,感觉这个方法是在后台分词用的,因为对一个文件建立索引,要构造Field,可能有重复的。
public int getPositionIncrementGap(String fieldName) { return 0; } }
这里,tokenStream()的作用非常大。它返回一个TokenStream类对象,这个TokenStream类对象应该是已经经过分词器处理过的。
与Analyzer抽象类有关的其他类
TokenStream也是一个抽象类:
package org.apache.lucene.analysis;
import java.io.IOException;
// 对后台选择的待分析处理的文件,一个TokenStream对象包含了对这个文件分词的词条序列
public abstract class TokenStream {
// 返回下一个分词的词条 public abstract Token next() throws IOException;
// 重置一个分词流,恢复到分词工作的开始状态 public void reset() throws IOException {}
// 关闭分词流,停止分词 public void close() throws IOException {} }
TokenStream类的方法表明,它最基本的是对分词流的状态进行管理。具体地,它如何对分析的对象处理,应该从继承该抽象类的子类的构造来看。
在包org.apache.lucene.analysis下可以看到有两个TokenStream的子类:Tokenizer和TokenFilter,它们还都是抽象类,从这两个抽象类可以看出,是在TokenStream的基础上,按照功能进行分类实现:处理分词、过滤分词。
Tokenizer类在Lucene中定义如下所示:
package org.apache.lucene.analysis;
import java.io.Reader; import java.io.IOException;
// Tokenizer所处理的输入来源是一个Reader对象
public abstract class Tokenizer extends TokenStream {
// 一个Reader对象作为它的成员 protected Reader input;
protected Tokenizer() {}
protected Tokenizer(Reader input) { this.input = input; }
// 关闭输入流 public void close() throws IOException { input.close(); } }
接着,看看TokenFilter类的实现,TokenFilter类在Lucene中定义如下所示:
package org.apache.lucene.analysis;
import java.io.IOException;
// TokenFilter是TokenStream的子类,在分词之后进行,起过滤器的作用 public abstract class TokenFilter extends TokenStream {
// 它以一个TokenStream对象作为成员 protected TokenStream input;
protected TokenFilter(TokenStream input) { this.input = input; }
public void close() throws IOException { input.close(); } }
TokenFilter是可以嵌套Tokenizer的:
当一个Tokenizer对象不为null时,如果需要对其进行过滤,可以构造一个TokenFilter来对分词的词条进行过滤。
同样地,在包org.apache.lucene.analysis下可以找到继承自Tokenizer类的具体实现类。
很明显了,实现Tokenizer类的具体类应该是分词的核心所在了。
对指定文本建立索引之前,应该先构造Field对象,在此基础上再构造Document对象,然后添加到IndexWriter中进行分析处理。在这个分析处理过程中,包含对其进行分词(Tokenizer),而经过分词处理以后,返回的是一个Token对象(经过分词器得到的词条),它可能是Field中的一个Term的一部分。
看一看Token类都定义了哪些内容:
package org.apache.lucene.analysis;
import org.apache.lucene.index.Payload; import org.apache.lucene.index.TermPositions;
public class Token implements Cloneable { String termText; // 一个词条的内容 int startOffset;
// 记录在源文件中第一次出现的位置 int endOffset;
// 记录在源文件中最后一次出现的位置t String type = "word";
// lexical type
Payload payload;
private int positionIncrement = 1; // 位置增量
public Token(String text, int start, int end) { // 初始化一个词条实例时,初始化词条文本内容、开始位置、最后位置 termText = text; startOffset = start; endOffset = end; }
public Token(String text, int start, int end, String typ) { // 初始化一个词条实例时,初始化词条文本内容、开始位置、最后位置、类型 termText = text; startOffset = start; endOffset = end; type = typ; }
// 设置位移增量的,相对于TokenStream中该Token的前一个,只能设置为1或0
//默认为1,如果为0,则表示多个Term都具有同一个位置。
public void setPositionIncrement(int positionIncrement) { if (positionIncrement < 0) throw new IllegalArgumentException ("Increment must be zero or greater: " + positionIncrement); this.positionIncrement = positionIncrement; }
public int getPositionIncrement() { return positionIncrement; }
// 设置词条(Token)的内容 public void setTermText(String text) { termText = text; }
public final String termText() { return termText; }
// 返回该词条(Token)在一个文件(待建立索引的文件)中的起始位置 public final int startOffset() { return startOffset; }
// 返回该词条(Token)在一个文件(待建立索引的文件)中的结束位置 public final int endOffset() { return endOffset; }
// 返回Token's lexical type
public final String type() { return type; }
// Payload是一个元数据(metadata)对象,对每个检索的词条(Term)都设置相应的Payload,存储在index中,通过Payload可以获取一个词条(Term)的详细信息 public void setPayload(Payload payload) { this.payload = payload; } public Payload getPayload() { return this.payload; }
// 将一个词条(Token) 的信息,转换成字符串形式,在该字符串中,使用逗号作为每个属性值的间隔符
public String toString() { StringBuffer sb = new StringBuffer(); sb.append("(" + termText + "," + startOffset + "," + endOffset); if (!type.equals("word")) sb.append(",type="+type); if (positionIncrement != 1) sb.append(",posIncr="+positionIncrement); sb.append(")"); return sb.toString(); }
// 需要的时候,该Token对象 可以被克隆
public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); // shouldn't happen since we implement Cloneable } } }
继承Tokenizer类的直接子类
Tokenizer类的直接子类有:
CharTokenizer(抽象类)、KeywordTokenizer、
org.apache.lucene.analysis.standard.StandardTokenizer、
org.apache.lucene.analysis.cn.ChineseTokenizer、
org.apache.lucene.analysis.ngram.NGramTokenizer、
org.apache.lucene.analysis.ngram.EdgeNGramTokenizer。
其中:
CharTokenizer是一个抽象类,它又有3个子类,如下:
org.apache.lucene.analysis.ru.RussianLetterTokenizer、
.WhitespaceTokenizer、LetterTokenizer(都和CharTokenizer类在包org.apache.lucene.analysis里)。
最后,LowerCaseTokenizer是最终类,又是LetterTokenizer类的子类
|
Lucene分析器的实现。
Lucene分词器Tokenizer,它的继承子类的实现。
Tokenizer类的继承关系,如图所示:
ChineseTokenizer类实现中文分词
中文分词在Lucene中的处理很简单,就是单个字分。它的实现类为ChineseTokenizer,在包org.apache.lucene.analysis.cn中,源代码如下:
package org.apache.lucene.analysis.cn;
import java.io.Reader;
import org.apache.lucene.analysis.*;
public final class ChineseTokenizer extends Tokenizer {
public ChineseTokenizer(Reader in) {
input = in;
}
private int offset = 0, bufferIndex=0, dataLen=0;
private final static int MAX_WORD_LEN = 255;
private final static int IO_BUFFER_SIZE = 1024;
private final char[] buffer = new char[MAX_WORD_LEN];
private final char[] ioBuffer = new char[IO_BUFFER_SIZE];
private int length;
private int start;
private final void push(char c) { // 对待分词的文本进行预处理,输入到缓冲区buffer中
if (length == 0) start = offset-1; // 根据词条长度,设置起始位置索引
buffer[length++] = Character.toLowerCase(c); // 预处理:将中文Unicode码转化成小写
}
private final Token flush() { // 根据缓冲区预处理后的文本,构造词条
if (length>0) {
return new Token(new String(buffer, 0, length), start, start+length);
}
else
return null;
}
public final Token next() throws java.io.IOException { // 返回下一个词条
length = 0;
start = offset;
while (true) {
final char c;
offset++;
if (bufferIndex >= dataLen) { // 当缓冲区没有溢出
dataLen = input.read(ioBuffer);
bufferIndex = 0;
}
if (dataLen == -1) return flush();
else
c = ioBuffer[bufferIndex++];
switch(Character.getType(c)) {
case Character.DECIMAL_DIGIT_NUMBER:
case Character.LOWERCASE_LETTER:
case Character.UPPERCASE_LETTER:
push(c);
if (length == MAX_WORD_LEN) return flush();
break;
case Character.OTHER_LETTER:
if (length>0) {
bufferIndex--;
offset--;
return flush();
}
push(c);
return flush();
default:
if (length>0) return flush();
break;
}
}
}
}
这里,还提及到一个CJKTokenizer分词类,它处理分词的时候,比ChineseTokenizer分词处理要好一点,但是也存在弊病,源代码给了一个例子,如下:
如果一个中文词汇C1C2C3C4被索引,使用ChineseTokenizer分词,返回的词条(Token)为:C1,C2,C3,C4;使用CJKTokenizer进行分词,则返回的词条(Token)为:C1C2,C2C3,C3C4。
问题在于:当检索关键字为C1,C1C2,C1C3,C4C2,C1C2C3……的时候,ChineseTokenizer可以对其实现分词,而CJKTokenizer就不能实现分词了。
CJKTokenizer类实现中文分词
CJKTokenizer类的源代码如下所示:
package org.apache.lucene.analysis.cjk;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.Tokenizer;
import java.io.Reader;
public final class CJKTokenizer extends Tokenizer {
/** Max word length */
private static final int MAX_WORD_LEN = 255;
/** buffer size: */
private static final int IO_BUFFER_SIZE = 256;
/** word offset, used to imply which character(in ) is parsed */
private int offset = 0;
/** the index used only for ioBuffer */
private int bufferIndex = 0;
/** data length */
private int dataLen = 0;
/**
* 字符缓冲区,存储那些经过处理后返回的词条
*/
private final char[] buffer = new char[MAX_WORD_LEN];
/**
* I/O buffer, used to store the content of the input(one of the <br>
* members of Tokenizer)
*/
private final char[] ioBuffer = new char[IO_BUFFER_SIZE];
/** word type: single=>ASCII double=>non-ASCII word=>default */
private String tokenType = "word";
private boolean preIsTokened = false;
public CJKTokenizer(Reader in) {
input = in;
}
public final Token next() throws java.io.IOException {
int length = 0;
/** the position used to create Token */
int start = offset;
while (true) {
/** current charactor */
char c;
/** unicode block of current charactor for detail */
Character.UnicodeBlock ub;
offset++;
if (bufferIndex >= dataLen) {
dataLen = input.read(ioBuffer);
bufferIndex = 0;
}
if (dataLen == -1) {
if (length > 0) {
if (preIsTokened == true) {
length = 0;
preIsTokened = false;
}
break;
} else {
return null;
}
} else {
//get current character
c = ioBuffer[bufferIndex++];
//get the UnicodeBlock of the current character
ub = Character.UnicodeBlock.of(c);
}
//if the current character is ASCII or Extend ASCII
if ((ub == Character.UnicodeBlock.BASIC_LATIN)
|| (ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS)
) {
if (ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {
/** convert HALFWIDTH_AND_FULLWIDTH_FORMS to BASIC_LATIN */
int i = (int) c;
i = i - 65248;
c = (char) i;
}
// if the current character is a letter or "_" "+" "#"
if (Character.isLetterOrDigit(c)
|| ((c == '_') || (c == '+') || (c == '#'))
) {
if (length == 0) {
// "javaC1C2C3C4linux" <br>
// ^--: the current character begin to token the ASCII
// letter
start = offset - 1;
} else if (tokenType == "double") {
offset--;
bufferIndex--;
tokenType = "single";
if (preIsTokened == true) {
// there is only one non-ASCII has been stored
length = 0;
preIsTokened = false;
break;
} else {
break;
}
}
// store the LowerCase(c) in the buffer
buffer[length++] = Character.toLowerCase(c);
tokenType = "single";
// break the procedure if buffer overflowed!
if (length == MAX_WORD_LEN) {
break;
}
} else if (length > 0) {
if (preIsTokened == true) {
length = 0;
preIsTokened = false;
} else {
break;
}
}
} else {
// non-ASCII letter, eg."C1C2C3C4"
if (Character.isLetter(c)) {
if (length == 0) {
start = offset - 1;
buffer[length++] = c;
tokenType = "double";
} else {
if (tokenType == "single") {
offset--;
bufferIndex--;
//return the previous ASCII characters
break;
} else {
buffer[length++] = c;
tokenType = "double";
if (length == 2) {
offset--;
bufferIndex--;
preIsTokened = true;
break;
}
}
}
} else if (length > 0) {
if (preIsTokened == true) {
// empty the buffer
length = 0;
preIsTokened = false;
} else {
break;
}
}
}
}
return new Token(new String(buffer, 0, length), start, start + length,
tokenType
);
}
}
CharTokenizer是一个抽象类,它主要是对西文字符进行分词处理的。常见的英文中,是以空格、标点为分隔符号的,在分词的时候,就是以这些分隔符作为分词的间隔符的。
package org.apache.lucene.analysis;
import java.io.IOException;
import java.io.Reader;
// CharTokenizer 是一个抽象类
public abstract class CharTokenizer extends Tokenizer {
public CharTokenizer(Reader input) {
super(input);
}
private int offset = 0, bufferIndex = 0, dataLen = 0;
private static final int MAX_WORD_LEN = 255;
private static final int IO_BUFFER_SIZE = 1024;
private final char[] buffer = new char[MAX_WORD_LEN];
private final char[] ioBuffer = new char[IO_BUFFER_SIZE];
protected abstract boolean isTokenChar(char c);
// 对字符进行处理,可以在CharTokenizer 的子类中实现
protected char normalize(char c) {
return c;
}
// 这个是核心部分,返回分词后的词条
public final Token next() throws IOException {
int length = 0;
int start = offset;
while (true) {
final char c;
offset++;
if (bufferIndex >= dataLen) {
dataLen = input.read(ioBuffer);
bufferIndex = 0;
}
;
if (dataLen == -1) {
if (length > 0)
break;
else
return null;
} else
c = ioBuffer[bufferIndex++];
if (isTokenChar(c)) { // if it's a token char
if (length == 0) // start of token
start = offset - 1;
buffer[length++] = normalize(c); // buffer it, normalized
if (length == MAX_WORD_LEN)
// buffer overflow!
break;
} else if (length > 0)
// at non-Letter w/ chars
break; // return 'em
}
return new Token(new String(buffer, 0, length), start, start + length);
}
}
实现CharTokenizer的具体类有3个,分别为:LetterTokenizer、RussianLetterTokenizer、WhitespaceTokenizer。
先看看LetterTokenizer类,其它的2个都是基于CharTokenizer的,而核心又是next() 方法:
package org.apache.lucene.analysis;
import java.io.Reader;
// 只要读取到非字符的符号,就分词
public class LetterTokenizer extends CharTokenizer {
public LetterTokenizer(Reader in) {
super(in);
}
protected boolean isTokenChar(char c) {
return Character.isLetter(c);
}
}
做个测试就可以看到:
package org.shirdrn.lucene;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import org.apache.lucene.analysis.LetterTokenizer;
public class LetterTokenizerTest {
public static void main(String[] args) {
Reader reader = new StringReader("That's a world,I wonder why.");
LetterTokenizer ct = new LetterTokenizer(reader);
try {
System.out.println(ct.next());
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果如下:
(That,0,4)
在分词过程中,遇到了单引号,就把单引号之前的作为一个词条返回。
可以验证一下,把构造的Reader改成下面的形式:
Reader reader = new StringReader("ThatisaworldIwonderwhy.");
输出结果为:
(ThatisaworldIwonderwhy,0,22)
没有非字符的英文字母串就可以作为一个词条,一个词条长度的限制为255个字符,可以在CharTokenizer抽象类中看到定义:
private static final int MAX_WORD_LEN = 255;
Lucene分析器的实现。
Lucene(分词)过滤器TokenFilter类,以及继承它的子类的实现类。
TokenFilter类的继承关系,如图所示:
TokenFilter是一个抽象类,定义了对一个经过分词(Tokenizer)后的TokenStream进行过滤的功能,它的源代码如下所示:
package org.apache.lucene.analysis;
import java.io.IOException;
public abstract class TokenFilter extends TokenStream {
// 通过输入一个TokenStream protected TokenStream input;
protected TokenFilter(TokenStream input) { this.input = input; }
public void close() throws IOException { input.close(); } }
ChineseFilter中文过滤器类:
package org.apache.lucene.analysis.cn;
import java.util.Hashtable; import org.apache.lucene.analysis.*;
// 中文过滤器,包含了含有外文字符的情况,因为中文搜索的关键字可能是:中文+外文
public final class ChineseFilter extends TokenFilter {
// 这里给出了英文的终止词汇,如果直接调用根本对中文没有过滤作用,或者也可以在此添加终止词汇,比如:“也、了、啊、吧、呵……”等等一些助词叹词,因为这些词语对于检索没有意义 public static final String[] STOP_WORDS = { "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with" };
private Hashtable stopTable;
// 构造函数,初始化一个终止词汇表 (过滤掉的词汇)
public ChineseFilter(TokenStream in) { super(in);
stopTable = new Hashtable(STOP_WORDS.length); for (int i = 0; i < STOP_WORDS.length; i++) stopTable.put(STOP_WORDS[i], STOP_WORDS[i]); }
// 如果stopTable为空,说明该过滤类根本没有进行过滤,直接将读入的词条(Token)返回
public final Token next() throws java.io.IOException {
for (Token token = input.next(); token != null; token = input.next()) { String text = token.termText(); // 获取分词后词条的内容
// 这里,如果chineseChar是单个字,则Character.getType(chineseChar)返回的类型值为5;大写英文字符类型值为1,小写字符类型值为2
if (stopTable.get(text) == null) {
// 如果分词后的词条内容没有出现在过滤列表stopTable中 switch (Character.getType(text.charAt(0))) {
case Character.LOWERCASE_LETTER: case Character.UPPERCASE_LETTER:
// 如果英文词条不是单个字符 if (text.length()>1) { return token; } break; case Character.OTHER_LETTER:
// 如果中文的过滤词汇表中是单个字的形式,则若是一个词汇中有一个字出现在stopTable中,那么整个词条都被过滤掉了。这是不合理的;所以只要把stopTable设置为空就可以实现,即不对分词的词条进行过滤 // 也可以在这里添加对词条的限制,来实现过滤
return token; }
}
} return null; }
}
再看一个限制词条长度的过滤器类:
package org.apache.lucene.analysis;
import java.io.IOException;
// 词条过长或者过短,则过滤掉
public final class LengthFilter extends TokenFilter {
final int min; final int max;
// 构造函数,初始化此条长度上限和下限
public LengthFilter(TokenStream in, int min, int max) { super(in); this.min = min; this.max = max; }
// 返回长度在min与max之间的词条 public final Token next() throws IOException { for (Token token = input.next(); token != null; token = input.next()) { int len = token.termText().length(); if (len >= min && len <= max) { return token; } // note: else we ignore it but should we index each part of it? }
return null; } }
可见,过滤器的目的就是对分词获得的词条的一些属性信息进行过滤,原则是:这些属性信息对检索没有实际意义。通过阅读标准过滤器类,再综合各种需要,我们就能根据自己的需要实现词条的过滤。Lucene包里给出的标准过滤器类。其实是对英文字符过滤的的,源代码如下:
package org.apache.lucene.analysis.standard;
import org.apache.lucene.analysis.*;
public final class StandardFilter extends TokenFilter implements StandardTokenizerConstants {
public StandardFilter(TokenStream in) { super(in); }
// APOSTROPHE、ACRONYM等都是在一个常量接口类StandardTokenizerConstants里定义的常量,分别代表在过滤中可能要对其进行处理的字符
private static final String APOSTROPHE_TYPE = tokenImage[APOSTROPHE]; private static final String ACRONYM_TYPE = tokenImage[ACRONYM];
// 如果词条不空,进行处理后返回经过过滤处理后的词条 public final org.apache.lucene.analysis.Token next() throws java.io.IOException { org.apache.lucene.analysis.Token t = input.next();
if (t == null) return null;
String text = t.termText(); String type = t.type();
// 英文中的 's或'S对检索意义不大,可以删除掉 if (type == APOSTROPHE_TYPE && (text.endsWith("'s") || text.endsWith("'S"))) { return new org.apache.lucene.analysis.Token (text.substring(0,text.length()-2),t.startOffset(), t.endOffset(), type);
} else if (type == ACRONYM_TYPE) { // 对“圆点”进行处理,直接把出现的“圆点”删除掉,保留其它字符
StringBuffer trimmed = new StringBuffer(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c != '.') trimmed.append(c); } return new org.apache.lucene.analysis.Token (trimmed.toString(), t.startOffset(), t.endOffset(), type);
} else { return t; } } }
常量接口类StandardTokenizerConstants 定义如下:
package org.apache.lucene.analysis.standard;
public interface StandardTokenizerConstants {
int EOF = 0; int ALPHANUM = 1; int APOSTROPHE = 2; int ACRONYM = 3; int COMPANY = 4; int EMAIL = 5; int HOST = 6; int NUM = 7; int P = 8; int HAS_DIGIT = 9; int ALPHA = 10; int LETTER = 11; int CJ = 12; int KOREAN = 13; int DIGIT = 14; int NOISE = 15;
int DEFAULT = 0;
String[] tokenImage = { "<EOF>", "<ALPHANUM>", "<APOSTROPHE>", "<ACRONYM>", "<COMPANY>", "<EMAIL>", "<HOST>", "<NUM>", "<P>", "<HAS_DIGIT>", "<ALPHA>", "<LETTER>", "<CJ>", "<KOREAN>", "<DIGIT>", "<NOISE>", }; }
还有一个org.apache.lucene.analysis.StopFilter类,和ChineseFilter类的实现很相似,只是这里把过滤字符列表是初始化一个StopFilter过滤器的时候指定的,而且该类实现了对过滤字符类表中字符进行转换的功能。
我感觉,最应该好好研究的是关于同义词的过滤问题。Lucene包中给了一个org.apache.lucene.index.memory.SynonymTokenFilter过滤类,比较复杂,因为这里面涉及到了一个重要的类:org.apache.lucene.index.memory.SynonymMap。通过研究对英文中同义词的过滤,来考虑中文同义词过滤的问题。
|
Lucene的StandardAnalyzer分析器。
不同的Lucene分析器Analyzer,它对TokenStream进行分词的方法是不同的,这需要根据具体的语言来选择。比如英文,一般是通过空格来分割词条,而中文汉字则不能通过这种方式,最简单的方式就是单个汉字作为一个词条。
TokenStream是通过从设备或者其他地方获取数据源而构造的一个流,我们要执行分词的动作,应该对这个TokenStream进行操作。
TokenStream也可以不是直接通过数据源构造的流,可以是经过分词操作之后读入TokenFilter的一个分词流。
从本地磁盘的文件读取文本内容,假定在文本文件shirdrn.txt中有下列文字:
中秋之夜,享受着月华的孤独,享受着爆炸式的思维跃迁。
通过使用FileReader构造一个流,对其进行分词:
package org.shirdrn.lucene;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
public class MyAnalyzer {
public static void main(String[] args) {
try {
File file = new File("E:\\shirdrn.txt");
Reader reader = new FileReader(file);
Analyzer a = new StandardAnalyzer();
//Analyzer a = new CJKAnalyzer();
//Analyzer a = new ChineseAnalyzer();
//Analyzer a = new WhitespaceAnalyzer();
TokenStream ts = a.tokenStream("", reader);
Token t = null;
int n = 0;
while((t = ts.next()) != null ){
n ++ ;
System.out.println("词条"+n+"的内容为 :"+t.termText());
}
System.out.println("== 共有词条 "+n+" 条 ==");
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里使用StandardAnalyzer分析器,而且使用了不带参数的构造器StandardAnalyzer(),在StandardAnalyzer类的这个不带参数的构造器中,指定了一个过滤字符数组STOP_WORDS:
public StandardAnalyzer() {
this(STOP_WORDS);
}
而在StandardAnalyzer类中定义的STOP_WORDS 数组实际是引用StopAnalyzer类的ENGLISH_STOP_WORDS数组,该数组中可以根据需要添加过滤的字符:
public static final String[] STOP_WORDS = StopAnalyzer.ENGLISH_STOP_WORDS;
StopAnalyzer类中ENGLISH_STOP_WORDS数组原始内容如下所示:
public static final String[] ENGLISH_STOP_WORDS = {
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "such",
"that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"
};
都是一些英文单词,而且这些单词对于检索关键字意义不大,所以在分析的时候应该把出现的这些单词过滤掉。
如果按照默认的STOP_WORDS运行上面我们的测试程序,则根本没有对中文起到过滤作用,测试结果如下所示:
词条1的内容为 :中
词条2的内容为 :秋
词条3的内容为 :之
词条4的内容为 :夜
词条5的内容为 :享
词条6的内容为 :受
词条7的内容为 :着
词条8的内容为 :月
词条9的内容为 :华
词条10的内容为 :的
词条11的内容为 :孤
词条12的内容为 :独
词条13的内容为 :享
词条14的内容为 :受
词条15的内容为 :着
词条16的内容为 :爆
词条17的内容为 :炸
词条18的内容为 :式
词条19的内容为 :的
词条20的内容为 :思
词条21的内容为 :维
词条22的内容为 :跃
词条23的内容为 :迁
== 共有词条 23 条 ==
我们可以在org.apache.lucene.analysis.StopAnalyzer类中定制自己的STOP_WORDS,例如我们定义:
public static final String[] ENGLISH_STOP_WORDS = {
"着", "的", "之", "式"
};
则再执行上面的测试程序,分词过程中会过滤掉出现在ENGLISH_STOP_WORDS数组中的词条,如下所示:
词条1的内容为 :中
词条2的内容为 :秋
词条3的内容为 :夜
词条4的内容为 :享
词条5的内容为 :受
词条6的内容为 :月
词条7的内容为 :华
词条8的内容为 :孤
词条9的内容为 :独
词条10的内容为 :享
词条11的内容为 :受
词条12的内容为 :爆
词条13的内容为 :炸
词条14的内容为 :思
词条15的内容为 :维
词条16的内容为 :跃
词条17的内容为 :迁
== 共有词条 17 条 ==
另外,因为StandardAnalyzer类具有很多带参数的构造函数,可以在实例化一个StandardAnalyzer的时候,通过构造函数定制分析器,例如使用:
public StandardAnalyzer(Set stopWords)
构造的分析器如下:
Set stopWords = new HashSet();
stopWords.add("着");
stopWords.add("的");
stopWords.add("之");
stopWords.add("式");
Analyzer a = new StandardAnalyzer(stopWords);
运行结果同修改StopAnalyzer类中的STOP_WORDS结果是一样的。
还有一个构造函数,通过使用数组指定stopWords的过滤词条:
public StandardAnalyzer(String[] stopWords) {
stopSet = StopFilter.makeStopSet(stopWords);
}
调用了StopFilter类的makeStopSet方法对stopWords中的字符进行了转换处理:
public static final Set makeStopSet(String[] stopWords) {
return makeStopSet(stopWords, false);
}
又调用了该类的一个重载的方法makeStopSet,第一个参数指定过滤词条的数组,第一个参数为boolean类型,设置是否要将大写字符转换为小写:
public static final Set makeStopSet(String[] stopWords, boolean ignoreCase) {
HashSet stopTable = new HashSet(stopWords.length);
for (int i = 0; i < stopWords.length; i++)
stopTable.add(ignoreCase ? stopWords[i].toLowerCase() : stopWords[i]);
return stopTable;
}
在StandardAnalyzer类中,没有把stopWords中的词条转换为小写。
上面的三种构造StandardAnalyzer分析器的方式都是在程序中指定要过滤词条,程序的独立性比较差,因为每次想要添加过滤词条都需要改动程序。
StandardAnalyzer还提供了两种从数据源读取过滤词条的文本的构造方式:
public StandardAnalyzer(File stopwords) throws IOException {
stopSet = WordlistLoader.getWordSet(stopwords);
}
public StandardAnalyzer(Reader stopwords) throws IOException {
stopSet = WordlistLoader.getWordSet(stopwords);
}
他们分别使用File和Reader分别来构造一个File对象和读取字符流,从指定的数据源读取内容,然后调用WordlistLoader类的getWordSet静态方法来对读取的字符流进行转换操作,以从File对象中获取字符为例:
public static HashSet getWordSet(File wordfile) throws IOException {
HashSet result = new HashSet();
FileReader reader = null;
try {
reader = new FileReader(wordfile);
result = getWordSet(reader);
}
finally {
if (reader != null)
reader.close();
}
return result;
}
实际上仍然通过File对象构造一个FileReader读取字符流,然后从流中取得过滤的词条,加入到HashSet 中。这里调用了获取HashSet的getWordSet方法,在方法getWordSet中才真正地实现了提取词条的操作:
public static HashSet getWordSet(Reader reader) throws IOException {
HashSet result = new HashSet();
BufferedReader br = null;
try {
if (reader instanceof BufferedReader) {
br = (BufferedReader) reader;
} else {
br = new BufferedReader(reader);
}
String word = null;
while ((word = br.readLine()) != null) {
result.add(word.trim());
}
}
finally {
if (br != null)
br.close();
}
return result;
}
这里提取词条要求读入的文本是按照行来分割过滤词条的,即每行作为一个词条。对于中文,只能是每个字作为一行,如果以两个的词语作为一行,处理后根本没有加入到过滤词条的HashSet中,这时因为StandardAnalyzer分析器是以单个中文汉字作为一个词条的。我们可以定制自己的分析器。
测试一下上述说明的情况。
在本地磁盘上建立一个txt文本stopWords.txt,添加过滤词条:
着
的
之
式
测试程序如下所示:
public static void main(String[] args) {
try {
File file = new File("E:\\shirdrn.txt");
FileReader stopWords = new FileReader("E:\\stopWords.txt");
Reader reader = new FileReader(file);
Analyzer a = new StandardAnalyzer(stopWords);
TokenStream ts = a.tokenStream("", reader);
Token t = null;
int n = 0;
while((t = ts.next()) != null ){
n ++ ;
System.out.println("词条"+n+"的内容为 :"+t.termText());
}
System.out.println("== 共有词条 "+n+" 条 ==");
} catch (Exception e) {
e.printStackTrace();
}
}
测试输出结果同前面的一样,都对词条进行了过滤:
词条1的内容为 :中
词条2的内容为 :秋
词条3的内容为 :夜
词条4的内容为 :享
词条5的内容为 :受
词条6的内容为 :月
词条7的内容为 :华
词条8的内容为 :孤
词条9的内容为 :独
词条10的内容为 :享
词条11的内容为 :受
词条12的内容为 :爆
词条13的内容为 :炸
词条14的内容为 :思
词条15的内容为 :维
词条16的内容为 :跃
词条17的内容为 :迁
== 共有词条 17 条 ==
Lucene的CJKAnalyzer分析器。
CJKAnalyzer分析器的思想:
对中文汉字,每两个字作为一个词条,例如A,B,C,D是四个中文汉字,使用CJKAnalyzer分析器分词后一共得到三个词条如下:
AB,BC,CD。
其实,CJKAnalyzer分析器在对中文分词方面比StandardAnalyzer分析器要好一点。因为根据中文的习惯,包括搜索的时候键入关键字的习惯,中文的词(大于一个汉字)比单个汉字的频率应该高一些。
但是,在设置相同的过滤词条文本以后,CJKAnalyzer分析器的缺点就是产生了冗余会比较大,相对于StandardAnalyzer分析器来说。使用StandardAnalyzer分析器可以考虑在以字作为词条时,通过过滤词条文本来优化分词。而CJKAnalyzer分析器在给定的过滤词条文本的基础之上,获取有用的词条实际是一个在具有一定中文语言习惯的基础上能够获得最高的期望。
如果使用默认的过滤词条文本:
package org.shirdrn.lucene;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cjk.CJKAnalyzer;
public class MyAnalyzer {
public static void main(String[] args) {
try {
File file = new File("E:\\shirdrn.txt");
FileReader stopWords = new FileReader("E:\\stopWords.txt");
Reader reader = new FileReader(file);
Analyzer a = new CJKAnalyzer();
TokenStream ts = a.tokenStream("", reader);
Token t = null;
int n = 0;
while((t = ts.next()) != null ){
n ++ ;
System.out.println("词条"+n+"的内容为 :"+t.termText());
}
System.out.println("== 共有词条 "+n+" 条 ==");
} catch (Exception e) {
e.printStackTrace();
}
}
}
即:没有对中文词条限制,结果可以看到:
词条1的内容为 :中秋
词条2的内容为 :秋之
词条3的内容为 :之夜
词条4的内容为 :享受
词条5的内容为 :受着
词条6的内容为 :着月
词条7的内容为 :月华
词条8的内容为 :华的
词条9的内容为 :的孤
词条10的内容为 :孤独
词条11的内容为 :享受
词条12的内容为 :受着
词条13的内容为 :着爆
词条14的内容为 :爆炸
词条15的内容为 :炸式
词条16的内容为 :式的
词条17的内容为 :的思
词条18的内容为 :思维
词条19的内容为 :维跃
词条20的内容为 :跃迁
== 共有词条 20 条 ==
产生的无用的词条大概占50%左右,而且,如果被分词的文件很大,存储也有一定的开销,相对于使用StandardAnalyzer分析器。相对于使用StandardAnalyzer分析器,使用CJKAnalyzer分析器的存储开销是StandardAnalyzer分析器的两倍。
这里,无论是那种分词方式(对于StandardAnalyzer分析器和CJKAnalyzer分析器来说),都要考虑对重复的词条进行处理。
CJKAnalyzer分析器的分词工具是CJKTokenizer核心类。至于如果过滤,这和StandardAnalyzer分析器很相似,但是它只是设置了在程序中指定了一个stopTable。可以参考StandardAnalyzer分析器实现读取文件系统中的文本的实现。
Lucene的ChineseAnalyzer分析器。
ChineseAnalyzer分析器其实就是StandardAnalyzer分析器,对单个的中文汉字作为一个词条。
也可以指定一个stopTable。
对数据源进行分析,是为建立索引服务的;为指定的文件建立索引,是为检索服务的。
对数据源分析,使用Lucene的分析器(Analyzer),根据分析器所得到的词条,构造一个索引器IndexWriter。索引器IndexWriter的功能主要就是创建索引,是建立索引工作中最核心的。
当构造完一个索引器IndexWriter之后,就可以向其中添加Document了。
在前面
Lucene-2.2.0 源代码阅读学习(1)
中,根据Lucene提供的一个Demo,详细分析研究一下索引器org.apache.lucene.index.IndexWriter类,看看它是如果定义的,掌握它建立索引的机制。
通过IndexWriter类的实现源代码可知,它包含的内容相当丰富了。乍一看无从入手,不知道从何处去分析。可以通过Lucene提供的Demo的,根据它的实现过程l来一点点地解析。
在Demo中,实例化一个索引器:
IndexWriter writer = new IndexWriter(INDEX_DIR, new StandardAnalyzer(), true);
它使用的IndexWriter的一个构造函数,定义如下所示:
public IndexWriter(String path, Analyzer a, boolean create)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(FSDirectory.getDirectory(path), a, create, true, null, true);
}
这个构造函数具有三个参数:
path :根据一个字符串描述的路径,为建立的索引文件指定存放目录。
a :一个分析器。
create:它是一个boolean型变量,如果为true,表示要重写指定的存放索引目录下的索引文件;如果为false,表示在指定存放索引目录下已经存在的索引文件的基础上,向其中继续追加新的索引文件。
简单做个测试吧:
package org.shirdrn.lucene;
import java.io.IOException;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
public class MyIndexWriterTest {
public static void main(String[] args) throws Exception {
String path = "E:\\index";
Analyzer a = new StandardAnalyzer();
IndexWriter myWriter = new IndexWriter(path,a,true);
}
}
这里没有指定要进行建立索引的文件,因此,应该是“建立空索引”。这里,指定了create的值为true,是新建索引文件。
这里,只是实例化了一个IndexWriter索引器,并没有建立索引。建立索引的过程是在一个IndexWriter索引器实例存在的前提下,通过为其添加Document,这样才能真正就爱你里索引。
运行程序,可以在指定的索引文件的存放目录E:\\index下看到生成的三个与索引有关的文件:
segments.gen (大小为1K)
segments_1 (大小为1K)
write.lock (大小为0K)
如果再次运行程序,会发现文件segments_1变成了segments_2,再次运行还会变成segments_3……,这就说明参数create为true表示重写现存的索引文件。
如果第一次执行上述程序,指定create为false,由于指定的索引目录下面缺少被追加索引的索引文件,将会抛出异常:
Exception in thread "main" java.io.FileNotFoundException: no segments* file found in
org.apache.lucene.store.FSDirectory@E:\index
: files:
at org.apache.lucene.index.SegmentInfos$FindSegmentsFile.run(SegmentInfos.java:516)
at org.apache.lucene.index.SegmentInfos.read(SegmentInfos.java:249)
at org.apache.lucene.index.IndexWriter.init(IndexWriter.java:616)
at org.apache.lucene.index.IndexWriter.<init>(IndexWriter.java:360)
at org.shirdrn.lucene.MyIndexWriterTest.main(MyIndexWriterTest.java:22)
从异常来看,追加索引在现存的索引文件的基础上追加。
如果指定的索引目录下已经存在一些索引文件,并且指定create的值为false,则执行向已存在的索引文件中追加索引,就可以看到索引目录下面的文件不会发生变化,程序执行会因为write.lock文件而抛出异常:
Exception in thread "main" org.apache.lucene.store.LockObtainFailedException: Lock obtain timed out:
SimpleFSLock@E:\index\write.lock
at org.apache.lucene.store.Lock.obtain(Lock.java:70)
at org.apache.lucene.index.IndexWriter.init(IndexWriter.java:598)
at org.apache.lucene.index.IndexWriter.<init>(IndexWriter.java:360)
at org.shirdrn.lucene.MyIndexWriterTest.main(MyIndexWriterTest.java:22)
继续看上面IndexWriter的构造函数的函数体内容:
init(FSDirectory.getDirectory(path), a, create, true, null, true);
IndexWriter的构造函数初始化,调用了一个init方法,init方法在IndexWriter类中有具体实现,它还有一个重载的init方法。先看这里用到的这个,它的实现代码如下所示:
/**
* 该方法中的参数列表中。各个参数的含义如下:
* d :指定的存放建立索引文件的索引目录
* a :一个传递进来分析器
* create :是否要重新写入索引文件,如果为true,则重写索引文件;如果为false,则追加写入索引文件
* closeDir :一个boolean型变量,表示是否关闭索引目录Directory d,与IndexWriter的一个成员变量相关
* deletionPolicy :指定删除索引文件使用的策略
* autoCommit :建立索引文件后,自动提交。
*/
private void init(Directory d, Analyzer a, final boolean create, boolean closeDir, IndexDeletionPolicy deletionPolicy, boolean autoCommit)
throws CorruptIndexException, LockObtainFailedException, IOException {
this.closeDir = closeDir;
directory = d;
analyzer = a;
this.infoStream = defaultInfoStream; // infoStream是一个PrintStream输出流对象,在这里指定它为defaultInfoStream=null。PrintStream类继承自FilterOutputStream,主要是对建立索引文件过程中的输出流进行管理
if (create) {
// 如果是残留索引文件,则清除写锁文件write.lock
directory.clearLock(IndexWriter.WRITE_LOCK_NAME);
}
Lock writeLock = directory.makeLock(IndexWriter.WRITE_LOCK_NAME);
if (!writeLock.obtain(writeLockTimeout)) // 获取写锁文件
throw new LockObtainFailedException("Index locked for write: " + writeLock);
this.writeLock = writeLock; // 重新保存写锁文件
try {
if (create) { // 如果create为true,表示重写索引文件。重写索引文件之前,要先读取已经存在的索引文件,并且要清除掉历史写入的segment信息
try {
segmentInfos.read(directory);
segmentInfos.clear();
} catch (IOException e) {
}
segmentInfos.write(directory); // 向指定的索引存放目录中写入segment信息
} else { // 如果create为false,只是读取,并不予以清除,因为是追加写入索引文件
segmentInfos.read(directory);
}
this.autoCommit = autoCommit; // 执行提交写入索引的标志
if (!autoCommit) {
// 如果如果提交写入索引失败,要回滚到原来的状态
rollbackSegmentInfos = (SegmentInfos) segmentInfos.clone(); // 克隆
}
// 默认的删除策略实现类为KeepOnlyLastCommitDeletionPolicy,它只是保证将最近提交删除的索引文件,提交删除动作
// IndexFileDeleter deleter是IndexWriter类的一个私有的成员变量,它在org.apache.lucene.index包里面,主要对删除索引文件进行实现和管理
deleter = new IndexFileDeleter(directory,
deletionPolicy == null ? new KeepOnlyLastCommitDeletionPolicy() : deletionPolicy,segmentInfos, infoStream);
} catch (IOException e) {
this.writeLock.release();
// 索引写入完成之后,要释放写锁
this.writeLock = null;
throw e;
}
}
通过IndexWriter索引器的构造函数,以及它初始化时调用的一个init方法,可以了解一个IndexWriter索引器的构造最重要的是在init方法中的初始化工作。它主要实现了根据指定的建立索引的方式(重写、追加写入),通过create标志位来判断,从而指定一种在操作索引文件的过程中删除索引文件的策略。
必须深入地理解IndexWriter索引器初始化的过程,以及在构造一个IndexWriter索引器过程中涉及到的一些其他的类,应该深入研究这些相关的类的定义。这样才能有助于深化IndexWriter索引器构造的原理机制。
可见,IndexWriter索引器的构造相当复杂,但是却非常重要。
接着昨天学习的
Lucene-2.2.0 源代码阅读学习(11)
继续学习。
IndexWriter的一个构造器,定义如下所示:
public IndexWriter(String path, Analyzer a, boolean create) throws CorruptIndexException, LockObtainFailedException, IOException { init(FSDirectory.getDirectory(path), a, create, true, null, true); }
已经知道,init方法的复杂性和重要性。对init方法所涉及到的有用的相关类进行系统地学习。
init方法的声明是这样的:
private void init(Directory d, Analyzer a, final boolean create, boolean closeDir, IndexDeletionPolicy deletionPolicy, boolean autoCommit)
首先看一下Directory这个类的继承关系,如图所示:
Directory是一个抽象类,它是与目录有关的一个类,主要定义了一些与目录相关的属性和行为,源代码如下所示:
package org.apache.lucene.store;
import java.io.IOException;
public abstract class Directory {
// 有目录相关的一个锁工厂,主要是为向目录中写入文件服务,使用工厂模式,写入文件内容必须获取锁
protected LockFactory lockFactory;
// 获取该目录下的所有文件名称字符串的一个数组
public abstract String[] list() throws IOException;
// 判断指定文件名称为name的文件是否存在
public abstract boolean fileExists(String name) throws IOException;
// 获取指定文件名称为name的文件最后修改的时间
public abstract long fileModified(String name) throws IOException;
// 设置指定文件名称为name的文件的修改时间为当前时间
public abstract void touchFile(String name) throws IOException;
// 删除已经存在于该目录下的指定文件名称为namde的文件 public abstract void deleteFile(String name) throws IOException;
// 重新命名文件,将原文件名from修改为to
public abstract void renameFile(String from, String to) throws IOException;
// 获取
指定文件名称为name的文件的长度
public abstract long fileLength(String name) throws IOException;
// 在该目录下创建一个名称为name的文件,同时返回一个索引输出流,以便向其中写入内容
public abstract IndexOutput createOutput(String name) throws IOException;
// 读取该目录下名称为name的文件,返回一个输入流,以便对该文件进行相关操作 public abstract IndexInput openInput(String name) throws IOException;
/** 读取该目录下名称为name的文件,指定了读入缓冲区的大小为bufferSize,返回一个输入流 * Currently the only Directory implementations that respect this * parameter are
{@link
FSDirectory} and
{@link
* org.apache.lucene.index.CompoundFileReader}. */
public IndexInput openInput(String name, int bufferSize) throws IOException { return openInput(name); }
// 获取一个锁对象,其中name指定的锁文件的名称,即write.lock文件
public Lock makeLock(String name) { return lockFactory.makeLock(name); } // 当指定名称为name的锁文件不再被使用的时候,从锁工厂LockFactory中清除该锁;注意:是从内存中清除,并非从文件系统中删除。 public void clearLock(String name) throws IOException { if (lockFactory != null) { lockFactory.clearLock(name); } }
// 关闭该目录,不再对该目录操作了
public abstract void close() throws IOException;
// 当该目录被管理,要对其进行操作之前,必须先创建一个锁工厂LockFactory实例,只有拥有了锁工厂才可以获取锁实例
public void setLockFactory(LockFactory lockFactory) { this.lockFactory = lockFactory; lockFactory.setLockPrefix(this.getLockID()); }
// 获取锁工厂实例 public LockFactory getLockFactory() { return this.lockFactory; }
// 过去锁实例的唯一表示ID的字符串描述
public String getLockID() { return this.toString(); }
// 拷贝源目录src下的文件,复制到目的目录dest下面,拷贝完成后关闭源目录src
public static void copy(Directory src, Directory dest, boolean closeDirSrc) throws IOException { final String[] files = src.list(); // 获取源目录src下的所有文件
if (files == null)
// 如果源目录src下没有文件,则抛出异常 throw new IOException("cannot read directory " + src + ": list() returned null");
// 如果获取到的源目录src目录下的文件列表files不空,就执行复制操作
byte[] buf = new byte[BufferedIndexOutput.BUFFER_SIZE]; for (int i = 0; i < files.length; i++) { IndexOutput os = null; IndexInput is = null; try { // 在目的目录dest下面创建一个新文件,名称与从源目录src下获得的文件名称相同 os = dest.createOutput(files[i]); // 打开源目录下对应的该文件,返回一个索引输入流 is = src.openInput(files[i]);
// 复制到目录目录下 long len = is.length(); long readCount = 0; while (readCount < len) { int toRead = readCount + BufferedIndexOutput.BUFFER_SIZE > len ? (int)(len - readCount) : BufferedIndexOutput.BUFFER_SIZE; is.readBytes(buf, 0, toRead); // 以字节流的方式读取源目录的文件,将toRead个字节读入到缓冲区buf中 os.writeBytes(buf, toRead); // 以字节流的方式写入目的目录的文件中,将缓冲区buf中toRead个字节写入到目的目录对应的文件中 readCount += toRead; } } finally { // 最后要关闭输入流 try { if (os != null) os.close(); } finally { if (is != null) is.close();
// 关闭输入流 } } } if(closeDirSrc) src.close(); // 关闭源目录 } }
从Directory抽象类的定义,我们可以得到如下几点:
1、管理锁工厂及其锁实例;
2、管理Directory目录实例的基本属性,主要是通过文件名称进行管理;
3、管理与操作该目录相关的一些流对象;
4、管理索引文件的拷贝。
然后就要对Directory抽象类的具体实现类进行学习了。
|
Directory抽象类比较常用的具体实现子类应该是FSDirectory类和RAMDirectory类。FSDirectory类是与文件系统的目录相关的,而RAMDirectory类是与内存相关的,即是指内存中的一个临时非永久的区域。
FSDirectory类源代码定义如下:
package org.apache.lucene.store;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Hashtable;
import org.apache.lucene.index.IndexFileNameFilter;
import org.apache.lucene.index.IndexWriter;
public class FSDirectory extends Directory {
// DIRECTORIES需要进行同步,用于存放指定键值对<File,FSDirectory>
private static final Hashtable DIRECTORIES = new Hashtable();
private static boolean disableLocks = false;
/**
* 设置锁文件是否可以使用
* They should only be disabled if the index
* is on a read-only medium like a CD-ROM.
*/
public static void setDisableLocks(boolean doDisableLocks) {
FSDirectory.disableLocks = doDisableLocks;
}
// 获取锁文件是否可以使用,返回true表示不可以使用
public static boolean getDisableLocks() {
return FSDirectory.disableLocks;
}
// 锁目录,该属性已经废弃
public static final String LOCK_DIR = System.getProperty("org.apache.lucene.lockDir",
System.getProperty("java.io.tmpdir"));
// 定义一个静态内部类,该类是基于文件系统的
private static Class IMPL;
static {
try {
String name =
System.getProperty("org.apache.lucene.FSDirectory.class",
FSDirectory.class.getName());
IMPL = Class.forName(name);
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot load FSDirectory class: " + e.toString(), e);
} catch (SecurityException se) {
try {
IMPL = Class.forName(FSDirectory.class.getName());
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot load default FSDirectory class: " + e.toString(), e);
}
}
}
private static MessageDigest DIGESTER;
static {
try {
DIGESTER = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e.toString(), e);
}
}
/** A buffer optionally used in renameTo method */
private byte[] buffer = null;
// 静态方法,根据指定的路径path,返回该路径的一个FSDirectiry实例
public static FSDirectory getDirectory(String path)
throws IOException {
return getDirectory(new File(path), null);
}
// 静态方法,根
据指定的路径path以及锁工厂LockFactory参数,返回该路径的一个FSDirectiry实例
public static FSDirectory getDirectory(String path, LockFactory lockFactory)
throws IOException {
return getDirectory(new File(path), lockFactory);
}
// 静态方法,根据指定的File对象,返回该路径的一个FSDirectiry实例
public static FSDirectory getDirectory(File file)
throws IOException {
return getDirectory(file, null);
}
// 静态方法,根据指定File对象以及锁工厂LockFactory参数,返回该路径的一个FSDirectiry实例
// 该方法是获取FSDirectiry实例的核心方法,其他重载的该方法都是经过转换,调用该方法实现的
public static FSDirectory getDirectory(File file, LockFactory lockFactory)
throws IOException
{
file = new File(file.getCanonicalPath());
if (file.exists() && !file.isDirectory()) // 如果file存在并且不是一个目录文件,抛出异常
throw new IOException(file + " not a directory");
if (!file.exists())
// 如果file不存在
if (!file.mkdirs()) // 如果创建由file指定的路径名所指定的目录失败,则很可能已经存在,抛出异常
throw new IOException("Cannot create directory: " + file);
FSDirectory dir;
synchronized (DIRECTORIES) {
dir = (FSDirectory)DIRECTORIES.get(file);
if (dir == null) {
try {
dir = (FSDirectory)IMPL.newInstance(); // 调用静态内部类IMPL获取一个与文件系统目录有关的Directory类,并加载该类
} catch (Exception e) {
throw new RuntimeException("cannot load FSDirectory class: " + e.toString(), e); // 加载失败
}
dir.init(file, lockFactory); // 根据指定的file和lockFactory,调用该类Directory的init方法,进行FSDirectory的初始化初始化工作
DIRECTORIES.put(file, dir);
} else {
// 如果该目录dir管理所用的锁工厂实例为空,或者不是同一个锁工厂实例,抛出异常
if (lockFactory != null && lockFactory != dir.getLockFactory()) {
throw new IOException("Directory was previously created with a different LockFactory instance; please pass null as the lockFactory instance and use setLockFactory to change it");
}
}
}
synchronized (dir) {
dir.refCount++;
// 用于记录该目录dir被引用的计数增加1
}
return dir;
}
// 获取FSDirectory实例,该方法已经废弃
public static FSDirectory getDirectory(String path, boolean create)
throws IOException {
return getDirectory(new File(path), create);
}
// 获取FSDirectory实例,该方法已经废弃
public static FSDirectory getDirectory(File file, boolean create)
throws IOException
{
FSDirectory dir = getDirectory(file, null);
// This is now deprecated (creation should only be done
// by IndexWriter):
if (create) {
dir.create();
}
return dir;
}
private void create() throws IOException {
if (directory.exists()) { // File directory是FSDirectory类的一个成员
String[] files = directory.list(IndexFileNameFilter.getFilter());
// 获取经过IndexFileNameFilter过滤后的该目录文件directory下的文件列表
if (files == null)
// 如果获取的文件列表为空
throw new IOException("cannot read directory " + directory.getAbsolutePath() + ": list() returned null");
for (int i = 0; i < files.length; i++) { // 删除返回的文件列表中的文件
File file = new File(directory, files[i]);
if (!file.delete())
throw new IOException("Cannot delete " + file);
// 删除异常
}
}
lockFactory.clearLock(IndexWriter.WRITE_LOCK_NAME); // 删除操作完成后,从锁工厂中清除锁,即释放锁
}
private File directory = null; // File directory是FSDirectory类的一个成员
private int refCount; // 用于记录该目录dir被引用的计数增加1
protected FSDirectory() {}; // permit subclassing
private void init(File path, LockFactory lockFactory) throws IOException {
// 根据指定的file和lockFactory,调用该类Directory的init方法,进行FSDirectory的初始化初始化工作
directory = path;
boolean doClearLockID = false;
if (lockFactory == null) { // 锁工厂实例为null
if (disableLocks) { // 如果锁不可以使用
lockFactory = NoLockFactory.getNoLockFactory(); // 调用NoLockFactory类,获取NoLockFactory实例,为当前的锁工厂实例。其实NoLockFactory是一个单态(singleton)模式的工厂类,应用中只能有一个锁实例,不需要进行同步
} else { // 如果锁可以使用,获取锁工厂类名称的字符串描述
String lockClassName = System.getProperty("org.apache.lucene.store.FSDirectoryLockFactoryClass");
if (lockClassName != null && !lockClassName.equals("")) { // 如果获取的锁工厂类名称的字符串描述不为null,而且者不为空
Class c;
try {
c = Class.forName(lockClassName);
// 加载该锁工厂类
} catch (ClassNotFoundException e) {
throw new IOException("unable to find LockClass " + lockClassName);
}
try {
lockFactory = (LockFactory) c.newInstance(); // 获取一个锁工厂的实例
} catch (IllegalAccessException e) {
throw new IOException("IllegalAccessException when instantiating LockClass " + lockClassName);
} catch (InstantiationException e) {
throw new IOException("InstantiationException when instantiating LockClass " + lockClassName);
} catch (ClassCastException e) {
throw new IOException("unable to cast LockClass " + lockClassName + " instance to a LockFactory");
}
// 根据获取的锁工厂实例的类型来设置对文件File path加锁的方式
if (lockFactory instanceof NativeFSLockFactory) {
((NativeFSLockFactory) lockFactory).setLockDir(path);
} else if (lockFactory instanceof SimpleFSLockFactory) {
((SimpleFSLockFactory) lockFactory).setLockDir(path);
}
} else {
// 没有其他的锁工厂类可用,则使用默认的锁工厂类创建一个锁工厂实例
lockFactory = new SimpleFSLockFactory(path);
doClearLockID = true;
}
}
}
setLockFactory(lockFactory); // 设置当前FSDirectory相关锁工厂实例
if (doClearLockID) {
// Clear the prefix because write.lock will be
// stored in our directory:
lockFactory.setLockPrefix(null);
}
}
// 返回目录文件File directory下的所有索引文件,索引文件使用文件名的字符串描述
public String[] list() {
return directory.list(IndexFileNameFilter.getFilter());
}
// 如果文件name存在,则返回true
public boolean fileExists(String name) {
File file = new File(directory, name); // 根据File directory指定的抽象路径,以及name指定的名称创建一个文件
return file.exists();
}
// 获取文件name最后修改的时间
public long fileModified(String name) {
File file = new File(directory, name);
return file.lastModified();
}
// 返回目录文件directory下名称为name的文件最后修改的时间
public static long fileModified(File directory, String name) {
File file = new File(directory, name);
return file.lastModified();
}
// 设置文件name当前修改的时间
public void touchFile(String name) {
File file = new File(directory, name);
file.setLastModified(System.currentTimeMillis());
}
// 获取文件name的长度
public long fileLength(String name) {
File file = new File(directory, name);
return file.length();
}
// 删除目录文件directory下的name文件
public void deleteFile(String name) throws IOException {
File file = new File(directory, name);
if (!file.delete())
throw new IOException("Cannot delete " + file);
}
// 重新命名文件,该方法已经废弃
public synchronized void renameFile(String from, String to)
throws IOException {
File old = new File(directory, from);
File nu = new File(directory, to);
if (nu.exists())
if (!nu.delete())
throw new IOException("Cannot delete " + nu);
if (!old.renameTo(nu)) {
java.io.InputStream in = null;
java.io.OutputStream out = null;
try {
in = new FileInputStream(old);
out = new FileOutputStream(nu);
if (buffer == null) {
buffer = new byte[1024];
}
int len;
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
old.delete();
}
catch (IOException ioe) {
IOException newExc = new IOException("Cannot rename " + old + " to " + nu);
newExc.initCause(ioe);
throw newExc;
}
finally {
try {
if (in != null) {
try {
in.close();
} catch (IOException e) {
throw new RuntimeException("Cannot close input stream: " + e.toString(), e);
}
}
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
throw new RuntimeException("Cannot close output stream: " + e.toString(), e);
}
}
}
}
}
}
// 创建一个名称为name的文件,返回一个输出流,以便对该文件进行写入操作
public IndexOutput createOutput(String name) throws IOException {
File file = new File(directory, name);
if (file.exists() && !file.delete()) // 如果name指定文件存在,或者没有被删除掉,抛出异常
throw new IOException("Cannot overwrite: " + file);
return new FSIndexOutput(file); // 返回文件File file的一个输出流
}
// 读取名称为name的文件,返回一个输出流,以便定义对读取出来的内容进行操作
public IndexInput openInput(String name) throws IOException {
return new FSIndexInput(new File(directory, name));
}
// 打开指定名称为name的文件,指定大小为缓冲区大小bufferSize,返回一个输入流
public IndexInput openInput(String name, int bufferSize) throws IOException {
return new FSIndexInput(new File(directory, name), bufferSize);
}
// 一个字符缓冲区,将字节转换为十六进制
private static final char[] HEX_DIGITS =
{'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
// 获取锁的标识ID
public String getLockID() {
String dirName;
try {
dirName = directory.getCanonicalPath();
} catch (IOException e) {
throw new RuntimeException(e.toString(), e);
}
byte digest[];
synchronized (DIGESTER) {
digest = DIGESTER.digest(dirName.getBytes());
}
StringBuffer buf = new StringBuffer();
buf.append("lucene-");
for (int i = 0; i < digest.length; i++) {
int b = digest[i];
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]);
}
return buf.toString();
}
// 关闭目录
public synchronized void close() {
if (--refCount <= 0) {
synchronized (DIRECTORIES) {
DIRECTORIES.remove(directory);
}
}
}
public File getFile() {
return directory;
}
/** For debug output. */
public String toString() {
return this.getClass().getName() + "@" + directory;
}
// FSIndexInput是一个静态内部类,用于管理文件系统中的索引文件输入流
protected static class FSIndexInput extends BufferedIndexInput {
private static class Descriptor extends RandomAccessFile {
// 用于记录文件打开状态的boolean型变量isOpen
private boolean isOpen;
long position;
final long length;
public Descriptor(File file, String mode) throws IOException {
super(file, mode);
isOpen=true;
length=length();
}
public void close() throws IOException {
if (isOpen) {
isOpen=false;
super.close();
}
}
protected void finalize() throws Throwable {
try {
close();
} finally {
super.finalize();
}
}
}
private final Descriptor file;
boolean isClone;
public FSIndexInput(File path) throws IOException {
this(path, BufferedIndexInput.BUFFER_SIZE);
}
public FSIndexInput(File path, int bufferSize) throws IOException {
super(bufferSize);
file = new Descriptor(path, "r");
}
// 比较底层的读取操作,主要是对字节操作
protected void readInternal(byte[] b, int offset, int len)
throws IOException {
synchronized (file) {
long position = getFilePointer();
if (position != file.position) {
file.seek(position);
file.position = position;
}
int total = 0;
do {
int i = file.read(b, offset+total, len-total);
if (i == -1)
throw new IOException("read past EOF");
file.position += i;
total += i;
} while (total < len);
}
}
public void close() throws IOException {
if (!isClone) file.close();
}
protected void seekInternal(long position) {
}
public long length() {
return file.length;
}
public Object clone() {
FSIndexInput clone = (FSIndexInput)super.clone();
clone.isClone = true;
return clone;
}
/** Method used for testing. Returns true if the underlying
* file descriptor is valid.
*/
boolean isFDValid() throws IOException {
return file.getFD().valid();
}
}
// FSIndexOutput是一个静态内部类,用于管理文件系统中的索引文件输出流,与FSIndexInput实现类似
protected static class FSIndexOutput extends BufferedIndexOutput {
RandomAccessFile file = null;
// 用于记录文件打开状态的boolean型变量isOpen
private boolean isOpen;
public FSIndexOutput(File path) throws IOException {
file = new RandomAccessFile(path, "rw");
isOpen = true;
}
// 从缓冲区写入文件的方法
public void flushBuffer(byte[] b, int offset, int size) throws IOException {
file.write(b, offset, size);
}
public void close() throws IOException {
if (isOpen) {
super.close();
file.close();
isOpen = false;
}
}
// 随机访问的方法实现
public void seek(long pos) throws IOException {
super.seek(pos);
file.seek(pos);
}
public long length() throws IOException {
return file.length();
}
}
}
上面FSDirectory类的实现挺复杂的,主要是在继承Directory抽象类的基础上,增加了文件系统目录锁特有的一些操作方式。
通过FSDirectory类源代码的阅读,关于文件系统目录FSDirectory类,总结几点:
1、锁工厂的获取及其管理;
2、对文件系统目录下索引文件的输入流和输出流的管理;
3、获取FSDirectory类实例;
4、获取锁工厂实例后,可以创建一个新的FSDirectory类实例,在此之前先要完成一些初始化工作;
5、继承自Directory抽象类,自然可以实现索引文件的的拷贝操作。
6、FSDirectory类中实现了很多静态内部类,这使得只能在FSDirectory类内部访问这些静态类,对外部透明。
RAMDirectory类是与内存目录相关的,它和FSDirectory有很大地不同,这主要从它的构造函数来看:
public RAMDirectory() { setLockFactory(new SingleInstanceLockFactory()); }
初始化的时候,指定的是LockFactory抽象类的一个具体实现类SingleInstanceLockFactory。SingleInstanceLockFactory类的特点是,所有的加锁操作必须通过该SingleInstanceLockFactory的一个实例而发生,也就是说,在进行加锁操作的时候,必须获取到这个SingleInstanceLockFactory的实例。
实际上,在获取到一个SingleInstanceLockFactory的实例后,那么对该目录Directory进行的所有的锁都已经获取到,这些锁都被存放到SingleInstanceLockFactory类定义的locks中。
因此,多个线程要进行加锁操作的时候,需要考虑同步问题。这主要是在获取SingleInstanceLockFactory中的SingleInstanceLock的时候,同步多个线程,包括请求加锁、释放锁,以及与此相关的共享变量。而SingleInstanceLockFactory类的定义也充分体现了这一点:
package org.apache.lucene.store;
import java.io.IOException; import java.util.HashSet; import java.util.Enumeration;
public class SingleInstanceLockFactory extends LockFactory {
private HashSet locks = new HashSet();
public Lock makeLock(String lockName) {
//从锁工厂中, 根据指定的锁lockName返回一个SingleInstanceLock实例 return new SingleInstanceLock(locks, lockName); }
public void clearLock(String lockName) throws IOException { synchronized(locks) { // 从SingleInstanceLockFactory中清除某个锁的时候,需要同步 if (locks.contains(lockName)) { locks.remove(lockName); } } } };
class SingleInstanceLock extends Lock {
String lockName; private HashSet locks;
public SingleInstanceLock(HashSet locks, String lockName) { this.locks = locks; this.lockName = lockName; }
public boolean obtain() throws IOException { synchronized(locks) {
// 获取锁时需要同步 return locks.add(lockName); } }
public void release() { synchronized(locks) { // 释放锁时需要同步 locks.remove(lockName); } }
public boolean isLocked() { synchronized(locks) { return locks.contains(lockName); } }
public String toString() { return "SingleInstanceLock: " + lockName; } }
因为RAMDirectory是与内存相关的目录,所以它不是永久存在的,不像FSDirectory,所以实例化一个RAMDirectory可以从一个FSDirectory的实例来完成。如下:
public RAMDirectory(File dir) throws IOException { this(FSDirectory.getDirectory(dir), true); }
public RAMDirectory(String dir) throws IOException { this(FSDirectory.getDirectory(dir), true); }
RAMDirectory的这两个构造方法,就是根据一个FSDirectory进行初始化的,即在打开一个FSDirectory的时候,同时就有一个RAMDirectory被创建了。
为什么不直接操作FSDirectory呢?可以想到,执行I/O操作速度很慢的,而在内存中的RAMDirectory处理的效率会有很大的提高。
RAMDirectory的特点决定了,对目录Directory进行复杂的操作时,都要把这些操作转移到内存中来处理。通过拷贝目录的方式也可以实例化一个RAMdirectory,如下所示:
public RAMDirectory(Directory dir) throws IOException { this(dir, false); }
private RAMDirectory(Directory dir, boolean closeDir) throws IOException { this(); Directory.copy(dir, this, closeDir); }
将指定的dir目录拷贝到当前的内存中,即实例化一个RAMDirectory。这里,closeDir是一个很重要的状态变量,指定了拷贝完成后,源目录dir是否关闭。如果实例化一个RAMDirectory完成后就关闭源目录dir,可能会因为处理的时间非常短,而需要再次打开源目录dir,持久化到文件系统目录,开销可能会比直接操作源目录dir要大,这点要权衡。
另外,RAMDirectory类定义的成员中,有一个HashMap成员:
HashMap fileMap = new HashMap();
fileMap中存放了从源目录中取得的File,所以在RAMDirectory维护目录中文件的时候,都需要用到fileMap。
而且,管理RAMDirectory的时候,都需要synchronized。
|
关于索引删除的策略IndexDeletionPolicy 。
public IndexWriter(Directory d, Analyzer a, boolean create)
throws CorruptIndexException, LockObtainFailedException, IOException {
init(d, a, create, false, null, true);
}
构造一个IndexWriter需要调用init()方法进行初始化,init()方法的声明如下所示:
/**
* 该方法中的参数列表中。各个参数的含义如下:
* d :指定的存放建立索引文件的索引目录
* a :一个传递进来分析器
* create :是否要重新写入索引文件,如果为true,则重写索引文件;如果为false,则追加写入索引文件
* closeDir :一个boolean型变量,表示是否关闭索引目录Directory d,它是IndexWriter的一个成员变量
* deletionPolicy :指定删除索引文件使用的策略
* autoCommit :建立索引文件后,不对已经存在的索引进行Clone,自动提交。
*/
private void init(Directory d, Analyzer a, final boolean create, boolean closeDir, IndexDeletionPolicy deletionPolicy, boolean autoCommit)
throws CorruptIndexException, LockObtainFailedException, IOException
IndexDeletionPolicy是一个接口,是有关在建立索引的过程中,对索引文件进行灵活地(删除)操作的一种自定义接口。可以在合适的时机进行删除操作,可以指定删除的时刻,完全可以根据自己的需要进行定制,但是,这可能会造成存储开销,但是相对于删除操作策略的灵活便利性,这也是比较值得的。
IndexDeletionPolicy接口的定义非常简单,而且为我们做了启发式的指引:
package org.apache.lucene.index;
import java.util.List;
import java.io.IOException;
// 该接口示例了两种删除索引文件的时机,即所谓的Points
public interface IndexDeletionPolicy {
// 一个IndexWriter索引器初始化的时候,可以删除一些时刻(提交点)提交的索引文件,这些提交点存放在一个List中,包括最早提交点和最近的提点之间的所有提交点,当然,删除他们是可选的,而且应当慎重,因为一旦删除便不可回滚。
public void onInit(List commits) throws IOException;
// 当新建的索引写入目录,并已经提交的时候,我们仍然可以删除指定List中一些时刻(提交点)提交的索引文件,当然,删除他们是可选的,而且应当慎重
public void onCommit(List commits) throws IOException;
}
IndexDeletionPolicy接口的实现类如图所示:
通过对IndexDeletionPolicy接口的实现类进行学习,把握最基本的索引删除的动机和行为。通过IndexDeletionPolicy接口的实现类的类名就可以大概了解到它的索引删除策略:
1、KeepOnlyLastCommitDeletionPolicy策略
KeepOnlyLastCommitDeletionPolicy类在初始化一个IndexWriter的时候,在init方法中如果指定IndexDeletionPolicy deletionPolicy为null,则默认的索引删除策略为KeepOnlyLastCommitDeletionPolicy。
KeepOnlyLastCommitDeletionPolicy类的源代码非常容易理解,如下所示:
package org.apache.lucene.index;
import java.util.List;
public final class KeepOnlyLastCommitDeletionPolicy implements IndexDeletionPolicy {
// 初始化IndexWriter的时候,除了保留最近的一个提交点以外,删除所有提交点提交的索引文件
public void onInit(List commits) {
// Note that commits.size() should normally be 1:
onCommit(commits);
}
// 除了最近时刻的提交点保留以外,其余的全部删除
public void onCommit(List commits) {
// Note that commits.size() should normally be 2 (if not
// called by onInit above):
int size = commits.size();
for(int i=0;i<size-1;i++) { // IndexCommitPoint也很重要,因为它涉及到了索引段,比较复杂,在后面系统学习
((IndexCommitPoint) commits.get(i)).delete();
}
}
}
2、KeepNoneOnInitDeletionPolicy策略
初始化时删除所有提交点的索引段,最后提交的时候,保留最近提交点的索引段。
class KeepNoneOnInitDeletionPolicy implements IndexDeletionPolicy {
int numOnInit;
int numOnCommit;
public void onInit(List commits) {
verifyCommitOrder(commits);
numOnInit++;
// 初始化的时候,就删除所有的提交点
Iterator it = commits.iterator();
while(it.hasNext()) {
((IndexCommitPoint) it.next()).delete();
}
}
public void onCommit(List commits) {
// 验证每个提交点提交的索引文件(索引段)名称的正确性
verifyCommitOrder(commits);
int size = commits.size();
// 除了最近时刻的提交点保留以外,其余的全部删除
for(int i=0;i<size-1;i++) {
((IndexCommitPoint) commits.get(i)).delete();
}
numOnCommit++;
}
}
3、KeepAllDeletionPolicy策略
保留所有提交点,不删除任何提交点的索引段。
class KeepAllDeletionPolicy implements IndexDeletionPolicy {
int numOnInit;
int numOnCommit;
public void onInit(List commits) {
verifyCommitOrder(commits);
numOnInit++;
}
public void onCommit(List commits) {
verifyCommitOrder(commits);
numOnCommit++;
}
}
4、KeepLastNDeletionPolicy策略
初始化时,不删除任何提交点;最后提交时,保留指定的最近的N个提交点。
class KeepLastNDeletionPolicy implements IndexDeletionPolicy {
int numOnInit;
int numOnCommit;
int numToKeep;
int numDelete;
Set seen = new HashSet(); // Set seen用于保证不重复删除某个提交点
public KeepLastNDeletionPolicy(int numToKeep) { // 初始化显式指定最近提交点的个数
this.numToKeep = numToKeep;
}
public void onInit(List commits) {
verifyCommitOrder(commits);
numOnInit++;
// false指定了不做任何删除
doDeletes(commits, false);
}
public void onCommit(List commits) {
verifyCommitOrder(commits);
doDeletes(commits, true);
// 根据初始化的numToKeep,保留最近numToKeep个提交点
}
private void doDeletes(List commits, boolean isCommit) {
if (isCommit) {
String fileName = ((IndexCommitPoint) commits.get(commits.size()-1)).getSegmentsFileName();
if (seen.contains(fileName)) {
throw new RuntimeException("onCommit was called twice on the same commit point: " + fileName);
}
seen.add(fileName); // seen中加入的是已经删除的提交点
numOnCommit++;
}
int size = commits.size();
for(int i=0;i<size-numToKeep;i++) { // 删除前size-numToKeep个提交点
((IndexCommitPoint) commits.get(i)).delete();
numDelete++;
}
}
}
5、ExpirationTimeDeletionPolicy策略
如果某个提交的响应已经超过指定时间,则删除掉这个提交点。
class ExpirationTimeDeletionPolicy implements IndexDeletionPolicy {
Directory dir;
double expirationTimeSeconds; // 指定提交超时时间
int numDelete;
public ExpirationTimeDeletionPolicy(Directory dir, double seconds) {
this.dir = dir;
this.expirationTimeSeconds = seconds;
}
public void onInit(List commits) throws IOException {
verifyCommitOrder(commits);
onCommit(commits);
}
public void onCommit(List commits) throws IOException {
verifyCommitOrder(commits);
IndexCommitPoint lastCommit = (IndexCommitPoint) commits.get(commits.size()-1);
// 根据索引文件的最后提交时间
double expireTime = dir.fileModified(lastCommit.getSegmentsFileName())/1000.0 - expirationTimeSeconds;
Iterator it = commits.iterator();
while(it.hasNext()) {
IndexCommitPoint commit = (IndexCommitPoint) it.next();
double modTime = dir.fileModified(commit.getSegmentsFileName())/1000.0;
if (commit != lastCommit && modTime < expireTime) {
commit.delete();
numDelete += 1;
}
}
}
}
上面五种删除策略,主要地,无非是在索引器初始化的时刻和建立索引提交的时刻,来选择是否删除提交点。
在接触到索引删除的策略IndexDeletionPolicy 的时候,提到一个提交点(IndexCommitPoint)的概念。在合适的时机,根据策略需求,需要对这些提交点(IndexCommitPoint)执行删除操作。
这些个提交点(IndexCommitPoint)究竟具有怎样的特征呢?
IndexCommitPoint是一个索引提交点的接口类,定义非常简单,如下所示:
package org.apache.lucene.index;
public interface IndexCommitPoint {
/**
* 获取与指定的索引提交点相关的索引段文件(这些索引段文件的名称形如segments_N)
* 例如,我们在测试实例化一个IndexWriter索引器的时候,在创建索引的过程中就生成了索引段文件
* 参考文章
Lucene-2.2.0 源代码阅读学习(11)
,可以看到生成的索引段文件为segments_1,大小为1K
*/
public String getSegmentsFileName();
// 删除指定的索引提交点相关的索引段文件
public void delete();
}
实现IndexCommitPoint接口的类为CommitPoint类。CommitPoint类是一个最终类,而且它是作为一个内部类来定义的,那么它的外部类为IndexFileDeleter类。由此可以看出,一些索引提交点(IndexCommitPoint)的存在,是依赖于IndexFileDeleter类的,只有选择了某种索引文件删除策略,才能够构造一个IndexFileDeleter类的实例。倘若初始化了一个IndexFileDeleter类的实例,没有索引删除策略,则这个IndexFileDeleter类的实例根本就没有应用的价值,更不必谈什么索引提交点(IndexCommitPoint)了。
在IndexWriter索引器类中,定义了一个内部成员:
private IndexFileDeleter deleter;
也就是说,一个索引器的实例化必然要初始化一个IndexFileDeleter类的实例,然后在索引器初始化的时候,初始化索引器主要是调用IndexWriter的init方法,而IndexWriter类只定义了两个重载的init方法,他们的声明如下:
private void init(Directory d, Analyzer a, boolean closeDir, IndexDeletionPolicy deletionPolicy, boolean autoCommit)
throws CorruptIndexException, LockObtainFailedException, IOException ;
private void init(Directory d, Analyzer a, final boolean create, boolean closeDir, IndexDeletionPolicy deletionPolicy, boolean autoCommit)
throws CorruptIndexException, LockObtainFailedException, IOException;
这里面,最重要的是第二个init方法,该方法才真正地实现了一些索引器的初始化工作,而第一个init方法只是在通过调用IndexReader类的静态方法:
public static boolean indexExists(Directory directory) throws IOException
来判断指定的索引目录中是否存在索引文件,从而间接地调用第二个init方法来初始化一个IndexWriter索引器。
然后,IndexWriter索引器类不同的构造方法根据构造需要,调用上面的两个init方法实现初始化工作。
在上面的第二个init方法中,根据指定的索引文件删除策略,实例化一个IndexFileDeleter:
deleter = new IndexFileDeleter(directory, deletionPolicy == null ? new KeepOnlyLastCommitDeletionPolicy() : deletionPolicy,segmentInfos, infoStream);
其中infoStream是PrintStream类的一个实例,而PrintStream类继承自FilterOutputStream类,即PrintStream是一个文件输出流类。
这里,如果deletionPolicy=null,即构造一个索引器没有指定删除策略,则自动指派其删除策略为KeepOnlyLastCommitDeletionPolicy,否则使用指定的删除策略deletionPolicy。
一个IndexWriter索引器与IndexFileDeleter索引文件删除工具相关,有必要关注一下IndexFileDeleter类的定义,先把它的一个重要的内部类CommitPoint类放在后面学习:
package org.apache.lucene.index;
import org.apache.lucene.index.IndexFileNames;
import org.apache.lucene.index.SegmentInfos;
import org.apache.lucene.index.SegmentInfo;
import org.apache.lucene.store.Directory;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
// 该类对建立索引过程中指定的Directory目录中的索引文件的删除操作进行管理
// 注意:在IndexFileDeleter实例化之前,必须持有write.lock锁
final class IndexFileDeleter {
// 在删除索引文件过程中可能会由于一些I/O等异常删除失败,将删除失败的文件放到deletable列表中,以期待再次尝试删除它们
private List deletable;
// 存储了与一个索引段文件相关的源数据中的文件的个数,即通过这个索引可以检索到的文件的数目,这里refCount的Key是索引文件的名称,Value就是该索引文件被引用的次数
private Map refCounts = new HashMap();
// 当前索引目录下的索引文件列表
private List commits = new ArrayList();
// 在某个检查点(checkpoint)处可能存在修改了引用计数,但是没有生成提交点,要暂时把这些索引文件存放到lastFiles列表中
private List lastFiles = new ArrayList();
// 提交删除指定索引策略下的索引文件列表
private List commitsToDelete = new ArrayList();
private PrintStream infoStream;
private Directory directory;
private IndexDeletionPolicy policy;
void setInfoStream(PrintStream infoStream) {
this.infoStream = infoStream;
}
private void message(String message) {
infoStream.println(this + " " + Thread.currentThread().getName() + ": " + message);
}
//================IndexFileDeleter()方法开始================
// 初始化一个IndexFileDeleter实例,初始化要做大量工作
public IndexFileDeleter(Directory directory, IndexDeletionPolicy policy, SegmentInfos segmentInfos, PrintStream infoStream)
throws CorruptIndexException, IOException {
this.infoStream = infoStream;
this.policy = policy;
this.directory = directory;
// 第一次遍历索引目录下的索引文件,初始化索引文件索引的文件计数为0
long currentGen = segmentInfos.getGeneration(); // 获取下一次提交时索引段文件segments_N的版本号
// 初始化一个对索引文件进行过滤的IndexFileNameFilter实例
IndexFileNameFilter filter = IndexFileNameFilter.getFilter();
String[] files = directory.list();
if (files == null)
throw new IOException("cannot read directory " + directory + ": list() returned null");
CommitPoint currentCommitPoint = null;
for(int i=0;i<files.length;i++) {
String fileName = files[i];
if (filter.accept(null, fileName) && !fileName.equals(IndexFileNames.SEGMENTS_GEN)) {
// IndexFileNames.SEGMENTS_GEN常量的值为segments.gen,可以在Lucene-2.2.0 源代码阅读学习(11) 看到生成的segments.gen文件
// 如果生成的索引文件合法,则添加到一个初始化索引计数为0的RefCount中
getRefCount(fileName);
if (fileName.startsWith(IndexFileNames.SEGMENTS)) {
// This is a commit (segments or segments_N), and
// it's valid (<= the max gen). Load it, then
// incref all files it refers to:
if (SegmentInfos.generationFromSegmentsFileName(fileName) <= currentGen) {
if (infoStream != null) {
message("init: load commit \"" + fileName + "\"");
}
SegmentInfos sis = new SegmentInfos();
sis.read(directory, fileName);
CommitPoint commitPoint = new CommitPoint(sis);
if (sis.getGeneration() == segmentInfos.getGeneration()) {
currentCommitPoint = commitPoint;
}
commits.add(commitPoint);
incRef(sis, true);
}
}
}
}
if (currentCommitPoint == null) {
throw new CorruptIndexException("failed to locate current segments_N file");
}
// 对索引目录中所有的索引段文件进行排序
Collections.sort(commits);
// 删除引用计数为0的索引文件.
Iterator it = refCounts.keySet().iterator();
while(it.hasNext()) {
String fileName = (String) it.next();
RefCount rc = (RefCount) refCounts.get(fileName);
if (0 == rc.count) {
if (infoStream != null) {
message("init: removing unreferenced file \"" + fileName + "\"");
}
deleteFile(fileName);
}
}
// 在索引器启动的时刻根据指定删除策略删除索引文件
policy.onInit(commits);
// 索引器启动的时刻成功地删除了索引文件,之后还要盘点当前驻留内存中的SegmentInfos,避免它们仍然使用删除的索引文件
if (currentCommitPoint.deleted) {
checkpoint(segmentInfos, false);
}
deleteCommits(); // 提交删除
}
//================IndexFileDeleter()方法结束================
// 根据索引文件删除策略决定删除的提交点,将commitsToDelete列表中的提交点从每个SegmentInfos中删除掉
private void deleteCommits() throws IOException {
int size = commitsToDelete.size();
if (size > 0) {
// First decref all files that had been referred to by
// the now-deleted commits:
for(int i=0;i<size;i++) {
CommitPoint commit = (CommitPoint) commitsToDelete.get(i);
if (infoStream != null) {
message("deleteCommits: now remove commit \"" + commit.getSegmentsFileName() + "\"");
}
int size2 = commit.files.size();
for(int j=0;j<size2;j++) {
decRef((List) commit.files.get(j));
}
decRef(commit.getSegmentsFileName());
}
commitsToDelete.clear();
// Now compact commits to remove deleted ones (保持有序):
size = commits.size();
int readFrom = 0;
int writeTo = 0;
while(readFrom < size) {
CommitPoint commit = (CommitPoint) commits.get(readFrom);
if (!commit.deleted) {
if (writeTo != readFrom) {
commits.set(writeTo, commits.get(readFrom));
}
writeTo++;
}
readFrom++;
}
while(size > writeTo) {
commits.remove(size-1);
size--;
}
}
}
/**
* 用于检查优化的方法
* 因为在复杂的操作过程中,可能发生异常,索引目录中可能存在不被引用的索引文件,
* 应该删除这些无用的索引文件,释放磁盘空间
*/
public void refresh() throws IOException {
String[] files = directory.list();
if (files == null)
throw new IOException("cannot read directory " + directory + ": list() returned null");
IndexFileNameFilter filter = IndexFileNameFilter.getFilter();
for(int i=0;i<files.length;i++) {
String fileName = files[i];
if (filter.accept(null, fileName) && !refCounts.containsKey(fileName) && !fileName.equals(IndexFileNames.SEGMENTS_GEN)) {
// 经过过滤、检查,找出残留的无用索引文件,删除他们
if (infoStream != null) {
message("refresh: removing newly created unreferenced file \"" + fileName + "\"");
}
deleteFile(fileName);
}
}
}
/**
* For definition of "check point" see IndexWriter comments:
* removed, we decref their files as well.
*/
public void checkpoint(SegmentInfos segmentInfos, boolean isCommit) throws IOException {
if (infoStream != null) {
message("now checkpoint \"" + segmentInfos.getCurrentSegmentFileName() + "\" [isCommit = " + isCommit + "]");
}
// Try again now to delete any previously un-deletable
// files (because they were in use, on Windows):
if (deletable != null) {
List oldDeletable = deletable;
deletable = null;
int size = oldDeletable.size();
for(int i=0;i<size;i++) {
deleteFile((String) oldDeletable.get(i));
}
}
// Incref the files:
incRef(segmentInfos, isCommit);
if (isCommit) {
// Append to our commits list:
commits.add(new CommitPoint(segmentInfos));
// Tell policy so it can remove commits:
policy.onCommit(commits);
// Decref files for commits that were deleted by the policy:
deleteCommits();
}
// DecRef old files from the last checkpoint, if any:
int size = lastFiles.size();
if (size > 0) {
for(int i=0;i<size;i++) {
decRef((List) lastFiles.get(i));
}
lastFiles.clear();
}
if (!isCommit) {
// Save files so we can decr on next checkpoint/commit:
size = segmentInfos.size();
for(int i=0;i<size;i++) {
SegmentInfo segmentInfo = segmentInfos.info(i);
if (segmentInfo.dir == directory) {
lastFiles.add(segmentInfo.files());
}
}
}
}
void incRef(SegmentInfos segmentInfos, boolean isCommit) throws IOException {
int size = segmentInfos.size();
for(int i=0;i<size;i++) {
SegmentInfo segmentInfo = segmentInfos.info(i);
if (segmentInfo.dir == directory) {
incRef(segmentInfo.files());
}
}
if (isCommit) {
// Since this is a commit point, also incref its
// segments_N file:
getRefCount(segmentInfos.getCurrentSegmentFileName()).IncRef();
}
}
// 对列表files中的索引文件,进行批量引用计数加1操作
private void incRef(List files) throws IOException {
int size = files.size();
for(int i=0;i<size;i++) {
String fileName = (String) files.get(i);
RefCount rc = getRefCount(fileName);
if (infoStream != null) {
message(" IncRef \"" + fileName + "\": pre-incr count is " + rc.count);
}
rc.IncRef();
}
}
// 对列表files中的索引文件,进行批量引用计数减1操作
private void decRef(List files) throws IOException {
int size = files.size();
for(int i=0;i<size;i++) {
decRef((String) files.get(i));
}
}
// 指定索引文件的引用计数减1
private void decRef(String fileName) throws IOException {
RefCount rc = getRefCount(fileName);
if (infoStream != null) {
message(" DecRef \"" + fileName + "\": pre-decr count is " + rc.count);
}
if (0 == rc.DecRef()) {
// 一个索引文件的引用计数为0了,即该索引文件已变成垃圾索引,要删除该索引文件
deleteFile(fileName);
refCounts.remove(fileName);
}
}
void decRef(SegmentInfos segmentInfos) throws IOException {
final int size = segmentInfos.size();
for(int i=0;i<size;i++) {
SegmentInfo segmentInfo = segmentInfos.info(i);
if (segmentInfo.dir == directory) {
decRef(segmentInfo.files());
}
}
}
// 根据指定的索引文件的名称,获取用于管理该索引文件的引用计数RefCount实例
private RefCount getRefCount(String fileName) {
RefCount rc;
if (!refCounts.containsKey(fileName)) {
rc = new RefCount();
refCounts.put(fileName, rc);
} else {
rc = (RefCount) refCounts.get(fileName);
}
return rc;
}
// 从Directory directory目录中删除指定索引文件fileName
private void deleteFile(String fileName)
throws IOException {
try {
if (infoStream != null) { // 如果输出流保持打开状态
message("delete \"" + fileName + "\"");
}
directory.deleteFile(fileName);
} catch (IOException e) {
// 如果删除失败
if (directory.fileExists(fileName)) {
// 删除失败索引文件还残留于索引目录中,并且,如果输出流关闭,则提示稍后删除
if (infoStream != null) {
message("IndexFileDeleter: unable to remove file \"" + fileName + "\": " + e.toString() + "; Will re-try later.");
}
if (deletable == null) {
// 将删除失败的索引文件添加到列表deletable中
deletable = new ArrayList();
}
deletable.add(fileName);
}
}
}
/**
* Blindly delete the files used by the specific segments,
* with no reference counting and no retry. This is only
* currently used by writer to delete its RAM segments
* from a RAMDirectory.
*/
public void deleteDirect(Directory otherDir, List segments) throws IOException {
int size = segments.size();
for(int i=0;i<size;i++) {
List filestoDelete = ((SegmentInfo) segments.get(i)).files();
int size2 = filestoDelete.size();
for(int j=0;j<size2;j++) {
otherDir.deleteFile((String) filestoDelete.get(j));
}
}
}
// RefCount类是用于管理一个索引文件的引用计数的,当然,一个索引文件可能没有被引用过,这时引用计数this.count=0,应该删除掉这个没有意义的索引文件
final private static class RefCount {
int count;
final private int IncRef() {
// 计数加1
return ++count;
}
final private int DecRef() { // 计数减1
return --count;
}
}
}
将静态内部类CommitPoint(是IndexCommitPoint接口的一个具体实现类)单独拿出来看:
/**
* 保存每个提交点的详细信息,为了更好地在应用删除策略时进行应用提供方便。
* 该类实现了Comparable接口;该类的实例,即提交点,在放到一个List中的时候,不能有重复的
*/
final private class CommitPoint implements Comparable, IndexCommitPoint {
long gen; // 下次提交索引段segments_N的版本
List files; // 属于当前索引目录的索引段的一个列表
String segmentsFileName; // 一个索引段
boolean deleted; // 删除标志
public CommitPoint(SegmentInfos segmentInfos) throws IOException {
segmentsFileName = segmentInfos.getCurrentSegmentFileName();
int size = segmentInfos.size();
// segmentInfos是一个索引段SegmentInfo的向量
files = new ArrayList(size);
gen = segmentInfos.getGeneration(); // 获取下次提交索引段segments_N的版本号
for(int i=0;i<size;i++) {
SegmentInfo segmentInfo = segmentInfos.info(i);
// 从segmentInfos向量列表中取出一个segmentInfo
if (segmentInfo.dir == directory) {
files.add(segmentInfo.files()); // 如果该索引段segmentInfo属于该索引目录,则加入到列表files中
}
}
}
/**
* 获取与该提交点相关的segments_N索引段
*/
public String getSegmentsFileName() {
return segmentsFileName;
}
/**
* 删除一个提交点
*/
public void delete() {
if (!deleted) {
deleted = true;
commitsToDelete.add(this);
}
}
public int compareTo(Object obj) {
CommitPoint commit = (CommitPoint) obj;
if (gen < commit.gen) {
return -1;
} else if (gen > commit.gen) {
return 1;
} else {
return 0;
}
}
}
根据
Lucene-2.2.0 源代码阅读学习(16)
中对IndexFileDeleter类和CommitPoint类的源代码的阅读学习,在此进行总结:
一个提交点所具有的信息如下所示:
long gen; // 下次提交索引段segments_N的版本
List files; // 属于当前索引目录的索引段的一个列表
String segmentsFileName; // 一个索引段
boolean deleted; // 删除标志
一个提交点具有的行为:
1、通过getSegmentsFileName()方法,得到一个索引段文件的名称;
2、通过delete()方法,获取到具有deleted标志(当delete为false时,即还没有被删除)的提交点,加入到commitsToDelete列表中,真正删除是在CommitPoint类的外部类IndexFileDeleter类中的deleteCommits()方法中;
3、该类的compareTo()实现了自然排序的功能,排序是根据gen = segmentInfos.getGeneration();返回的整数值进行实现的。也就是说,如果把一个个的CommitPoint加入到列表中的时候,它是有序的,可以很方便地获取最早的提交点和最近提交点。
在IndexFileDeleter类和CommitPoint类中,都涉及到了关于索引段Segment的内容,研究一下SegmentInfos类和SegmentInfo类。
先看一下SegmentInfo类的结构,然后再学习代码:
SegmentInfos类实现的源代码:
package org.apache.lucene.index;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Vector;
final class SegmentInfos extends Vector {
//
public static final int FORMAT_LOCKLESS = -2;
//
public static final int FORMAT_SINGLE_NORM_FILE = -3;
// 用于指向最近的文件的格式(因为Lucene2.1以后对索引文件的格式进行了优化的改变),可以参考官方文档
http://lucene.apache.org/java/2_2_0/fileformats.html#Segments%20File
private static final int CURRENT_FORMAT = FORMAT_SINGLE_NORM_FILE;
public int counter = 0;
// 用于命名当前最新的索引段文件
/**
* 统计索引文件变化的频率(如添加索引、删除索引会使索引文件的格式发生变化)
* 根据当前的时间(精确到毫秒)创建一个唯一的版本号数字串.
*/
private long version = System.currentTimeMillis();
private long generation = 0; // 下次提交时"segments_N"的N=generation
private long lastGeneration = 0; // 最后一次成功读取或者写入,"segments_N"中N=lastGeneration
/**
* 如果索引文件不是null的,则构造一个输出流,输出segments_N文件
*/
private static PrintStream infoStream;
public final SegmentInfo info(int i) {
return (SegmentInfo) elementAt(i);
}
/**
* 从指定的文件列表files中获取当前segments_N文件的版本号(generation)
*/
public static long getCurrentSegmentGeneration(String[] files) {
if (files == null) {
// 如果指定的索引目录中没有索引文件,返回-1
return -1;
}
long max = -1; // 不存在任何索引文件,当默认当前版本号为-1
for (int i = 0; i < files.length; i++) { // 对索引目录中所有索引文件遍历,取出segments_N中最大的N的作为当前版本号
String file = files[i];
// IndexFileNames.SEGMENTS="segments",segments是生成的索引文件,在IndexFileNames类中定义了所有的索引文件名
// IndexFileNames.SEGMENTS_GEN="segments.gen"
if (file.startsWith(IndexFileNames.SEGMENTS) && !file.equals(IndexFileNames.SEGMENTS_GEN)) {
long gen = generationFromSegmentsFileName(file);
// 调用后面的方法,获取索引文件的版本号(generation)
if (gen > max) {
max = gen;
}
}
}
return max; // 将segments_N中最大的N返回,作为当前版本号(generation)
}
/**
* 重载的方法,从指定的索引目录中获取当前segments_N文件的版本号(generation)
*/
public static long getCurrentSegmentGeneration(Directory directory) throws IOException {
String[] files = directory.list();
if (files == null)
throw new IOException("cannot read directory " + directory + ": list() returned null");
return getCurrentSegmentGeneration(files); //调用getCurrentSegmentGeneration()方法,从索引目录中读取的文件列表files中获取当前segments_N文件的版本号(generation)
}
/**
* 指定索引文件列表,获取当前segments_N文件的名称
*/
public static String getCurrentSegmentFileName(String[] files) throws IOException {
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS, "",getCurrentSegmentGeneration(files)); // 调用了IndexFileNames类的fileNameFromGeneration()方法,在后面有讲解
}
/**
* 重载的方法,指定索引目录,获取当前segments_N文件的名称
*/
public static String getCurrentSegmentFileName(Directory directory) throws IOException {
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",
getCurrentSegmentGeneration(directory));
}
/**
* 重载的方法,根据索引文件的信息,即最后成功读取或写入时的版本号lastGeneration,获取当前segments_N文件的名称
*/
public String getCurrentSegmentFileName() {
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",lastGeneration);
}
/**
* 从索引文件名称的字符串中解析索引文件的版本号,即segments_N中的N,并且最后返回N的值
*/
public static long generationFromSegmentsFileName(String fileName) {
if (fileName.equals(IndexFileNames.SEGMENTS)) {
// 如果文件名称为segments,没有扩展名,则返回0
return 0;
} else if (fileName.startsWith(IndexFileNames.SEGMENTS)) {
return Long.parseLong(fileName.substring(1+IndexFileNames.SEGMENTS.length()),Character.MAX_RADIX); // 取segments_N中的子串N,并将N转换为Long型
} else { // 解析失败,抛出异常
throw new IllegalArgumentException("fileName \"" + fileName + "\" is not a segments file");
}
}
/**
* 获取下一个将被写入索引目录的segments_N文件
*/
public String getNextSegmentFileName() {
long nextGeneration;
if (generation == -1) { // 如果当前索引目录中没有任何索引文件,则最新写入的索引文件的版本号为1,即segments_1
nextGeneration = 1;
} else {
nextGeneration = generation+1; // 否则,当前的版本号+1为将要写入的索引文件的版本号
}
// 返回将要写入索引目录的索引文件的名称,即文件名segments_N,N用nextGeneration替换
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",nextGeneration);
}
/**
* 读取指定的索引文件
*/
public final void read(Directory directory, String segmentFileName) throws CorruptIndexException, IOException {
boolean success = false;
IndexInput input = directory.openInput(segmentFileName); // 为索引文件segmentFileName创建一个输入流
generation = generationFromSegmentsFileName(segmentFileName); // 下次要提交的索引文件的版本号
lastGeneration = generation; // 最后成功读取或写入索引文件的版本号
try {
int format = input.readInt(); // 读取4个字节,返回一个Int型整数,索引文件中具有版本号的记录
if(format < 0){
// 如果文件包含了外部的版本号
// 要解析成内部能够使用的信息
if (format < CURRENT_FORMAT) // 如果读取到的Int整数小于当前从索引文件中获取的版本号,则是错误的
throw new CorruptIndexException("Unknown format version: " + format);
version = input.readLong(); // 读取版本号Long串
counter = input.readInt();
// 读取用于命名当前的索引文件的gen值
}
else{ // 索引文件没有外部格式信息,就去当前从索引文件中读取到的整数值为当前的索引文件命名
counter = format;
}
for (int i = input.readInt(); i > 0; i--) { // 读取索引段信息
addElement(new SegmentInfo(directory, format, input)); // 构造一个用于管理索引文件的SegmentInfo对象,添加到SegmentInfos向量列表中去
}
if(format >= 0){ // 对于旧格式的索引文件,版本号信息可能在文件的末尾
if (input.getFilePointer() >= input.length())
version = System.currentTimeMillis(); // 如果旧文件格式没有版本号信息,则设置当前版本号
else
version = input.readLong(); // 否则,如果不是旧格式索引文件,直接从索引文件中读取版本号
}
success = true;
// 获取到索引文件的版本号,则标志位success置true,表示可以生成当前版本的索引文件(名称)
}
finally {
input.close();
if (!success) {
clear();
}
}
}
/**
* 如果读取索引文件失败,重新尝试再次去读取
*/
public final void read(Directory directory) throws CorruptIndexException, IOException {
generation = lastGeneration = -1;
new FindSegmentsFile(directory) { // FindSegmentsFile是一个静态抽象内部类,在此实现从索引目录中加载索引文件
protected Object doBody(String segmentFileName) throws CorruptIndexException, IOException {
read(directory, segmentFileName); // 初始化一个FindSegmentsFile的实例时,调用上面实现的读取索引文件的read方法
return null;
}
}.run();
// 调用继承自抽象类FindSegmentsFile的run方法进行读取,(run方法的实现比较复杂)
}
/**
* 执行写入当前的索引文件操作
*/
public final void write(Directory directory) throws IOException {
String segmentFileName = getNextSegmentFileName();
// Always advance the generation on write:
if (generation == -1) {
generation = 1;
} else {
generation++;
}
IndexOutput output = directory.createOutput(segmentFileName); // 构造一个索引文件输出流
boolean success = false;
try {
output.writeInt(CURRENT_FORMAT); // 写入FORMAT
output.writeLong(++version); // 写入版本号
output.writeInt(counter); // 写入当前的索引文件的外部信息(即segment_N中的N的值)
output.writeInt(size());
// 写入该SegmentInfos中的每个SegmentInfo的信息
for (int i = 0; i < size(); i++) {
info(i).write(output);
}
}
finally {
try {
output.close(); // 关闭索引文件输出流,成功写入索引目录
success = true;
} finally {
if (!success) { // 如果写入失败,执行回滚操作,删除非法的写入失败的索引文件
directory.deleteFile(segmentFileName);
}
}
}
try {
output = directory.createOutput(IndexFileNames.SEGMENTS_GEN);
// 创建segment.gen文件,打开一个输出文件流
try { // 写入维护所需要的信息
output.writeInt(FORMAT_LOCKLESS);
output.writeLong(generation);
output.writeLong(generation);
} finally {
output.close();
}
} catch (IOException e) {
// It's OK if we fail to write this file since it's
// used only as one of the retry fallbacks.
}
lastGeneration = generation;
}
/**
* 克隆一个SegmentInfos
*/
public Object clone() {
SegmentInfos sis = (SegmentInfos) super.clone();
for(int i=0;i<sis.size();i++) {
sis.setElementAt(((SegmentInfo) sis.elementAt(i)).clone(), i);
}
return sis;
}
/**
* SegmentInfos生成的版本号
*/
public long getVersion() {
return version;
}
public long getGeneration() {
return generation;
}
/**
* 从segments文件中读取当前的版本号.
*/
public static long readCurrentVersion(Directory directory)
throws CorruptIndexException, IOException {
return ((Long) new FindSegmentsFile(directory) {
protected Object doBody(String segmentFileName) throws CorruptIndexException, IOException {
IndexInput input = directory.openInput(segmentFileName);
int format = 0;
long version = 0;
try {
format = input.readInt();
if(format < 0){
if (format < CURRENT_FORMAT)
throw new CorruptIndexException("Unknown format version: " + format);
version = input.readLong(); // read version
}
}
finally {
input.close();
}
if(format < 0)
return new Long(version);
// We cannot be sure about the format of the file.
// Therefore we have to read the whole file and cannot simply seek to the version entry.
SegmentInfos sis = new SegmentInfos();
sis.read(directory, segmentFileName);
return new Long(sis.getVersion());
}
}.run()).longValue();
}
/**
* segments 文件输出流
*/
public static void setInfoStream(PrintStream infoStream) {
SegmentInfos.infoStream = infoStream;
}
/* Advanced configuration of retry logic in loading
segments_N file */
private static int defaultGenFileRetryCount = 10;
private static int defaultGenFileRetryPauseMsec = 50;
private static int defaultGenLookaheadCount = 10;
/**
* Advanced: set how many times to try loading the
* segments.gen file contents to determine current segment
* generation. This file is only referenced when the
* primary method (listing the directory) fails.
*/
public static void setDefaultGenFileRetryCount(int count) {
defaultGenFileRetryCount = count;
}
public static int getDefaultGenFileRetryCount() {
return defaultGenFileRetryCount;
}
/**
* Advanced: set how many milliseconds to pause in between
* attempts to load the segments.gen file.
*/
public static void setDefaultGenFileRetryPauseMsec(int msec) {
defaultGenFileRetryPauseMsec = msec;
}
public static int getDefaultGenFileRetryPauseMsec() {
return defaultGenFileRetryPauseMsec;
}
/**
* Advanced: set how many times to try incrementing the
* gen when loading the segments file. This only runs if
* the primary (listing directory) and secondary (opening
* segments.gen file) methods fail to find the segments
* file.
*/
public static void setDefaultGenLookaheadCount(int count) {
defaultGenLookaheadCount = count;
}
public static int getDefaultGenLookahedCount() {
return defaultGenLookaheadCount;
}
public static PrintStream getInfoStream() {
return infoStream;
}
private static void message(String message) {
if (infoStream != null) {
infoStream.println(Thread.currentThread().getName() + ": " + message);
}
}
////********这里是FindSegmentsFile抽象静态内部类的定义,可以参考Lucene实现源代码********////
}
从SegmentInfos类的实现过程可以看出,该类主要是对SegmentInfo进行管理的。在每次执行打开索引目录、打开索引文件、写入文件等等,都需要对SegmentInfos进行维护。
因为SegmentInfos记录了对索引文件进行操作(如:建立索引、删除索引)而生成的一些索引文件格式、版本号的信息,所以每当索引文件有操作需求,都要从SegmentInfos中获取当前的一些详细记录,SegmentInfos是操作索引文件的依据,同时操作索引文件结束后,要及时更新SegmentInfos的记录信息,为下次操作索引文件提供准确的信息。
SegmentInfos类主要通过两个文件来维护这些信息:segment_N和segment.gen文件。
segment_N文件存储的是当前正处于激活状态的索引文件的信息,也就是当前操作的索引文件的维护信息。
segment.gen文件是专门用于管理segment_N文件的。这里,segment_N文件是动态变化的,比如每次写入新的索引文件或者删除索引文件都涉及到当前索引文件的版本问题。segment.gen主要管理的的操作索引文件的版本信息的。
在处理提交点的时候,也要参考索引文件的版本,都需要从segment.gen中读取;根据实际的操作,还要在操作结束的时候更新segment.gen文件,保证下次操作的正确性。
关于SegmentInfos类的具体实现大致已经在文章
Lucene-2.2.0 源代码阅读学习(17)
中有了一个简单的印象,可以在文章
Lucene-2.2.0 源代码阅读学习(17)
中的末尾部分看到一点非常有用的总结。
然而,到底SegmentInfos类能够实现哪些功能,让我们能够亲眼看到它产生了哪些东西呢?我们可以从SegmentInfos类的一些重要的成员方法中开始追踪一些真实存在的东西到底去向如何,比如segmentName,以及version和gen等等,他们都是有值的,那么,这些值应该被怎样地输出呢,又输出到哪里去了呢,下面仔细学习研究。
先做个引子:
在前面的文章
Lucene-2.2.0 源代码阅读学习(4)
中,我们做了一个小例子,对指定目录中的一些txt文本进行索引,然后做了一个简单的检索关键字的测试。
就从这里说起,在文章
Lucene-2.2.0 源代码阅读学习(4)
中,没有学习到内在的机制,而只是为学习分词做了一个引导。现在,也不对如果构建Document和Field进行解释,因为这一块也非常地复杂,够学习一阵子了。但是,我们要看的就是,在这个过程中都有哪些产物,即产生了哪些文件,知道了建立索引过程中产生了哪些文件,有助于我们在SegmentInfos类中对一些与维护索引文件相关的信息的去向进行追踪。
在文章
Lucene-2.2.0 源代码阅读学习(4)
中,运行测试程序后,在指定的索引文件目录(测试程序中指定为E:\Lucene\myindex)生成了很多文件(因为指定的要建立索引的txt文件具有一定的量,如果只有一两个txt文本文件,而且它们的大小都不是很大,则产生的索引文件数量会很少,可能只有3个或者4个),如图所示:
图中segments.gen和segments_f文件都不陌生了。看看他们是怎么生成的,又保存有哪些信息。
segments_N文件和segments.gen文件的生成
1、先看segment_N文件:
在将文件写入到磁盘的目录中之前,一般来说首先要创建一个输出流。在SegmentInfos类中,有一个成员方法write(),在该方法中:
IndexOutput output = directory.createOutput(segmentFileName);
根据指定的索引段文件的名称segmentFileName,创建一个指向索引目录directory的输出流output。
关于这个segmentFileName,是要先从指定的索引目录中读取出来的,在write()方法中第一行代码中就获取了这个索引段的文件名:
String segmentFileName = getNextSegmentFileName();
这里的 getNextSegmentFileName()方法是SegmentInfos类的一个成员方法,了解它有助于我们继续追踪:
public String getNextSegmentFileName() {
long nextGeneration;
if (generation == -1) {
nextGeneration = 1;
} else {
nextGeneration = generation+1;
}
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",nextGeneration);
}
该方法返回的就是我们将要处理的一个索引段文件的名称。最后一句return返回,调用了IndexFileNames类的fileNameFromGeneration()方法,它也很重要,因为要使用文件名称作为参数获取索引目录下的维护索引的文件都要从这里获得。
关注一下IndexFileNames类的实现:
package org.apache.lucene.index;
// 该类主要是对索引文件的命名进行管理
final class IndexFileNames {
/** 索引段文件名 */
static final String SEGMENTS = "segments";
/** generation reference文件名*/
static final String SEGMENTS_GEN = "segments.gen";
/** Name of the index deletable file (only used in pre-lockless indices) */
static final String DELETABLE = "deletable";
/** norms file的扩展名 */
static final String NORMS_EXTENSION = "nrm";
/** 复合文件扩展名*/
static final String COMPOUND_FILE_EXTENSION = "cfs";
/** 删除文件扩展名 */
static final String DELETES_EXTENSION = "del";
/** plain norms扩展名 */
static final String PLAIN_NORMS_EXTENSION = "f";
/** Extension of separate norms */
static final String SEPARATE_NORMS_EXTENSION = "s";
/**
* Lucene的全部索引文件扩展名列表
*/
static final String INDEX_EXTENSIONS[] = new String[] {
"cfs", "fnm", "fdx", "fdt", "tii", "tis", "frq", "prx", "del",
"tvx", "tvd", "tvf", "gen", "nrm"
};
/** 被添加到复合索引文件上的文件扩展名 */
static final String[] INDEX_EXTENSIONS_IN_COMPOUND_FILE = new String[] {
"fnm", "fdx", "fdt", "tii", "tis", "frq", "prx",
"tvx", "tvd", "tvf", "nrm"
};
/** old-style索引文件扩展名 */
static final String COMPOUND_EXTENSIONS[] = new String[] {
"fnm", "frq", "prx", "fdx", "fdt", "tii", "tis"
};
/** 词条向量支持的文件扩展名 */
static final String VECTOR_EXTENSIONS[] = new String[] {
"tvx", "tvd", "tvf"
};
/**
* 根据基础文件名(不包括后缀,比如segments.gen文件,segments部分为基础文件名)、扩展名和generarion计算指定文件的完整文件名
*/
static final String fileNameFromGeneration(String base, String extension, long gen) {
if (gen == SegmentInfo.NO) {
return null;
} else if (gen == SegmentInfo.WITHOUT_GEN) {
return base + extension;
} else {
return base + "_" + Long.toString(gen, Character.MAX_RADIX) + extension;
}
}
}
fileNameFromGeneration实现的功能:根据传进来的base(比如segments)、扩展名、gen来生成一个新的文件名,并返回。
在SegmentInfos类中getNextSegmentFileName() 方法调用了fileNameFromGeneration,如下所示:
return IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS,"",nextGeneration);
第一个参数值为"segments",第二个参数值为"",第三个是一个gen(它是一个Long型的数字),如果假设这里的nextGeneration=5,调用fileNameFromGeneration()方法后,返回的是一个索引段文件名:segments_5。
这样,就可以根据生成的segments_N文件名,创建一个输出流,将需要的信息写入到该文件中。
2、再看segments.gen文件:
仔细观察,其实SegmentInfos类的write方法就是对segments_N文件和segments.gen文件进行写入操作的。
在写入segments_N文件以后,紧接着就是处理segments.gen文件:
output = directory.createOutput(IndexFileNames.SEGMENTS_GEN);
因为在一个索引目录下,属于同一个索引段的索引文件就是通过一个segments.gen文件来维护的,segments.gen文件的文件名自然不需要那么麻烦地去获取。直接使用IndexFileNames.SEGMENTS_GEN = segments.gen作为参数构造一个输出流,进行输出,写入到索引目录中即可。
关于segments_N文件和segments.gen文件保存的信息
同样在SegmentInfos类的write方法中能够看到,这两个文件中都加入了哪些信息。
■ 关于segments_N文件
如下所示:
output.writeInt(CURRENT_FORMAT); // write FORMAT
output.writeLong(++version); // every write changes the index
output.writeInt(counter); // write counter
output.writeInt(size());
// write infos
for (int i = 0; i < size(); i++) {
info(i).write(output);
}
(1) CURRENT_FORMAT
其中,CURRENT_FORMAT是SegmentInfos类的一个成员:
/* This must always point to the most recent file format. */
private static final int CURRENT_FORMAT = FORMAT_SINGLE_NORM_FILE;
上面CURRENT_FORMAT的值就是FORMAT_SINGLE_NORM_FILE的值-3:
/** This format adds a "hasSingleNormFile" flag into each segment info.
* See <a href="
http://issues.apache.org/jira/browse/LUCENE-756">LUCENE-756</a
>
for details.
*/
public static final int FORMAT_SINGLE_NORM_FILE = -3;
(2) version
version是SegmentInfos类的一个成员,版本号通过系统来获取:
/**
* counts how often the index has been changed by adding or deleting docs.
* starting with the current time in milliseconds forces to create unique version numbers.
*/
private long version = System.currentTimeMillis();
(3) counter
用于为当前待写入索引目录的索引段文件命名的,即segments_N中的N将使用counter替换。
counter也是SegmentInfos类的一个成员,初始化是为0:
public int counter = 0;
在read()方法中,使用从索引目录中已经存在的segment_N中读取的出format的值,然后根据format的值来指派counter的值,如下所示:
int format = input.readInt();
if(format < 0){ // file contains explicit format info
// check that it is a format we can understand
if (format < CURRENT_FORMAT)
throw new CorruptIndexException("Unknown format version: " + format);
version = input.readLong(); // read version
counter = input.readInt();
// read counter
}
else{ // file is in old format without explicit format info
counter = format;
}
(4) size()
size()就是SegmentInfos的大小,SegmentInfos中含有多个SegmentInfo,注意:SegmentInfos类继承自Vector。
(5) info(i)
info()方法的定义如下所示:
public final SegmentInfo info(int i) {
return (SegmentInfo) elementAt(i);
}
可见,SegmentInfos是SegmentInfo的一个容器,它只把当前这个索引目录中的SegmentInfo装进去,以便对他们管理维护。
这里,info(i).write(output);又调用了SegmentInfo类的write()方法,来向索引输出流output中加入信息。SegmentInfo类的write()方法如下所示:
/**
* Save this segment's info.
*/
void write(IndexOutput output)
throws IOException {
output.writeString(name);
output.writeInt(docCount);
output.writeLong(delGen);
output.writeByte((byte) (hasSingleNormFile ? 1:0));
if (normGen == null) {
output.writeInt(NO);
} else {
output.writeInt(normGen.length);
for(int j = 0; j < normGen.length; j++) {
output.writeLong(normGen[j]);
}
}
output.writeByte(isCompoundFile);
}
从上可以看到,还写入了SegmentInfo的具体信息:name、docCount、delGen、(byte)(hasSingleNormFile ? 1:0)、NO/normGen.length、normGen[j]、isCompoundFile。
■ 关于segments.gen文件
通过SegmentInfos类的write()方法可以看到:
output.writeInt(FORMAT_LOCKLESS);
output.writeLong(generation);
output.writeLong(generation);
segments.gen文件中只是写入了两个字段的信息:FORMAT_LOCKLESS和generation。
因为segments.gen文件管理的就是segments_N文件中的N的值,与该文件相关就只有一个generation,和一个用于判断是否是无锁提交的信息:
/** This format adds details used for lockless commits. It differs
* slightly from the previous format in that file names
* are never re-used (write once). Instead, each file is
* written to the next generation. For example,
* segments_1, segments_2, etc. This allows us to not use
* a commit lock. See <a
* href="
http://lucene.apache.org/java/docs/fileformats.html">file
* formats</a> for details.
*/
public static final int FORMAT_LOCKLESS = -2;
最后,总结一下:
现在知道了segments_N文件和segment.gen文件都记录了什么内容。
其中,segments_N文件与SegmentInfo类的关系十分密切,接下来要学习SegmentInfo类了
研究SegmentInfo类的实现。
虽然在阅读代码的时候,是一件很枯燥的事情,尤其是当代码非常地长,这使得我们感觉这是一种压力,但是如果把代码当成是一种乐趣的时候,你会发现代码是那样的富有感情色彩。呵呵。
SegmentInfo类在Lucene 2.0.0版本的时候,定义非常简单,就定义了一个构造函数,如下所示:
package org.apache.lucene.index;
import org.apache.lucene.store.Directory;
final class SegmentInfo {
public String name;
// unique name in dir
public int docCount;
// number of docs in seg
public Directory dir; // where segment resides
public SegmentInfo(String name, int docCount, Directory dir) {
this.name = name;
this.docCount = docCount;
this.dir = dir;
}
}
但是,到了2.2.0版本的时候,SegmentInfo类的定义就相当复杂了。
因为2.2.0版本是在之前的版本的基础上进行开发的,所以涉及到一些概念,尤其是文件格式,提前了解一下这些概念对于理解SegmentInfo类有很大帮助。
*************************************************************************************
Normalization Factors(标准化因子):可以参考
http://lucene.apache.org/java/2_2_0/fileformats.html#Normalization%20Factors
【在2.1之前的版本,存在下面的一些格式的文件】
在每个Document逻辑文件中,对于一个被索引的Field,都对应着一个norm文件,该norm文件的一个字节与Document有非常密切的关系。
这个.f[0-9]*文件,是为每个Document设置的,当Document中的一个Field被命中的时候,.f[0-9]*文件包含的就是:一个字节编码值乘以排序分值。
一个单独的norm文件的创建,仅仅是在一个复合segments文件被创建的时候才存在。即:一个单独的norm文件的存在是依赖于复合segments的,如果复合segments文件不存在,就不会生成一个单独的norm文件。
【在2.1版本及其之后的版本,存在下面的一些格式的文件】
只使用一个.nrm文件,该文件包含了所有的标准化因子。所有的.nrm文件都包含NormsHeader和<Norms> NumFieldsWithNorms 这两部分。
其中:
NormsHeader包含4个字节,前三个字节为“N”、“R”、“M”,最后一个字节指定了FORMAT版本号,当前因为是2.2.0版本,版本号为-1,可以在SegmentInfos类的定义开始部分看到:
public static final int FORMAT = -1;
<Norms>对应这一个<Byte>,而NumFieldsWithNorms 对应于一个Segments的大小,即SegSize。
一个.nrm文件的每个字节都编码成一个浮点值,该字节的前3bits(即0-2)表示尾数,后5bits(即3-7)表示指数。
当一个已经存在segments文件的标准化因子值(即norm的value)被修改,同时一个单独的norm文件被创建;当Field N被修改,同时一个.sN文件被创建,这个.sN文件用来保存该Field的标准化因子值(即norm的value)。
当适当的时候,一个单独的norm文件的创建,可以是为了一个复合segments文件,也可以不是复合segments文件。
*************************************************************************************
在2.2.0版本中,SegmentInfo类实现的源代码如下所示:
package org.apache.lucene.index;
mport org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.store.IndexInput;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
final class SegmentInfo {
static final int NO = -1; // 如果不存在标准化因子.nrm文件,和.del文件
static final int YES = 1;
// 如果存在标准化因子.nrm文件,和.del文件
static final int CHECK_DIR = 0;
// 需要检查该索引目录,是否存在标准化因子.nrm文件,和.del文件
static final int WITHOUT_GEN = 0; // 一个索引文件的名称中,没有gen值,即:根本没有对其进行过添加(或者是追加、删除)操作
public String name;
// 在索引目录中的唯一索引文件名
public int docCount; // 在索引段中Document的数量
public Directory dir; // 一个索引段文件所属于的索引目录为dir
private boolean preLockless; // 为true,当一个索引段文件在无锁提交之前被写入
private long delGen; // 当前版本(generation)删除时的gen的值为delGen(即segments_delGen.del)
private long[] normGen; // 每个Field对应的norm文件的gen
private byte isCompoundFile; // 如果为NO,则表示不用检查;如果为YES表示检查是否存在2.1版本之前的扩展名为.cfs和.nrm的文件
private boolean hasSingleNormFile;
//如果一个segments存在一个单独的norm文件。在当前版本中,false表示一个segments是由DocumentWriter生成的,true表示为最新创建的合并的索引段文件(包括复合segments文件和非复合segments文件)
private List files; // 在当前索引目录中,当前索引段用到的文件在缓存中的列表
// 构造一个SegmentInfo对象(根据指定的参数:索引段名称、索引文件中Document的数量、指定的所以目录)
public SegmentInfo(String name, int docCount, Directory dir) {
this.name = name;
this.docCount = docCount;
this.dir = dir;
delGen = NO;
// 初始化一个SegmentInfo的时候,指定存在标准化因子.nrm文件,和.del文件
isCompoundFile = CHECK_DIR; // 检查该索引目录dir,看是否存在一些2.1版本之前的扩展名为.cfs和.nrm的文件
preLockless = true; // 为true,当一个索引段文件在无锁提交之前被写入
hasSingleNormFile = false; // 指定一个segments是由DocumentWriter生成的
}
// 构造一个SegmentInfo
对象
public SegmentInfo(String name, int docCount, Directory dir, boolean isCompoundFile, boolean hasSingleNormFile) {
this(name, docCount, dir);
this.isCompoundFile = (byte) (isCompoundFile ? YES : NO);
this.hasSingleNormFile = hasSingleNormFile;
preLockless = false; // 为false,当一个索引段文件不是在无锁提交之前被写入
}
/**
* 根据指定的SegvmentInfo,拷贝它到我们当前的SegmentInfo实例中,其实就是重新构造一个
SegmentInfo,重新构造的该SegmentInfo与指定的SegmentInfo src是相同的
*/
void reset(SegmentInfo src) {
files = null;
name = src.name;
docCount = src.docCount;
dir = src.dir;
preLockless = src.preLockless;
delGen = src.delGen;
if (src.normGen == null) { // 每个Field对应的norm文件的gen不存在(为null)
normGen = null;
} else {
normGen = new long[src.normGen.length];
// arraycopy的声明为:public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
System.arraycopy(src.normGen, 0, normGen, 0, src.normGen.length);
}
isCompoundFile = src.isCompoundFile;
hasSingleNormFile = src.hasSingleNormFile;
}
// 构造一个SegmentInfo对象,通过构造一个索引输入流对象IndexInput
SegmentInfo(Directory dir, int format, IndexInput input) throws IOException {
this.dir = dir;
name = input.readString();
docCount = input.readInt();
if (format <= SegmentInfos.FORMAT_LOCKLESS) {
delGen = input.readLong();
if (format <= SegmentInfos.FORMAT_SINGLE_NORM_FILE) {
hasSingleNormFile = (1 == input.readByte());
} else {
hasSingleNormFile = false;
}
int numNormGen = input.readInt();
if (numNormGen == NO) {
normGen = null;
} else {
normGen = new long[numNormGen];
for(int j=0;j<numNormGen;j++) {
normGen[j] = input.readLong();
}
}
isCompoundFile = input.readByte();
preLockless = (isCompoundFile == CHECK_DIR);
} else {
delGen = CHECK_DIR; // CHECK_DIR = 0
normGen = null;
isCompoundFile = CHECK_DIR;
preLockless = true; // 一个索引段文件在无锁提交之前被写入过
hasSingleNormFile = false; // 一个segments是由DocumentWriter生成的
}
}
// 为每个Field的norm文件设置gen值
void setNumFields(int numFields) {
if (normGen == null) {
// 如果是2.1版本之前的segment文件,或者该segment文件没有对应的norm文件,则normGen=null
normGen = new long[numFields];
if (preLockless) {
// 保持normGen[k]==CHECK_DIR (==0), 之后会为这些norm文件检查文件系统,因为它们是以前版本中无锁提交的
} else {
// 这是一个无锁提交写入的segment, 不存在单独的norm文件
for(int i=0;i<numFields;i++) {
normGen[i] = NO; // 对每个Field,设置norm文件的gen值为NO=-1
}
}
}
}
// 是否存在.del文件
boolean hasDeletions()
throws IOException {
// 如果delGen == NO==-1: 表示使用LOCKLESS模式,其中一些segment不存在.del文件
if (delGen == NO) {
return false;
} else if (delGen >= YES) { // YES的值为1,如果segment文件是无锁提交的,其中一些存在.del文件
return true;
} else {
return dir.fileExists(getDelFileName());
// getDelFileName()在后面定义了该方法,获取删除的该索引目录下的文件名
}
}
void advanceDelGen() {
// delGen 0 is reserved for pre-LOCKLESS format
if (delGen == NO) {
delGen = YES;
} else {
delGen++;
}
files = null;
}
// 设置delGen的值为NO,即-1
void clearDelGen() {
delGen = NO;
files = null;
}
public Object clone () { // SegmentInfo支持克隆
SegmentInfo si = new SegmentInfo(name, docCount, dir);
si.isCompoundFile = isCompoundFile;
si.delGen = delGen;
si.preLockless = preLockless;
si.hasSingleNormFile = hasSingleNormFile;
if (normGen != null) {
si.normGen = (long[]) normGen.clone();
}
return si;
}
String getDelFileName() { 获取删除的该索引目录下的文件名
if (delGen == NO) {
// 如果delGen==NO,则表示不删除
return null;
} else {
// 如果delGen是CHECK_DIR,,则它是以前的无锁提交格式
return IndexFileNames.fileNameFromGeneration(name, "." +IndexFileNames.DELETES_EXTENSION, delGen); // 即返回文件名为name_delGen.del,例如segments_5.del
}
}
/**
* 如果该索引段的这个Field作为separate norms文件(_<segment>_N.sX)进行存储
* fieldNumber是一个需要检查的field的索引
*/
boolean hasSeparateNorms(int fieldNumber)
throws IOException {
if ((normGen == null && preLockless) || (normGen != null && normGen[fieldNumber] == CHECK_DIR)) {
// Must fallback to directory file exists check:
String fileName = name + ".s" + fieldNumber;
return dir.fileExists(fileName);
} else if (normGen == null || normGen[fieldNumber] == NO) {
return false;
} else {
return true;
}
}
/**
* 返回true如果这个segments中的任何一个Field都存在一个单独的norm文件
*/
boolean hasSeparateNorms()
throws IOException {
if (normGen == null) { // 如果normGen = null
if (!preLockless) {
// 不存在norm文件,返回false
return false;
} else {
// segment使用pre-LOCKLESS模式保存,需要回退到最初的目录下,进行核查
String[] result = dir.list();
if (result == null) // 如果获取的文件列表为null
throw new IOException("cannot read directory " + dir + ": list() returned null");
// 否则,如果获取的文件列表不空
String pattern;
pattern = name + ".s";
// 设置文件名的匹配格式字符串,形如name.s的形式
int patternLength = pattern.length();
for(int i = 0; i < result.length; i++){ // 循环匹配
if(result[i].startsWith(pattern) && Character.isDigit(result[i].charAt(patternLength)))
return true;
}
return false;
}
} else { // 如果normGen != null
// 这个segment使用LOCKLESS模式保存
// 需要检查是否任何一个normGen都是 >= 1的
// (它们有一个单独的norm文件):
for(int i=0;i<normGen.length;i++) {
if (normGen[i] >= YES) {
// YES=1
return true;
}
}
// 查找normGen == 0的,这些情况是re
// pre-LOCKLESS模式提交的,需要检查:
for(int i=0;i<normGen.length;i++) {
if (normGen[i] == CHECK_DIR) {
if (hasSeparateNorms(i)) {
return true;
}
}
}
}
return false;
}
/**
* 为每个Field的norm文件的gen,执行加1操作
* @param fieldIndex:指定的Field的norm文件需要被重写,fieldIndex即对应的norm文件的gen值
*/
void advanceNormGen(int fieldIndex) {
if (normGen[fieldIndex] == NO) {
normGen[fieldIndex] = YES;
} else {
normGen[fieldIndex]++;
}
files = null;
}
/**
* 获取Field的norm文件的文件名称;number是一个Field的索引值
*/
String getNormFileName(int number) throws IOException {
String prefix;
long gen;
if (normGen == null) {
gen = CHECK_DIR; // CHECK_DIR==0
} else {
gen = normGen[number]; // 根据Field的索引值获取它对应的norm文件的gen值,然后使用该gen值取得索引段文件的文件名
}
if (hasSeparateNorms(number)) {
// case 1: separate norm
prefix = ".s";
return IndexFileNames.fileNameFromGeneration(name, prefix + number, gen); // 使用gen值取得索引段文件的文件名,如果name=“segments”,number=7,gen=4,则返回的文件名为segments_4.s7
}
if (hasSingleNormFile) { // 如果存在一个单独的norm文件
// case 2: lockless (or nrm file exists) - single file for all norms
prefix = "." + IndexFileNames.NORMS_EXTENSION; // IndexFileNames.NORMS_EXTENSION=nrm
return IndexFileNames.fileNameFromGeneration(name, prefix, WITHOUT_GEN); // 如果name=“segments”,则返回的文件名为segments.nrm
}
// case 3: norm file for each field
prefix = ".f";
return IndexFileNames.fileNameFromGeneration(name, prefix + number, WITHOUT_GEN); // 如果name=“segments”,number=7,则返回的文件名为segments.f7
}
/**
*指定,是否segment文件作为复合文件存储()
*/
void setUseCompoundFile(boolean isCompoundFile) {
if (isCompoundFile) {
this.isCompoundFile = YES;
} else {
this.isCompoundFile = NO;
}
files = null;
}
/**
* 如果索引文件被作为复合文件存储,则返回true
*/
boolean getUseCompoundFile() throws IOException {
if (isCompoundFile == NO) {
return false;
} else if (isCompoundFile == YES) {
return true;
} else {
return dir.fileExists(name + "." + IndexFileNames.COMPOUND_FILE_EXTENSION);
}
}
/**
*保存segment的信息,其实就是输出(写入)到磁盘中的索引目录中
*/
void write(IndexOutput output)
throws IOException {
output.writeString(name);
output.writeInt(docCount);
output.writeLong(delGen);
output.writeByte((byte) (hasSingleNormFile ? 1:0));
if (normGen == null) {
output.writeInt(NO);
} else {
output.writeInt(normGen.length);
for(int j = 0; j < normGen.length; j++) {
output.writeLong(normGen[j]);
}
}
output.writeByte(isCompoundFile);
}
/*
* 返回所有的被当前SegmentInfo引用的所有的文件的列表,由于是在本地缓存中,你不应该设法去修改他们
*/
public List files() throws IOException {
if (files != null) {
return files;
}
files = new ArrayList();
boolean useCompoundFile = getUseCompoundFile();
if (useCompoundFile) {
files.add(name + "." + IndexFileNames.COMPOUND_FILE_EXTENSION);
// IndexFileNames.COMPOUND_FILE_EXTENSION=csf
} else {
/* INDEX_EXTENSIONS_IN_COMPOUND_FILE定义了如下所示的复合文件扩展名:
static final String[] INDEX_EXTENSIONS_IN_COMPOUND_FILE = new String[] {
"fnm", "fdx", "fdt", "tii", "tis", "frq", "prx",
"tvx", "tvd", "tvf", "nrm"
};
*/
for (int i = 0; i < IndexFileNames.INDEX_EXTENSIONS_IN_COMPOUND_FILE.length; i++) {
String ext = IndexFileNames.INDEX_EXTENSIONS_IN_COMPOUND_FILE[i];
String fileName = name + "." + ext;
if (dir.fileExists(fileName)) {
files.add(fileName);
}
}
}
String delFileName = IndexFileNames.fileNameFromGeneration(name, "." + IndexFileNames.DELETES_EXTENSION, delGen); // IndexFileNames.DELETES_EXTENSION=del;如果name=“segments”,delGen=6,则返回的文件名为segments_6.del
if (delFileName != null && (delGen >= YES || dir.fileExists(delFileName))) {
files.add(delFileName);
}
// Careful logic for norms files
if (normGen != null) {
for(int i=0;i<normGen.length;i++) {
long gen = normGen[i];
if (gen >= YES) {
// Definitely a separate norm file, with generation:
files.add(IndexFileNames.fileNameFromGeneration(name, "." + IndexFileNames.SEPARATE_NORMS_EXTENSION + i, gen)); // IndexFileNames.SEPARATE_NORMS_EXTENSION=s;如果name=“segments”,gen=6,i=8,则返回的文件名为segments_6.s8
} else if (NO == gen) {
// No separate norms but maybe plain norms
// in the non compound file case:
if (!hasSingleNormFile && !useCompoundFile) {
String fileName = name + "." + IndexFileNames.PLAIN_NORMS_EXTENSION + i; // IndexFileNames.PLAIN_NORMS_EXTENSION=f;如果name=“segments”,gen=6,i=8,则返回的文件名为segments_6.f8
if (dir.fileExists(fileName)) {
files.add(fileName);
}
}
} else if (CHECK_DIR == gen) {
// 2.1版本之前:我们需要验证这些文件的存在性
String fileName = null;
if (useCompoundFile) {
fileName = name + "." + IndexFileNames.SEPARATE_NORMS_EXTENSION + i; // 若name="segments",i=X=8,则该类文件形如segments.s8
} else if (!hasSingleNormFile) {
fileName = name + "." + IndexFileNames.PLAIN_NORMS_EXTENSION + i; // 若name="segments",i=X=8,则该类文件形如segments.f8
}
if (fileName != null && dir.fileExists(fileName)) {
files.add(fileName);
}
}
}
} else if (preLockless || (!hasSingleNormFile && !useCompoundFile)) {
// 2.1版本之前的: 需要为我们当前的索引段扫描索引目录找到所有的匹配的_X.sN或_X.fN的文件
String prefix;
if (useCompoundFile)
prefix = name + "." + IndexFileNames.SEPARATE_NORMS_EXTENSION; // 若name="segments",则该类文件形如segments.s
else
prefix = name + "." + IndexFileNames.PLAIN_NORMS_EXTENSION;
// 若name="segments",则该类文件形如segments.f
int prefixLength = prefix.length();
String[] allFiles = dir.list();
if (allFiles == null)
throw new IOException("cannot read directory " + dir + ": list() returned null");
for(int i=0;i<allFiles.length;i++) {
String fileName = allFiles[i];
if (fileName.length() > prefixLength && Character.isDigit(fileName.charAt(prefixLength)) && fileName.startsWith(prefix)) {
files.add(fileName);
}
}
}
return files;
}
}
通过SegmentInfo类的定义,总结一下:
1、主要针对2.1版本前后的不同形式的Segments进行处理,尤其是复合segments文件;
2、对每个Field的norm文件进行处理:设置该norm文件的gen;
3、使用write()方法,将一个处理过的Segments文件写入索引目录;
4、2.1版本以后,统一使用一个.nrm文件,该文件包含了所有的标准化因子,因为需要对2.1版本以前的版本进行支持,需要处理2.1版本之前的一些版本中,对标准化因子设置文件进行处理。
5、根据getNormFileName(int number)方法,可以总结出:
(1) 通过一个索引段文件的索引值number,可以得到它的norm文件的gen值,这个gen值其实就是这个segments的gen,即形如segments_N形式,N=gen。
(2) 2.1版本以后的,norm文件只使用一个.nrm扩展名的文件来代替以前的norm文件,则这个文件就是segments.nrm文件,并且对于同一个索引段,只有一个segments.nrm文件。
(3) 2.1版本以前的,norm文件的形式有:segments.fN、segments_N.sX两种。其中N是gen值,X是一个Field在Document中的索引值。
6、通过调用files()方法,可以获取索引目录下存在的不同扩展名的文件的一个列表,即该索引目录下的所有可能存在文件(通过文件扩展名区分),可以分出下面几组:
(1) 如果useCompoundFile=true,获取到扩展名为.cfs的复合文件;
(2) 如果useCompoundFile=false,获取到扩展名为如下的复合文件:
fnm、fdx、fdt、tii、tis、frq、prx、tvx、tvd、tvf、nrm;
(3) 如果是删除文件,获取到扩展名为.del的文件,若name="segments",delGen=6,则文件名形如segments_6.del;
(4) 如果normGen != null且normGen[i] >= YES=1,获取到扩展名为.sX的文件,若name="segments",gen=6,i=X=8,则该类文件形如segments_6.s8;
(5) 如果normGen != null且normGen[i] = NO=-1且hasSingleNormFile=false且useCompoundFile=false,获取到扩展名为.fX的文件,若name="segments",gen=6,i=X=8,则该类文件形如segments_6.f8;
(6) 如果normGen != null且normGen[i] = CHECK_DIR=0且useCompoundFile=true,获取到扩展名为.sX的文件,若name="segments",i=X=8,则该类文件形如segments.s8;
(7) 如果normGen != null且normGen[i] = CHECK_DIR=0且hasSingleNormFile=false且useCompoundFile=false,获取到扩展名为.fX的文件,若name="segments",i=X=8,则该类文件形如segments.f8;
(8) 如果normGen == null的时候,preLockless = true或(||)(hasSingleNormFile=false且useCompoundFile=false),这时若useCompoundFile=true,则获取到扩展名为.s的文件,若name="segments",则该类文件形如segments.s;
(9) 如果normGen == null的时候,preLockless = true或(||)(hasSingleNormFile=false且useCompoundFile=false),这时若useCompoundFile=false,则获取到扩展名为.f的文件,若name="segments",则该类文件形如segments.f;
关于Field类和Document类。
初始化一个IndexWriter索引器之后,就可以向其中添加Document了。然而,Document逻辑文件能够与一个物理文件对应起来,在Lucene中,Document主要是维护添加到其中的多个Field。
关于Field在文章
Lucene-2.2.0 源代码阅读学习(3)
中可以了解到一点相关内容,Field类的定义比较简单,它给出了7种构造一个Field对象的构造方法:
public Field(String name, byte[] value, Store store)
public Field(String name, Reader reader)
public Field(String name, Reader reader, TermVector termVector)
public Field(String name, String value, Store store, Index index)
public Field(String name, String value, Store store, Index index, TermVector termVector)
public Field(String name, TokenStream tokenStream)
public Field(String name, TokenStream tokenStream, TermVector termVector)
Field类的内部定义了三个内部类,其中内部类Store和Index是非常重要的,它指定了一个Field被创建的时候,在索引、存储、分词方面进行选择,可以有不同的选择方式:
// Store是一个内部类,它是static的,主要为了设置Field的存储属性
public static final class Store extends Parameter implements Serializable {
private Store(String name) {
super(name);
}
// 在索引中压缩存储Field的值
public static final Store COMPRESS = new Store("COMPRESS");
// 在索引中存储Field的值
public static final Store YES = new Store("YES");
// 在索引中不存储Field的值
public static final Store NO = new Store("NO");
}
//通过Index设置索引方式
public static final class Index extends Parameter implements Serializable {
private Index(String name) {
super(name);
}
// 不对Field进行索引,所以这个Field就不能被检索到(一般来说,建立索引而使它不被检索,这是没有意义的)
// 如果对该Field还设置了Field.Store为Field.Store.YES或Field.Store.COMPRESS,则可以检索
public static final Index NO = new Index("NO");
// 对Field进行索引,同时还要对其进行分词(由Analyzer来管理如何分词)
public static final Index TOKENIZED = new Index("TOKENIZED");
// 对Field进行索引,但不对其进行分词
public static final Index UN_TOKENIZED = new Index("UN_TOKENIZED");
// 对Field进行索引,但是不使用Analyzer
public static final Index NO_NORMS = new Index("NO_NORMS");
}
第三个内部类是TermVector类,内部类的定义如下:
// 指定一个Field是否要存储一个词条向量,以怎样的方式来存储这个词条向量
public static final class TermVector extends Parameter implements Serializable {
private TermVector(String name) { // 通过指定一个字符串,来构造一个Field的TermVector,指定该Field的对词条的设置方式
super(name);
}
// 不存储
public static final TermVector NO = new TermVector("NO");
// 为每个Document都存储一个TermVector
public static final TermVector YES = new TermVector("YES");
// 存储,同时存储位置信息
public static final TermVector WITH_POSITIONS = new TermVector("WITH_POSITIONS");
// 存储,同时存储偏移量信息
public static final TermVector WITH_OFFSETS = new TermVector("WITH_OFFSETS");
// 存储,同时存储位置、偏移量信息
public static final TermVector WITH_POSITIONS_OFFSETS = new TermVector("WITH_POSITIONS_OFFSETS");
}
另外,Field类实现了Fieldabel接口类,可以通过了解Fieldable接口的定义,来了解Field类都实现了哪些方法,这些方法的功能是什么。
Fieldable接口类的定义如下所示:
package org.apache.lucene.document;
import java.io.Reader;
import java.io.Serializable;
import org.apache.lucene.analysis.TokenStream;
public interface Fieldable extends Serializable {
/* 设置命中该Field的boost因子,这个值将被乘到一个排序分值上,这个分值是这个Document中的该Field被命中的分值。
// The boost is multiplied by
{@link
org.apache.lucene.document.Document#getBoost()} of the document。
Document的getBoost()方法返回一个值,这个值将与boost相乘,这里要求是包含该Field的Document。如果一个Document中有多个名字相同的Field,则这些值需要乘到一起。得到的乘积结果,再乘到
{@link
org.apache.lucene.search.Similarity#lengthNorm(String,int)}上,并且,在存储到索引中之前,rounded by
{@link
org.apache.lucene.search.Similarity#encodeNorm(float)} 要保证乘积结果没有溢出。
*/
void setBoost(float boost);
// 返回boost的值
float getBoost();
/** 返回一个Field的内部字符串,例如:.
* "date"、"title"、"body"等等
*/
String name();
// Field的字符串值,或null。
public String stringValue();
// Field作为一个Reader的值,或null。
public Reader readerValue();
// Field作为Binary的值,或null。
public byte[] binaryValue();
// Field作为一个TokenStream的值,或null。
public TokenStream tokenStreamValue();
// 当且仅当Field的值被存储到索引中时,返回
boolean isStored();
// 当且仅当Field的被索引时,以便能够检索到,返回true
boolean isIndexed();
// Field的值是否被分词
boolean isTokenized();
// 是否被压缩存储
boolean isCompressed();
// 词条用于索引该Field,则以词条向量的形式存储
boolean isTermVectorStored();
/**
* True iff terms are stored as term vector together with their offsets
* (start and end positon in source text).
*/
boolean isStoreOffsetWithTermVector();
/**
* True iff terms are stored as term vector together with their token positions.
*/
boolean isStorePositionWithTermVector();
// Field的值是否以二进制存储
boolean isBinary();
// 该Field是否索引,但不分词
boolean getOmitNorms();
/** Expert:
*
* If set, omit normalization factors associated with this indexed field.
* This effectively disables indexing boosts and length normalization for this field.
*/
void setOmitNorms(boolean omitNorms);
// 是否延迟加载,检索
boolean isLazy();
}
通过Fieldable接口的定义,可以了解到实现该接口的Field类可以完成哪些功能了。
在Field构造好了以后,需要将每个Field添加到Document中,便于Document对Field进行管理。
Document类的源代码如下所示:
package org.apache.lucene.document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Searcher;
import java.util.*;
public final class Document implements java.io.Serializable {
List fields = new Vector(); // 用于管理多个Field的列表
private float boost = 1.0f;
// 构造一个不含有Field的Document对象
public Document() {}
// 参看Field中说明
public void setBoost(float boost) {
this.boost = boost;
}
public float getBoost() {
return boost;
}
// 向一个Document(向量列表)中添加Field
public final void add(Fieldable field) {
fields.add(field);
}
// 从Document中移除名称为name的Field,如果存在多个名称为name的Field,则移除第一个被添加到Document中的Field
public final void removeField(String name) {
Iterator it = fields.iterator();
while (it.hasNext()) {
Fieldable field = (Fieldable)it.next();
if (field.name().equals(name)) {
it.remove();
return;
}
}
}
// 从Document中移除名称为name的Field,如果存在多个名称为name的Field,则全部移除掉
public final void removeFields(String name) {
Iterator it = fields.iterator();
while (it.hasNext()) {
Fieldable field = (Fieldable)it.next();
if (field.name().equals(name)) {
it.remove();
}
}
}
// 从Document中获取名称为name的Field,如果存在多个名称为name的Field,则返回第一个被添加到Document中的Field
public final Field getField(String name) {
for (int i = 0; i < fields.size(); i++) {
Field field = (Field)fields.get(i);
if (field.name().equals(name))
return field;
}
return null;
}
// 从Document中获取名称为name的Field,如果存在多个名称为name的Field,则返回第一个被添加到Document中的Field
public Fieldable getFieldable(String name) {
for (int i = 0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
if (field.name().equals(name))
return field;
}
return null;
}
// 从Document中获取名称为name的Field的String串,如果存在多个名称为name的Field,则返回第一个被添加到Document中的Field的String串
public final String get(String name) {
for (int i = 0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
if (field.name().equals(name) && (!field.isBinary()))
return field.stringValue();
}
return null;
}
// 从Document中获取所有的Field,返回一个Enumeration类型
public final Enumeration fields() {
return ((Vector)fields).elements();
}
// 从Document中获取所有的Field,返回一个List类型
public final List getFields() {
return fields;
}
// 从Document中获取名称为name的所有Field,返回一个Field[]数组
public final Field[] getFields(String name) {
List result = new ArrayList();
for (int i = 0; i < fields.size(); i++) {
Field field = (Field)fields.get(i);
if (field.name().equals(name)) {
result.add(field);
}
}
if (result.size() == 0)
return null;
return (Field[])result.toArray(new Field[result.size()]);
}
// 从Document中获取名称为name的所有Field,返回一个Fieldable[]数组
public Fieldable[] getFieldables(String name) {
List result = new ArrayList();
for (int i = 0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
if (field.name().equals(name)) {
result.add(field);
}
}
if (result.size() == 0)
return null;
return (Fieldable[])result.toArray(new Fieldable[result.size()]);
}
/// 从Document中获取名称为name的所有Field的字符串值,返回一个字符串数组
public final String[] getValues(String name) {
List result = new ArrayList();
for (int i = 0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
if (field.name().equals(name) && (!field.isBinary()))
result.add(field.stringValue());
}
if (result.size() == 0)
return null;
return (String[])result.toArray(new String[result.size()]);
}
// 从Document中获取名称为name的所有Field的byte[] 的数组值,返回一个byte[][]数组
public final byte[][] getBinaryValues(String name) {
List result = new ArrayList();
for (int i = 0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
if (field.name().equals(name) && (field.isBinary()))
result.add(field.binaryValue());
}
if (result.size() == 0)
return null;
return (byte[][])result.toArray(new byte[result.size()][]);
}
// 从Document中获取名称为name的所有Field的byte字节值,返回一个byte[]数组
public final byte[] getBinaryValue(String name) {
for (int i=0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
if (field.name().equals(name) && (field.isBinary()))
return field.binaryValue();
}
return null;
}
// 输出Document中所有Field的名称(以字符串的形式)
public final String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("Document<");
for (int i = 0; i < fields.size(); i++) {
Fieldable field = (Fieldable)fields.get(i);
buffer.append(field.toString());
if (i != fields.size()-1)
buffer.append(" ");
}
buffer.append(">");
return buffer.toString();
}
}
Document类的最后一个方法,输出了一个Document中的所有的Field的信息,以字符串的形式输出,这个可以在检索的时候,看到具体检索关键字检索到的信息的详情,可以在文章
Lucene-2.2.0 源代码阅读学习(4)
中看到输出示例。
做个简单的例子:
直接在程序中通过字符串构造Field。
测试的主函数代码如下所示:
package org.shirdrn.lucene;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
public class DcoumentAndField {
public static void main(String[] args) {
Document doc = new Document();
Field f1 = new Field("检索","不以物喜,不以己悲。",Field.Store.YES,Field.Index.TOKENIZED);
Field f2 = new Field("雅诗","衣带渐宽终不悔,为伊消得人憔悴。",Field.Store.NO,Field.Index.UN_TOKENIZED);
Field f3 = new Field("娱乐","某某透露,最终决定,要坚持走出一条属于自己的人生路。",Field.Store.NO,Field.Index.NO_NORMS);
Field f4 = new Field("娱乐","百度的门户建设,应该是一件不容忽视的事件。",Field.Store.NO,Field.Index.NO_NORMS);
doc.add(f1);
doc.add(f2);
doc.add(f3);
doc.add(f4);
System.out.println("所有的Field信息 :"+doc.toString());
System.out.println("获取指定Field信息:"+doc.get("雅诗"));
System.out.println("获取指定Field信息:"+doc.getField("检索"));
System.out.println("指定Field信息[0]:"+doc.getValues("娱乐")[0]);
System.out.println("指定Field信息[1]:"+doc.getValues("娱乐")[1]);
}
}
运行结程序,输出结果如下所示:
所有的Field信息 :Document<stored/uncompressed,indexed,tokenized<检索:不以物喜,不以己悲。> indexed<雅诗:衣带渐宽终不悔,为伊消得人憔悴。> indexed,omitNorms<娱乐:某某透露,最终决定,要坚持走出一条属于自己的人生路。> indexed,omitNorms<娱乐:百度的门户建设,应该是一件不容忽视的事件。>>
获取指定Field信息:衣带渐宽终不悔,为伊消得人憔悴。
获取指定Field信息:stored/uncompressed,indexed,tokenized<检索:不以物喜,不以己悲。>
指定Field信息[0]:某某透露,最终决定,要坚持走出一条属于自己的人生路。
指定Field信息[1]:百度的门户建设,应该是一件不容忽视的事件。
到目前为止,已经对Field和Document很熟悉了。
然后,又要回到IndexWriter类了,该类特别重要,同时又是特别复杂的。IndexWriter初始化完成后,才开始了真正地建立索引的过程。建立索引是从,向一个IndexWriter实例中添加Document开始。
因此,要用到IndexWriter类的一些有用的方法了。
回到IndexWriter索引器类中来,学习该类添加Document的方法。
这时,需要用到一个非常重要的类:DocumentWriter,该类对Document进行了很多处理,比如“文档倒排”就是其中的一项重要内容。
实例化一个IndexWriter索引器之后,要向其中添加Document,在IndexWriter类中有两个实现该功能的方法:
public void addDocument(Document doc) throws CorruptIndexException, IOException {
addDocument(doc, analyzer);
}
public void addDocument(Document doc, Analyzer analyzer) throws CorruptIndexException, IOException {
ensureOpen();
// 确保IndexWriter是打开的,这样才能向其中添加Document
SegmentInfo newSegmentInfo = buildSingleDocSegment(doc, analyzer); // 构造一个SegmentInfo实例,SegmentInfo是用来维护索引段信息的
synchronized (this) {
ramSegmentInfos.addElement(newSegmentInfo);
maybeFlushRamSegments();
}
}
可以看出,第一个addDocument方法调用了第二个重载的方法,所以关键在于第二个addDocument方法。
这里,ramSegmentInfos是IndexWriter类的一个成员,该ramSegmentInfos是存在于RAMDirectory中的,定义为:
SegmentInfos ramSegmentInfos = new SegmentInfos();
关于SegmentInfos类,可以参考文章
Lucene-2.2.0 源代码阅读学习(18)
。
上面,buildSingleDocSegment()方法,通过给定的Document和Analyzer来构造一个SegmentInfo实例,关于SegmentInfo类,可以参考文章
Lucene-2.2.0 源代码阅读学习(19)
,buildSingleDocSegment()方法的实现如下所示:
SegmentInfo buildSingleDocSegment(Document doc, Analyzer analyzer)
throws CorruptIndexException, IOException {
DocumentWriter dw = new DocumentWriter(ramDirectory, analyzer, this); // 实例化一个DocumentWriter对象
dw.setInfoStream(infoStream); // 设置一个PrintStream infoStream流对象
String segmentName = newRamSegmentName(); // 在内存中新建一个索引段名称
dw.addDocument(segmentName, doc); // 将Document添加到指定的名称为segmentName的索引段文件中
/*
根据指定的segmentName、ramDirectory,
Document的数量为1个,构造一个SegmentInfo对象,根据SegmentInfo的构造函数:
public SegmentInfo(String name, int docCount, Directory dir, boolean isCompoundFile, boolean hasSingleNormFile)
可知,指定构造的不是一个复合文件,也不是一个具有单独norm文件的SegmentInfo对象,因为我们使用的是2.2版本的,从2.1版本往后,就统一使用一个.nrm文件来代替以前使用的norm文件*/
SegmentInfo si = new SegmentInfo(segmentName, 1, ramDirectory, false, false);
si.setNumFields(dw.getNumFields()); // 设置SegmentInfo中Field的数量
return si; // 返回构造好的SegmentInfo对象
}
在内存中新建一个索引段名称,调用了IndexWriter类的一个方法:
final synchronized String newRamSegmentName() {
// synchronized,需要考虑线程同步问题
return "_ram_" + Integer.toString(ramSegmentInfos.counter++, Character.MAX_RADIX);
}
初始化一个SegmentInfo实例时,counter的值为0,counter是用来为一个新的索引段命名的,在SegmentInfo类中定义了这个成员:
public int counter = 0;
上面的newRamSegmentName()方法返回的是一个索引段的名称(该名称用来在内存中,与RAMDirectory相关的),即文件名称为_ram_1。
从上面可以看出,IndexWriter类的addDocument()方法中,最重要的调用buildSingleDocSegment()方法,创建一个SegmentInfo对象,从而在buildSingleDocSegment()方法中使用到了DocumentWriter类,这才是关键了。
下面研究DocumentWriter这个核心类,从IndexWriter类中addDocument()方法入手,先把用到DocumentWriter类的一些具体细节拿出来研究。
一个DocumentWriter的构造
DocumentWriter(Directory directory, Analyzer analyzer, IndexWriter writer) {
this.directory = directory;
this.analyzer = analyzer;
this.similarity = writer.getSimilarity();
this.maxFieldLength = writer.getMaxFieldLength();
this.termIndexInterval = writer.getTermIndexInterval();
}
在这个构造方法中,最大的Field长度为10000,即this.maxFieldLength = writer.getMaxFieldLength();,可以在IndexWriter类中找到定义:
public int getMaxFieldLength() {
ensureOpen();
return maxFieldLength;
}
然后maxFieldLength定义为:
private int maxFieldLength = DEFAULT_MAX_FIELD_LENGTH;
其中,DEFAULT_MAX_FIELD_LENGTH值为:
public final static int DEFAULT_MAX_FIELD_LENGTH = 10000;
同理,默认词条索引区间为128,即this.termIndexInterval = writer.getTermIndexInterval();,也可以在IndexWriter类中找到定义。
另外,this.similarity = writer.getSimilarity();,其实DocumentWriter的这个成员similarity=new DefaultSimilarity();。DefaultSimilarity类继承自Similarity抽象类,该类是用来处理有关“相似性”的,与检索密切相关,其实就是对一些数据在运算过程中可能涉及到数据位数的舍入与进位。具体地,Similarity类的定义可查看org.apache.lucene.search.Similarity。
这样,一个DocumentWriter就构造完成了。
DocumentWriter类的addDocument()方法
final void addDocument(String segment, Document doc)
throws CorruptIndexException, IOException {
// 创建一个FieldInfos对象,用来存储加入到索引的Document中的各个Field的信息
fieldInfos = new FieldInfos();
fieldInfos.add(doc); // 将Document加入到FieldInfos中
// postingTable是用于存储所有词条的HashTable
postingTable.clear(); // clear postingTable
fieldLengths = new int[fieldInfos.size()]; // 初始化int[]数组fieldLengths,用来记录当前Document中所有Field的长度
fieldPositions = new int[fieldInfos.size()]; // 初始化int[]数组fieldPositions,用来记录当前Document中所有Field在分析完成后所处位置
fieldOffsets = new int[fieldInfos.size()]; // 初始化int[]数组fieldOffsets,用来记录当前Document中所有Field的offset
fieldStoresPayloads = new BitSet(fieldInfos.size());
fieldBoosts = new float[fieldInfos.size()]; // 初始化int[]数组fieldBoosts,用来记录当前Document中所有Field的boost值
Arrays.fill(fieldBoosts, doc.getBoost()); // 为fieldBoosts数组中的每个元素赋值,根据Document中记录的boost值
try {
// 在将FieldInfos写入之前,要对Document中的各个Field进行“倒排”
invertDocument(doc);
// 对postingTable中的词条进行排序,返回一个排序的Posting[]数组
Posting[] postings = sortPostingTable();
// 将FieldInfos写入到索引目录directory中,即写入到文件segments.fnm中
fieldInfos.write(directory, segment + ".fnm");
// 构造一个FieldInfos的输出流FieldsWriter,将Field的详细信息(包括上面提到的各个数组中的值)写入到索引目录中
FieldsWriter fieldsWriter =
new FieldsWriter(directory, segment, fieldInfos);
try {
fieldsWriter.addDocument(doc); // 将Document加入到FieldsWriter
} finally {
fieldsWriter.close(); // 关闭FieldsWriter输出流
}
// 将经过排序的Posting[]数组写入到索引段文件中(segmentsv.frq文件和segments.prx文件)
writePostings(postings, segment);
// 写入被索引的Field的norm信息
writeNorms(segment);
} finally {
// 关闭TokenStreams
IOException ex = null;
Iterator it = openTokenStreams.iterator();
// openTokenStreams是DocumentWriter类定义的一个链表成员,即:private List openTokenStreams = new LinkedList();
while (it.hasNext()) {
try {
((TokenStream) it.next()).close();
} catch (IOException e) {
if (ex != null) {
ex = e;
}
}
}
openTokenStreams.clear(); // 清空openTokenStreams
if (ex != null) {
throw ex;
}
}
}
DocumentWriter实现对Document的“倒排”
在DocumentWriter类的addDocument()方法中,在对Document中的各个Field输出到索引目录之前,要对所有加入到IndexWriter索引器(一个IndexWriter的构造,指定了一个Analyzer分析器)的Document执行倒排,即调用倒排的方法invertDocument()。
invertDocument()方法的实现如下所示:
// 调用底层分析器接口,遍历Document中的Field,对数据源进行分析
private final void invertDocument(Document doc)
throws IOException {
Iterator fieldIterator = doc.getFields().iterator(); // 通过Document获取Field的List列表doc.getFields()
while (fieldIterator.hasNext()) {
Fieldable field = (Fieldable) fieldIterator.next();
String fieldName = field.name();
int fieldNumber = fieldInfos.fieldNumber(fieldName); // 根据一个Field的fieldName得到该Field的编号number(number是FieldInfo类的一个成员)
int length = fieldLengths[fieldNumber];
// 根据每个Field的编号,设置每个Field的长度
int position = fieldPositions[fieldNumber]; // 根据每个Field的编号,设置每个Field的位置
if (length>0) position+=analyzer.getPositionIncrementGap(fieldName);
int offset = fieldOffsets[fieldNumber]; // 根据每个Field的编号,设置每个Field的offset
if (field.isIndexed()) { // 如果Field被索引
if (!field.isTokenized()) { // 如果Field没有进行分词
String stringValue = field.stringValue(); // 获取Field的String数据值
if(field.isStoreOffsetWithTermVector()) // 是否把整个Field的数据作为一个词条存储到postingTable中
// 把整个Field的数据作为一个词条存储到postingTable中
addPosition(fieldName, stringValue, position++, null, new TermVectorOffsetInfo(offset, offset + stringValue.length()));
else // 否则,不把整个Field的数据作为一个词条存储到postingTable中
addPosition(fieldName, stringValue, position++, null, null);
offset += stringValue.length();
length++;
} else
{ // 需要对Field进行分词
TokenStream stream = field.tokenStreamValue();
if (stream == null) { // 如果一个TokenStream不存在,即为null,则必须从一个Analyzer中获取一个TokenStream流
Reader reader;
if (field.readerValue() != null)
// 如果从Field获取的Reader数据不为null
reader = field.readerValue();
// 一个Reader流存在
else if (field.stringValue() != null)
reader = new StringReader(field.stringValue());
// 根据从Field获取的字符串数据构造一个Reader输入流
else
throw new IllegalArgumentException
("field must have either String or Reader value");
// 把经过分词处理的Field加入到postingTable中
stream = analyzer.tokenStream(fieldName, reader);
}
// 将每个Field对应的TokenStream加入到链表openTokenStreams中,等待整个Document中的所有Field都分析处理完毕后,对链表openTokenStreams中的每个链表TokenStream进行统一关闭
openTokenStreams.add(stream);
// 对第一个Token,重置一个TokenStream
stream.reset();
Token lastToken = null;
for (Token t = stream.next(); t != null; t = stream.next()) {
position += (t.getPositionIncrement() - 1); // 每次切出一个词,就将position加上这个词的长度
Payload payload = t.getPayload(); // 每个词都对应一个Payload,它是关于一个词存储到postingTable中的元数据(metadata)
if (payload != null) {
fieldStoresPayloads.set(fieldNumber); // private BitSet fieldStoresPayloads;,BitSet是一个bits的向量,调用BitSet类的set方法,设置该Field的在索引fieldNumber处的bit值
}
TermVectorOffsetInfo termVectorOffsetInfo;
if (field.isStoreOffsetWithTermVector()) { // 如果指定了Field的词条向量的偏移量,则存储该此条向量
termVectorOffsetInfo = new TermVectorOffsetInfo(offset + t.startOffset(), offset + t.endOffset());
} else {
termVectorOffsetInfo = null;
}
// 把该Field的切出的词条存储到postingTable中
addPosition(fieldName, t.termText(), position++, payload, termVectorOffsetInfo);
lastToken = t;
if (++length >= maxFieldLength) {// 如果当前切出的词条数已经达到了该Field的最大长度
if (infoStream != null)
infoStream.println("maxFieldLength " +maxFieldLength+ " reached, ignoring following tokens");
break;
}
}
if(lastToken != null) // 如果最后一个切出的词不为null,设置offset的值
offset += lastToken.endOffset() + 1;
}
fieldLengths[fieldNumber] = length;
// 存储Field的长度
fieldPositions[fieldNumber] = position; // 存储Field的位置
fieldBoosts[fieldNumber] *= field.getBoost(); // 存储Field的boost值
fieldOffsets[fieldNumber] = offset; // 存储Field的offset值
}
}
// 所有的Field都有经过分词处理的具有Payload描述的词条,更新FieldInfos
for (int i = fieldStoresPayloads.nextSetBit(0); i >= 0; i = fieldStoresPayloads.nextSetBit(i+1)) {
fieldInfos.fieldInfo(i).storePayloads = true;
}
}
使用快速排序对postingTable进行排序
当FieldInfos中的每个Field进行分词以后,所有切出的词条都放到了一个HashTable postingTable中,这时所有的词条在postingTable中是无序的。在DocumentWriter的addDocument()方法中调用了sortPostingTable()方法,对词条进行了排序,排序使用“快速排序”方式,“快速排序”的时间复杂度O(N*logN),排序速度很快。
sortPostingTable()方法的实现如下所示:
private final Posting[] sortPostingTable() {
// 将postingTable转换成Posting[]数组,便于快速排序
Posting[] array = new Posting[postingTable.size()];
Enumeration postings = postingTable.elements();
for (int i = 0; postings.hasMoreElements(); i++)
array[i] = (Posting) postings.nextElement();
// 调用quickSort()方法,使用快速排序对Posting[]数组进行排序
quickSort(array, 0, array.length - 1);
return array;
}
快速排序的算法都不陌生,在Lucene中也给出了实现,快速排序方法如下:
private static final void quickSort(Posting[] postings, int lo, int hi) {
if (lo >= hi)
return;
int mid = (lo + hi) / 2;
if (postings[lo].term.compareTo(postings[mid].term) > 0) {
Posting tmp = postings[lo];
postings[lo] = postings[mid];
postings[mid] = tmp;
}
if (postings[mid].term.compareTo(postings[hi].term) > 0) {
Posting tmp = postings[mid];
postings[mid] = postings[hi];
postings[hi] = tmp;
if (postings[lo].term.compareTo(postings[mid].term) > 0) {
Posting tmp2 = postings[lo];
postings[lo] = postings[mid];
postings[mid] = tmp2;
}
}
int left = lo + 1;
int right = hi - 1;
if (left >= right)
return;
Term partition = postings[mid].term;
for (; ;) {
while (postings[right].term.compareTo(partition) > 0)
--right;
while (left < right && postings[left].term.compareTo(partition) <= 0)
++left;
if (left < right) {
Posting tmp = postings[left];
postings[left] = postings[right];
postings[right] = tmp;
--right;
} else {
break;
}
}
quickSort(postings, lo, left);
quickSort(postings, left + 1, hi);
}
关于Posting类
该类是为排序服务的,提取了与词条信息有关的一些使用频率较高的属性,定义成了该Posting类,实现非常简单,如下所示:
final class Posting { // 在一个Document中与词条有关的信息
Term term; // 一个词条
int freq;
// 词条Term term在该Document中的频率
int[] positions;
// 位置
Payload[] payloads; // Payloads信息
TermVectorOffsetInfo [] offsets; // 词条向量的offset(偏移量)信息
Posting(Term t, int position, Payload payload, TermVectorOffsetInfo offset) { // Posting构造器
term = t;
freq = 1;
positions = new int[1];
positions[0] = position;
if (payload != null) {
payloads = new Payload[1];
payloads[0] = payload;
} else
payloads = null;
if(offset != null){
offsets = new TermVectorOffsetInfo[1];
offsets[0] = offset;
} else
offsets = null;
}
}
Document的倒排非常重要。总结一下:
1、该invertDocument()方法遍历了FieldInfos的每个Field,根据每个Field的属性进行分析,如果需要分词,则调用底层分析器接口,执行分词处理。
2、在invertDocument()方法中,对Field的信息进行加工处理,尤其是每个Field的切出的词条,这些词条最后将添加到postingTable中。
上面DocumentWriter类的addDocument()方法中writePostings()方法,是对已经经过倒排的文档,将词条的一些有用信息写入到索引段文件中。
关于writePostings()方法的实现参考文章
Lucene-2.2.0 源代码阅读学习(23)
。
最后的总结:
在学习DocumentWriter类的addDocument()方法的过程中,涉及到了该类的很多方法,其中关于文档的倒排的方法是非常重要的。
此外,还涉及到了FieldInfos类和FieldInfo类,他们的关系很像SegmentInfos类和SegmentInfo类。FieldInfos类主要是对Document添加到中的Field进行管理的,可以通过FieldInfos类来访问Document中所有Field的信息。每个索引段(索引段即Segment)都拥有一个单独的FieldInfos。
应该对FieldInfos类和FieldInfo类有一个了解。
关于FieldInfos类和FieldInfo类。
FieldInfo类与一个Document中的一个Field相对应,而FieldInfos类又是多个FieldInfo的容器,对每个Document的所有Field对应的FieldInfo进行管理。
FieldInfos类和FieldInfo类之间的关系,恰似SegmentInfos类(可以参考文章
Lucene-2.2.0 源代码阅读学习(18)
)和SegmentInfo类(可以参考文章
Lucene-2.2.0 源代码阅读学习(19)
)之间的关系。
FieldInfo类的实现比较简单,该类的定义如下所示:
package org.apache.lucene.index;
final class FieldInfo {
String name;
// 一个Field的名称
boolean isIndexed; // 该Field是否被索引
int number; // 该Field的编号
// 是否存储该Field的词条向量
boolean storeTermVector;
boolean storeOffsetWithTermVector;
boolean storePositionWithTermVector;
boolean omitNorms; // 是否忽略与被索引的该Field相关的norm文件信息
boolean storePayloads; // 是否该Field存储与词条位置相关的Payload
// 构造一个FieldInfo对象
FieldInfo(String na, boolean tk, int nu, boolean storeTermVector,
boolean storePositionWithTermVector, boolean storeOffsetWithTermVector,
boolean omitNorms, boolean storePayloads) {
name = na;
isIndexed = tk;
number = nu;
this.storeTermVector = storeTermVector;
this.storeOffsetWithTermVector = storeOffsetWithTermVector;
this.storePositionWithTermVector = storePositionWithTermVector;
this.omitNorms = omitNorms;
this.storePayloads = storePayloads;
}
}
上面就是2.2.0版本中FieldInfo类的全部定义。
下面是FieldInfos了的定义了,该类主要是通过FieldInfo来管理一个Document中的全部Field,源代码如下所示:
package org.apache.lucene.index;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import java.io.IOException;
import java.util.*;
// FieldInfo描述的是Document中的Field的信息,而FieldInfos类是用来管理一个个的FieldInfo的。
final class FieldInfos {
// 下面一组byte成员,使用十六进制数初始化,用来管理FieldInfo的属性
static final byte IS_INDEXED = 0x1; // 是否索引
static final byte STORE_TERMVECTOR = 0x2;
// 是否存储词条向量
static final byte STORE_POSITIONS_WITH_TERMVECTOR = 0x4; // 是否存储与词条向量相关的位置
static final byte STORE_OFFSET_WITH_TERMVECTOR = 0x8; // 是否存储与词条向量相关的offset
static final byte OMIT_NORMS = 0x10; // 是否存储被忽略的norms
static final byte STORE_PAYLOADS = 0x20; // 是否存储Payload
private ArrayList byNumber = new ArrayList(); // byNumber是通过编号,用来存放FieldInfo的列表
private HashMap byName = new HashMap(); // byNname是通过名称,用来存放FieldInfo的列表
FieldInfos() { } // 没有参数的FieldInfos的构造函数
// 通过索引目录d和一个索引输入流name构造一个FieldInfos对象
FieldInfos(Directory d, String name) throws IOException {
IndexInput input = d.openInput(name);
try {
read(input); // input输入流已打开,从索引目录中读取
} finally {
input.close();
}
}
// 为一个Document添加Field的信息(这种添加和直接向Document中添加Field不一样,这次添加的不是一些固有信息,是一些更详细的补充信息)
public void add(Document doc) {
List fields = doc.getFields(); // 先获取到该Document中已经添加进去的所有Field
Iterator fieldIterator = fields.iterator();
while (fieldIterator.hasNext()) {
Fieldable field = (Fieldable) fieldIterator.next();
add(field.name(), field.isIndexed(), field.isTermVectorStored(), field.isStorePositionWithTermVector(),
field.isStoreOffsetWithTermVector(), field.getOmitNorms()); // 传参,调用核心的add方法执行添加
}
}
/**
* 添加被索引的Field,需要指定是否具有词条向量
*/
public void addIndexed(Collection names, boolean storeTermVectors, boolean storePositionWithTermVector,boolean storeOffsetWithTermVector) {
Iterator i = names.iterator();
while (i.hasNext()) {
add((String)i.next(), true, storeTermVectors, storePositionWithTermVector, storeOffsetWithTermVector);
}
}
/**
* 当Field没有存储词条向量,添加Field
*
* @param names The names of the fields
* @param isIndexed Whether the fields are indexed or not
*
* @see #add(String, boolean)
*/
public void add(Collection names, boolean isIndexed) {
Iterator i = names.iterator();
while (i.hasNext()) {
add((String)i.next(), isIndexed);
}
}
/**
* Calls 5 parameter add with false for all TermVector parameters.
*
* @param name The name of the Fieldable
* @param isIndexed true if the field is indexed
* @see #add(String, boolean, boolean, boolean, boolean)
*/
public void add(String name, boolean isIndexed) {
add(name, isIndexed, false, false, false, false);
}
/**
* Calls 5 parameter add with false for term vector positions and offsets.
*
* @param name The name of the field
* @param isIndexed true if the field is indexed
* @param storeTermVector true if the term vector should be stored
*/
public void add(String name, boolean isIndexed, boolean storeTermVector){
add(name, isIndexed, storeTermVector, false, false, false);
}
/** If the field is not yet known, adds it. If it is known, checks to make
* sure that the isIndexed flag is the same as was given previously for this
* field. If not - marks it as being indexed. Same goes for the TermVector
* parameters.
*
* @param name The name of the field
* @param isIndexed true if the field is indexed
* @param storeTermVector true if the term vector should be stored
* @param storePositionWithTermVector true if the term vector with positions should be stored
* @param storeOffsetWithTermVector true if the term vector with offsets should be stored
*/
public void add(String name, boolean isIndexed, boolean storeTermVector,
boolean storePositionWithTermVector, boolean storeOffsetWithTermVector) {
add(name, isIndexed, storeTermVector, storePositionWithTermVector, storeOffsetWithTermVector, false);
}
/** If the field is not yet known, adds it. If it is known, checks to make
* sure that the isIndexed flag is the same as was given previously for this
* field. If not - marks it as being indexed. Same goes for the TermVector
* parameters.
*
* @param name The name of the field
* @param isIndexed true if the field is indexed
* @param storeTermVector true if the term vector should be stored
* @param storePositionWithTermVector true if the term vector with positions should be stored
* @param storeOffsetWithTermVector true if the term vector with offsets should be stored
* @param omitNorms true if the norms for the indexed field should be omitted
*/
public void add(String name, boolean isIndexed, boolean storeTermVector,
boolean storePositionWithTermVector, boolean storeOffsetWithTermVector, boolean omitNorms) {
add(name, isIndexed, storeTermVector, storePositionWithTermVector,
storeOffsetWithTermVector, omitNorms, false);
}
/** 如果该Field没有被添加过,则添加它。如果已经添加过,核查后确保它的是否被索引标志位与已经存在的一致,如果是“不索引”标志,则修改标志位为true.
*该add添加方法才是最核心的实现方法。
* @param name The name of the field
* @param isIndexed true if the field is indexed
* @param storeTermVector true if the term vector should be stored
* @param storePositionWithTermVector true if the term vector with positions should be stored
* @param storeOffsetWithTermVector true if the term vector with offsets should be stored
* @param omitNorms true if the norms for the indexed field should be omitted
* @param storePayloads true if payloads should be stored for this field
*/
public FieldInfo add(String name, boolean isIndexed, boolean storeTermVector,
boolean storePositionWithTermVector, boolean storeOffsetWithTermVector,
boolean omitNorms, boolean storePayloads) {
FieldInfo fi = fieldInfo(name); // 根据指定的name构造一个FieldInfo对象
if (fi == null) {
// 如果构造的FieldInfo为null,则调用addInternal()方法,重新构造一个
return addInternal(name, isIndexed, storeTermVector, storePositionWithTermVector, storeOffsetWithTermVector, omitNorms, storePayloads);
} else {
// 如果构造的FieldInfo不为null(即已经存在一个相同name的FieldInfo)
if (fi.isIndexed != isIndexed) { // 如果存在的FieldInfo被索引
fi.isIndexed = true;
// 一旦被索引了,总是索引
}
if (fi.storeTermVector != storeTermVector) {
fi.storeTermVector = true; // 一旦存储词条向量,总是存储
}
if (fi.storePositionWithTermVector != storePositionWithTermVector) {
fi.storePositionWithTermVector = true; // once vector, always vector
}
if (fi.storeOffsetWithTermVector != storeOffsetWithTermVector) {
fi.storeOffsetWithTermVector = true; // once vector, always vector
}
if (fi.omitNorms != omitNorms) {
fi.omitNorms = false;
// 一旦存储norms,则总是存储norms
}
if (fi.storePayloads != storePayloads) {
fi.storePayloads = true;
}
}
return fi;
// 返回一个FieldInfo对象
}
private FieldInfo addInternal(String name, boolean isIndexed,
boolean storeTermVector, boolean storePositionWithTermVector,
boolean storeOffsetWithTermVector, boolean omitNorms, boolean storePayloads) {
FieldInfo fi =
new FieldInfo(name, isIndexed, byNumber.size(), storeTermVector, storePositionWithTermVector,
storeOffsetWithTermVector, omitNorms, storePayloads);
byNumber.add(fi);
// byNumber是一个List,将构造的FieldInfo加入到列表中
byName.put(name, fi); // byName是一个HashMap,其中的键值对<name,fi>表示一个名字为键,一个FieldInfo对象的引用作为值
return fi;
}
public int fieldNumber(String fieldName) { // 根据指定的Field的名称,获取该Field的编号
try {
FieldInfo fi = fieldInfo(fieldName);
if (fi != null)
return fi.number;
}
catch (IndexOutOfBoundsException ioobe) {
return -1;
}
return -1;
}
public FieldInfo fieldInfo(String fieldName) {
// 根据指定的Field的名称,从byName列表中取出该Field
return (FieldInfo) byName.get(fieldName);
}
// 根据指定的编号,获取Field的名称name
public String fieldName(int fieldNumber) {
try {
return fieldInfo(fieldNumber).name;
}
catch (NullPointerException npe) {
return "";
}
}
// 根据指定的Field的编号,获取一个FieldInfo对象
public FieldInfo fieldInfo(int fieldNumber) {
try {
return (FieldInfo) byNumber.get(fieldNumber); // 从byNymber列表中取出索引为指定fieldNumber的FieldInfo对象
}
catch (IndexOutOfBoundsException ioobe) {
return null;
}
}
public int size() { // 计算byName这个HashMap的大小
return byNumber.size();
}
public boolean hasVectors() { // 返回byName这个HashMap中FieldInfo指定对应的Field不存储词条向量的标志值,即false
boolean hasVectors = false;
for (int i = 0; i < size(); i++) {
if (fieldInfo(i).storeTermVector) {
hasVectors = true;
break;
}
}
return hasVectors;
}
public void write(Directory d, String name) throws IOException {
// 将FieldInfo的信息输出到索引目录中,name是索引目录中存在的索引段文件名segments.fnm(可以参考文章
Lucene-2.2.0 源代码阅读学习(21)
中,DocumentWriter的addDocument()方法)
IndexOutput output = d.createOutput(name);
try {
write(output); // 调用下面的write方法,对FieldInfo的信息进行格式化(输出)写入索引目录
} finally {
output.close();
}
}
public void write(IndexOutput output) throws IOException { // 对FieldInfo的信息进行格式化(输出)写入索引目录
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;
output.writeString(fi.name);
output.writeByte(bits);
}
}
private void read(IndexInput input) throws IOException { // 通过打开一个输入流,读取FieldInfo的信息
int size = input.readVInt();//read in the size
for (int i = 0; i < size; i++) {
String name = input.readString().intern();
byte bits = input.readByte();
boolean isIndexed = (bits & IS_INDEXED) != 0;
boolean storeTermVector = (bits & STORE_TERMVECTOR) != 0;
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;
addInternal(name, isIndexed, storeTermVector, storePositionsWithTermVector, storeOffsetWithTermVector, omitNorms, storePayloads); // 调用addInternal()在内存中构造一个FieldInfo对象,对它进行管理
}
}
}
对FieldInfo类和FieldInfos类进行总结:
1、FieldInfo作为一个实体,保存了Field的一些主要的信息;
2、因为对Field的操作比较频繁,而每次管理都在内存中加载FieldInfo这个轻量级但信息很重要的对象,能够大大提高建立索引的速度;
3、FieldInfos包含的信息比较丰富,通过一个FieldInfo对象,调出Document中的Field到内存中,对每个Field进行详细的管理。
4、FieldInfos支持独立从索引目录中读取Document中的信息(主要根据Document参数,管理其中的Field),然后再写回到索引目录。
综合总结:
FieldInfos是对Document中的Field进行管理的,它主要是在内存中进行管理,然后写入到索引目录中。具体地,它所拥有的信息都被写入了一个与索引段文件相关的segments.fnm文件中(可以参考文章
Lucene-2.2.0 源代码阅读学习(21)
,在DocumentWriter类的addDocument()方法中可以看到)
DocumnetWriter类的实现,是在FieldInfos类的基础上。FieldInfos类对Document的所有的Field的静态信息进行管理,而DocumentWriter类表现出了更强大的管理Document的功能,主要是对Field进行了一些高级的操作,比如使用Analyzer分析器进行分词、对切分出来的词条进行排序(文档倒排)等等。
下一步,就要仔细研究DocumnetWriter类了。
通过对DocumentWriter类的writePostings()方法进行学习。同时,研究并解决几个我一直感到困惑的几个类的用途,以及到底怎样阐述能使自己有一种感性的认识。
writePostings()方法的实现
writePostings()方法是对已经经过倒排的文档,将词条的一些有用信息写入到索引段文件中。该方法的实现如下所示:
private final void writePostings(Posting[] postings, String segment)
throws CorruptIndexException, IOException {
IndexOutput freq = null, prox = null;
TermInfosWriter tis = null; // TermInfosWriter类是与词条的写操作有关的
TermVectorsWriter termVectorWriter = null;
try {
// 打开文件流,为倒排的索引进行存储
freq = directory.createOutput(segment + ".frq"); // 打开segments.frq文件
prox = directory.createOutput(segment + ".prx"); // 打开segments.prx文件
tis = new TermInfosWriter(directory, segment, fieldInfos,termIndexInterval); // 创建一个TermInfosWriter对象
TermInfo ti = new TermInfo(); // 创建一个TermInfo对象,该对象用于在内存中管理词条的
String currentField = null;
boolean currentFieldHasPayloads = false;
for (int i = 0; i < postings.length; i++) { // 遍历Posting数组中的每以个对象
Posting posting = postings[i];
// 检查:是否需要转换成一个新的Field
String termField = posting.term.field();
if (currentField != termField) { // 从Posting数组中获取Field的名称(Strnig类型)如果不为null
// 改变currentField。看是否有需要存储的信息
currentField = termField;
FieldInfo fi = fieldInfos.fieldInfo(currentField); // 根据currentField名称从FieldInfos中找到这个FieldInfo对象
currentFieldHasPayloads = fi.storePayloads;
if (fi.storeTermVector) {
if (termVectorWriter == null) {
termVectorWriter =
new TermVectorsWriter(directory, segment, fieldInfos); // 构造一个TermVectorsWriter对象,该对象与词条向量的写操作相关
termVectorWriter.openDocument();
}
termVectorWriter.openField(currentField); // 根据指定的Field的名称currentField打开一个文件输出流
} else if (termVectorWriter != null) {
termVectorWriter.closeField();
}
}
// 为带有指针的sengments.frq文件和segments.prx文件设置一个入口
ti.set(1, freq.getFilePointer(), prox.getFilePointer(), -1); // ti是一个TernInfo类实例,用来管理词条的
tis.add(posting.term, ti); // tis是一个TermInfosWriter类实例,它的方法为add(Term term, TermInfo ti),将一个<Term, TermInfo>对加入到其中
// 为segments.frq文件添加一个入口
int postingFreq = posting.freq;
if (postingFreq == 1)
// optimize freq=1
freq.writeVInt(1);
// set low bit of doc num.
else {
freq.writeVInt(0); // the document number
freq.writeVInt(postingFreq); // frequency in doc
}
int lastPosition = 0; // write positions
int[] positions = posting.positions;
Payload[] payloads = posting.payloads;
int lastPayloadLength = -1;
// 下面是对词条Term的Payload和positions信息进行优化处理,写入输出到索引目录中
// The following encoding is being used for positions and payloads:
// Case 1: current field does not store payloads
// Positions -> <PositionDelta>^freq
// PositionDelta -> VInt
// The PositionDelta is the difference between the current
// and the previous position
// Case 2: current field stores payloads
// Positions -> <PositionDelta, Payload>^freq
// Payload -> <PayloadLength?, PayloadData>
// PositionDelta -> VInt
// PayloadLength -> VInt
// PayloadData -> byte^PayloadLength
// In this case PositionDelta/2 is the difference between
// the current and the previous position. If PositionDelta
// is odd, then a PayloadLength encoded as VInt follows,
// if PositionDelta is even, then it is assumed that the
// length of the current Payload equals the length of the
// previous Payload.
for (int j = 0; j < postingFreq; j++) { // 用希腊字母编码
int position = positions[j];
int delta = position - lastPosition;
if (currentFieldHasPayloads) {
int payloadLength = 0;
Payload payload = null;
if (payloads != null) {
payload = payloads[j];
if (payload != null) {
payloadLength = payload.length;
}
}
if (payloadLength == lastPayloadLength) {
// the length of the current payload equals the length
// of the previous one. So we do not have to store the length
// again and we only shift the position delta by one bit
prox.writeVInt(delta * 2);
} else {
// the length of the current payload is different from the
// previous one. We shift the position delta, set the lowest
// bit and store the current payload length as VInt.
prox.writeVInt(delta * 2 + 1);
prox.writeVInt(payloadLength);
lastPayloadLength = payloadLength;
}
if (payloadLength > 0) {
// write current payload
prox.writeBytes(payload.data, payload.offset, payload.length);
}
} else {
// field does not store payloads, just write position delta as VInt
prox.writeVInt(delta);
}
lastPosition = position;
}
if (termVectorWriter != null && termVectorWriter.isFieldOpen()) {
termVectorWriter.addTerm(posting.term.text(), postingFreq, posting.positions, posting.offsets);
}
}
if (termVectorWriter != null)
termVectorWriter.closeDocument();
} finally {
// 关闭所有的流对象
IOException keep = null;
if (freq != null) try { freq.close(); } catch (IOException e) { if (keep == null) keep = e; }
if (prox != null) try { prox.close(); } catch (IOException e) { if (keep == null) keep = e; }
if (tis != null) try { tis.close(); } catch (IOException e) { if (keep == null) keep = e; }
if (termVectorWriter != null) try { termVectorWriter.close(); } catch (IOException e) { if (keep == null) keep = e; }
if (keep != null) throw (IOException) keep.fillInStackTrace();
}
}
上面的writePostings()方法,将与词条相关的一些信息写入到索引目录中,指定的两个文件中,它们是.frq文件(与此条频率有关的)和.prx文件(该文件存储了与词条的位置有关的信息)。
[.fnm文件存储了一个Document中的所有Field的名称,即扩展名由来:Field's name]
关于Posting类
Posting类定义在DocumentWriter类的内部。
在DocumentWriter类的addPosition方法中的最后两行可以看到:
Term term = new Term(field, text, false);
postingTable.put(term, new Posting(term, position, payload, offset));
一个postingTable是一个HashMap,存储的是一个个的<键,值>对。它的键是Term对象,值是Posting对象。现在看看Term和Posting到底拥有哪些信息。
1、关于Term类,它的源代码如下所示:
package org.apache.lucene.index;
public final class Term implements Comparable, java.io.Serializable {
String field;
// 一个Field的名称
String text; // 一个词条的文本内容
// 通过Field的名称和词条的文本内容构造一个词条
public Term(String fld, String txt) {
this(fld, txt, true);
}
/* 当打开一个DocumentWriter的时候,会存在一个字符串池(pool),该池中具有Term的field字段,即Field的名称。如果对另一个Field进行分词后,如果产生的词条Term的field(是一个String字符串)在当前的字符串池中已经存在,则返回该字符串field的引用;如果新产生的词条Term的field在当前的字符串池中不存在,说明是一个新的词条(即当前处理的这些词条Term中没有名称为field的词条,也就是,添加到Document中的Field是一个新的Field)则将该Term的field加入到字符串池中。
*/
Term(String fld, String txt, boolean intern) {
field = intern ? fld.intern() : fld;
// field names are interned
text = txt;
// unless already known to be
}
public final String field() { return field; }
public final String text() { return text; }
/**
* 优化已经构造出来的词条Term。返回一个新的词条(与被优化的那个词条具有相同的名称)。
*/
public Term createTerm(String text)
{
return new Term(field,text,false);
}
/ 如果两个词条Term具有相同的field和text,则返回true
public final boolean equals(Object o) {
if (o == this)
return true;
if (o == null)
return false;
if (!(o instanceof Term))
return false;
Term other = (Term)o;
return field == other.field && text.equals(other.text);
}
// 返回一个词条的field和the text的哈希码之和
public final int hashCode() {
return field.hashCode() + text.hashCode();
}
public int compareTo(Object other) {
return compareTo((Term)other);
}
// 定制客户化排序方式
public final int compareTo(Term other) {
if (field == other.field)
// fields are interned
return text.compareTo(other.text);
else
return field.compareTo(other.field);
}
// 重新设置一个词条Term的名称field和文本信息text
final void set(String fld, String txt) {
field = fld;
text = txt;
}
public final String toString() { return field + ":" + text; }
// 根据一个打开的对象输入流,从字符串池中读取一个Term的名称field
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, ClassNotFoundException
{
in.defaultReadObject();
field = field.intern();
}
}
Term是文本中的一个词(即word),它是检索的基本单位。
2、关于Posting类,它的源代码如下所示:
final class Posting {
// 关于在Document中的一个词条Term的信息
Term term; // 一个Term对象
int freq; // 该指定词条Term在Document中的频率
int[] positions; // 该词条的位置数组,因为在一个Document中可能存在多个相同的该Term,所有它的位置不是唯一的
Payload[] payloads; // 该词条Term的payload信息
TermVectorOffsetInfo [] offsets;
// 该词条向量的偏移量数组
Posting(Term t, int position, Payload payload, TermVectorOffsetInfo offset) {
term = t;
freq = 1;
positions = new int[1];
positions[0] = position;
if (payload != null) {
payloads = new Payload[1];
payloads[0] = payload;
} else
payloads = null;
if(offset != null){
offsets = new TermVectorOffsetInfo[1];
offsets[0] = offset;
} else
offsets = null;
}
}
3、关于Payload类
上面,Posting类中讲一个Payload[]数组作为它的成员,到底Payload如何定义?从它的源代码解读:
package org.apache.lucene.index;
import java.io.Serializable;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
/**
一个Payload是一个元数据(就是数据的数据) ,在Posting类中用到它,说明它是一个用于描述Posting对象的(构成和特征)。进行分词处理的时候,每切出一个词条Term,即这个词条存在了,则同时存储一个Payload信息。当把一个<Term,Posting>对放到postTable中的时候,就为该Posting增加了一个Payload元数据。
这个元数据最早被添加到哪个类的对象里了呢?从DocumentWriter类的invertDocument()方法中,可以看到对该类的addPosition()方法的调用,这调用addPosition()方法的时候,传了一个Payload,继续看这个Payload的来源,可以看到,是在一个TokenStream打开,进行分词的过程中,从一个Token中获取到的Payload,即 Payload payload = t.getPayload();,这里t是一个Token对象。
继续看addPostition()方法,这行代码ti.payloads[freq] = payload;,其中ti是Posting的实例,每个ti(即Posting)对应着一个Payload[]数组:ti.payloads = new Payload[ti.positions.length];,这个数组的大小为这个Posting的位置数组positions[]的长度。
上面就是Payload的来源,及其一些去向和处理。
之后,根据Payload构造了一个Posting对象,并且在invertDocument()方法中进行分词,根据切出来的词构造了一个词条Term,将Posting实例和Term实例作为一个<Term,Posting>对放到postTable中,如下所示:
Term term = new Term(field, text, false);
postingTable.put(term, new Posting(term, position, payload, offset));
*/
public class Payload implements Serializable {
// payload数据的byte数组
protected byte[] data;
// 在byte数组中的偏移量
protected int offset;
// payload的长度
protected int length;
// 创建一个空的payload
protected Payload() {
}
// 根据一个字节数组,即Payload的内容,创建一个Payload
public Payload(byte[] data) {
this(data, 0, data.length);
}
// 创建一个Payload,指定了它的内容data(一个字节数组) 、字节数组的偏移量、data的长度
public Payload(byte[] data, int offset, int length) {
if (offset < 0 || offset + length > data.length) {
throw new IllegalArgumentException();
}
this.data = data;
this.offset = offset;
this.length = length;
}
// 返回Payload内容的长度
public int length() {
return this.length;
}
// 返回指定索引位置的一个字节byte
public byte byteAt(int index) {
if (0 <= index && index < this.length) {
return this.data[this.offset + index];
}
throw new ArrayIndexOutOfBoundsException(index);
}
// 创建一个字节数组(分配了空间),并将Payload的信息拷贝到该字节数组中
public byte[] toByteArray() {
byte[] retArray = new byte[this.length];
System.arraycopy(this.data, this.offset, retArray, 0, this.length);
return retArray;
}
// 将Payload的数据拷贝到指定的一个字节数组中
public void copyTo(byte[] target, int targetOffset) {
if (this.length > target.length + targetOffset) {
throw new ArrayIndexOutOfBoundsException();
}
System.arraycopy(this.data, this.offset, target, targetOffset, this.length);
}
}
现在,用该对Posting、Payload有了进一步的了解了,一个TokenStream打开以后,进行分词处理,返回的是Token,然后从Token中提取有用的信息,来构造我们需要的词条Term,这时一个词条Term就诞生了。因为一个词条是静态的,并不能反映在实际应用中的动态变化轨迹,所以又使用Posting类对一个Term进行封装,为一个词条赋予了更加丰富的内容。
阅读了这么多代码,该综合总结一下了。
通过在文章
Lucene-2.2.0 源代码阅读学习(4)
中的那个例子,跟踪一下一个IndexWriter索引器实例化过程,及其建立索引的过程中都经过了哪些处理(主要看涉及到了哪些类来完成建立索引的强大功能)。
在文章
Lucene-2.2.0 源代码阅读学习(4)
中的主函数如下所示:
public static void main(String[] args){
MySearchEngine mySearcher = new MySearchEngine();
String indexPath = "E:\\Lucene\\myindex";
File file = new File("E:\\Lucene\\txt");
mySearcher.setIndexPath(indexPath);
mySearcher.setFile(file);
IndexWriter writer;
try {
writer = new IndexWriter(mySearcher.getIndexPath(),new CJKAnalyzer(),true);
mySearcher.createIndex(writer, mySearcher.getFile());
mySearcher.searchContent("contents","注册");
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
从红色标注的代码行看起:
初始化一个IndexWriter索引器的时候(红色标注的第一行代码),还没有向其中添加任何的Document。当执行到红色标注的第二行的时候,开始从指定的本地磁盘目录中读取数据源文件(没有经过任何处理),并且根据已经存在的IndexWriter索引器writer,向这个writer中添加Document,可以看到文章
Lucene-2.2.0 源代码阅读学习(4)
中createIndex()方法中的处理是这样的:
writer.addDocument(FileDocument.Document(file));
具体地,从本地磁盘指定目录中读取文件,调用FileDocument类的Document()方法对待处理的原始文件进行一番处理,主要是构造Field,并将构造好的Field添加到Document中,如下所示:
// 通过f的所在路径构造一个Field对象,并设定该Field对象的一些属性:
// “path”是构造的Field的名字,通过该名字可以找到该Field
// Field.Store.YES表示存储该Field;Field.Index.UN_TOKENIZED表示不对该Field进行分词,但是对其进行索引,以便检索
doc.add(new Field("path", f.getPath(), Field.Store.YES, Field.Index.UN_TOKENIZED));
// 构造一个具有最近修改修改时间信息的Field
doc.add(new Field("modified",
DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE),
Field.Store.YES, Field.Index.UN_TOKENIZED));
// 构造一个Field,这个Field可以从一个文件流中读取,必须保证由f所构造的文件流是打开的
doc.add(new Field("contents", new FileReader(f)));
处理完成之后,最后返回一个Document对象,这个Document对象已经含有一定的信息了。接着就把返回的一个个Document对象加入到IndexWriter writer索引器中。
自然而然,从writer.addDocument(FileDocument.Document(file));可以看到,调用的IndexWriter类的addDocumnet()方法,对加入到其中的Document进行处理。这个处理是相当复杂的,我们一步步跟踪它的处理过程,掌握它建立索引的历程。
为了直观,设置了分级序号,即在一个类中是顶级,在该类的某个方法第一次被调用的时候为一级,在此方法中调用到的其他方法都为一级的子级,以此类推。
现在,进入到IndexWriter索引器(IndeWriter writer已经存在,打开了一个流)的内部:
在IndexWriter类中:
1、addDocument()方法中
public void addDocument(Document doc, Analyzer analyzer) throws CorruptIndexException, IOException {
ensureOpen();
SegmentInfo newSegmentInfo = buildSingleDocSegment(doc, analyzer);
synchronized (this) {
ramSegmentInfos.addElement(newSegmentInfo);
maybeFlushRamSegments();
}
}
通过上面知道,传递进来的Document doc和Analyzer analyzer都已经存在在内存中了,可以随时使用他们。
整体概括层次(1.x)
—1.1、调用了ensureOpen()方法
看IndexWriter的addDocument()方法中,首先调用了ensureOpen()方法,该方法根据一个closed标识来保证当前实例化的IndexWriter是否处于打开状态。关于closed的标识的设置,当一个IndexWriter索引器实例化的时候,该值就已经初始化为false了,表示索引器writer已经处于打开状态。如果想要关闭writer,直接调用IndexWriter类的close()方法,可以设置closed的标识为true,表示索引器被关闭了,不能进行有关建立索引的操作了。
—1.2、调用了buildSingleDocSegment(doc, analyzer)方法
IndexWriter的addDocument()方法中,调用buildSingleDocSegment(doc, analyzer)方法,根据传递进来的doc和analyzer(它们都存在于当前的内存中),构造了一个SegmentInfo对象。具体构造过程如下所示:
SegmentInfo buildSingleDocSegment(Document doc, Analyzer analyzer)
throws CorruptIndexException, IOException {
DocumentWriter dw = new DocumentWriter(ramDirectory, analyzer, this);
dw.setInfoStream(infoStream);
String segmentName = newRamSegmentName();
dw.addDocument(segmentName, doc);
SegmentInfo si = new SegmentInfo(segmentName, 1, ramDirectory, false, false);
si.setNumFields(dw.getNumFields());
return si;
}
该buildSingleDocSegment()方法,通过使用DocumentWriter类的实例,实现对Document doc进行处理,处理的过程中使用了传递进来的Analyzer analyzer分析器。
处理完成以后,才构造的SegmentInfo对象,并且根据已经存在的DocumentWriter实例所包含的信息,设置SegmentInfo对象的内容,最后返回一个SegmentInfo si实例。
—1.3、向SegmentInfos中添加一个SegmentInfo
即:ramSegmentInfos.addElement(newSegmentInfo);
—1.4、向SegmentInfos中添加一个SegmentInfo
即:maybeFlushRamSegments();
该方法在IndexWriter类中定义用来监测当前缓冲区,及时将缓冲区中的数据flush到索引目录中。其中可能存在索引段合并的问题。实现代码如下所示:
protected final void maybeFlushRamSegments() throws CorruptIndexException, IOException {
// 如果缓冲区中有足够多的新的Document,或者足够的缓冲的删除的词条
if (ramSegmentInfos.size() >= minMergeDocs || numBufferedDeleteTerms >= maxBufferedDeleteTerms) {
flushRamSegments();
}
}
调用的 flushRamSegments()方法的定义为:
private final synchronized void flushRamSegments() throws CorruptIndexException, IOException {
flushRamSegments(true);
}
这里又调用了flushRamSegments()方法,定义如下:
protected final synchronized void flushRamSegments(boolean triggerMerge)
throws CorruptIndexException, IOException {
if (ramSegmentInfos.size() > 0 || bufferedDeleteTerms.size() > 0) {
mergeSegments(ramSegmentInfos, 0, ramSegmentInfos.size());
if (triggerMerge) maybeMergeSegments(minMergeDocs);
}
}
该方法调用的两个方法mergeSegments()和maybeMergeSegments()才是合并索引段最核心的操作,参考后文。
详细解析层次1.1.x(无)
详细解析层次1.2.x
— —1.2.1、构造DocumentWriter实例
构造一个DocumentWriter实例,需要三个参数。这里:
第一个是ramDirectory,它是IndexWriter类的一个成员,是RAMDirectory类的实例,该类的实例与内存中的目录操作有密切关系。
第二个是analyzer,一个在内存中存在的Analyzer实例。
第三个是this,即当前的这个IndexWriter writer索引器的实例。
DocumentWriter类的这个构造方法定义如下:
DocumentWriter(Directory directory, Analyzer analyzer, IndexWriter writer) {
this.directory = directory;
this.analyzer = analyzer;
this.similarity = writer.getSimilarity();
this.maxFieldLength = writer.getMaxFieldLength();
this.termIndexInterval = writer.getTermIndexInterval();
}
可见,在构造一个DocumentWriter的实例的时候,在该构造函数所具有的参数以外,还设定了DocumentWriter类的几个成员属性的值。一共额外设置了三个属性值,都是从IndexWriterwriter索引器中获取到的。其中:
Similarity是在一个IndexWriter writer实例化的时候,便设置了它的内容,它是关于标准化因子的内容的,可以从IndexWriter类中找到该成员的设置如下所示:
private Similarity similarity = Similarity.getDefault();
使用了默认的Similarity,即DefaultSimilarity类的一个实例,该DefaultSimilarity类是Similarity类的继承子类,该类与检索的关系非常密切。(后面再学习)
maxFieldLength指一个可以为多少个Field建立索引,在Lucene中指定的默认的值为10000,可以根据需要修改这个数值。可以从IndexWriter类中看到定义:
public final static int DEFAULT_MAX_FIELD_LENGTH = 10000
termIndexInterval是词条索引区间,与在内存中处理词条相关。如果该值设置的越大,则导致IndexReader使用的内存空间就越小,也就减慢了词条Term的随机存储速度。该参数决定了每次查询要求计算词条Term的数量。在Lucene中的默认值为128,仍然可以从IndexWriter类中看到定义:
public final static int DEFAULT_TERM_INDEX_INTERVAL = 128;
— —1.2.2、为已经构造好的DocumentWriter dw设置一个PrintStream流
即:dw.setInfoStream(infoStream);
一个PrintStream是一个过滤流,继承自FilterOutputStream类。它可以加入到另一个输出流中,并且很方便地把它所具有的各种信息打印出来。PrintStream流具有很好的性能。
— —1.2.3、在内存中创建一个临时的索引段的名称
即:String segmentName = newRamSegmentName();
调用了IndexWriter类的newRamSegmentName(),生成一个临时的索引段名称,该newRamSegmentName()方法比较简单:
final synchronized String newRamSegmentName() {
return "_ram_" + Integer.toString(ramSegmentInfos.counter++, Character.MAX_RADIX);
}
ramSegmentInfos是SegmentInfos类的实例,该类的counter成员是用来为将要写入到索引目录中的索引段文件命名的,应该是内部名,counter的信息后写入到索引段文件中。如果第一次调用该方法,生成的名称为_ram_1。
— —1.2.4、调用DocumentWriter类的addDocument()方法对doc进行处理
即dw.addDocument(segmentName, doc);
关于该方法的说明可以参考文章
Lucene-2.2.0 源代码阅读学习(21)
。
这里,叙述一下在addDocument()方法中都做了哪些事情:
首先,在addDocument()方法中构造了一个FieldInfos对象,将传递进来的doc加入到其中。从doc中提取关于Field的信息,将操作频繁的信息提取出来,构造一个个的FieldInfo对象。然后FieldInfos在对FieldInfo进行管理。
关于FieldInfos类和FieldInfo的说明可以参考文章
Lucene-2.2.0 源代码阅读学习(22)
。
其次,在addDocument()方法中利用FieldInfos管理FieldInfo的便利性,再次提取Field的信息,构造Posting类的实例,关于Posting类说明可以参考文章
Lucene-2.2.0 源代码阅读学习(23)
。这里面,在一个FieldInfos写入到索引文件之前,要对doc进行倒排(因为doc已经加入到FieldInfos中了),倒排的过程中对Field进行了分词处理。
再次,对doc倒排之后,形成一个Posting[]数组,接着对它进行排序(使用快速排序),之后FieldInfos才能将各个Field的名称写入索引目录中fieldInfos.write(directory, segment + ".fnm");。如果传递进来的segment值为_ram_1它写到了文件_ram_1.fnm文件中。
接着,构造一个FieldsWriter对象:new FieldsWriter(directory, segment, fieldInfos),并且将doc添加到FieldWriter对象中,即fieldsWriter.addDocument(doc),在FieldWriter的addDocument中进行了处理。
最后,处理的是:
writePostings(postings, segment);
writeNorms(segment);
writePostings()方法的实现可以参考文章
Lucene-2.2.0 源代码阅读学习(23)
。
writeNorms()方法的实现如下:
private final void writeNorms(String segment) throws IOException {
for(int n = 0; n < fieldInfos.size(); n++){
FieldInfo fi = fieldInfos.fieldInfo(n);
if(fi.isIndexed && !fi.omitNorms){
float norm = fieldBoosts[n] * similarity.lengthNorm(fi.name, fieldLengths[n]);
IndexOutput norms = directory.createOutput(segment + ".f" + n);
try {
norms.writeByte(Similarity.encodeNorm(norm));
} finally {
norms.close();
}
}
}
}
— —1.2.5、根据经过处理的信息,构造SegmentInfo对象
即:
SegmentInfo si = new SegmentInfo(segmentName, 1, ramDirectory, false, false);
si.setNumFields(dw.getNumFields());
SegmentInfo的该构造函数声明如下:
public SegmentInfo(String name, int docCount, Directory dir, boolean isCompoundFile, boolean hasSingleNormFile)
第一个参数就是索引的文件名,第二个参数是Document的数量,第三个指定是否是复合文件,第四个参数指定是否具有单独的norm文件。
因为是在Lucene 2.2.0版本,已经不使用单独的norm文件了,一个索引段由一个文件统一管理nrom信息。
这里默认设置了Document的数量为1个,且不使用复合文件。
第二行si.setNumFields(dw.getNumFields());,为这个SegmentInfo对象设置Field的数量信息。
最后返回构造好的该SegmentInfo对象。
详细解析层次1.2.4.x
— — —1.2.4.1、关于FieldInfos的第一个write()方法
FieldInfos有两个write()方法,第二个是核心的,先把参数传进第一个方法中:
public void write(Directory d, String name) throws IOException {
IndexOutput output = d.createOutput(name);
/*
if(d instanceof RAMDirectory){
System.out.println("d is a instance of RAMDirectory! ");
}
else if(d instanceof FSDirectory){
System.out.println("d is a instance of FSDirectory! ");
}
*/
try {
write(output); // 调用核心的write()方法
} finally {
output.close();
}
}
上面注释掉的一段代码是用来检测传递进来的Dicrectory到底是FSDirectory还是RAMDirectory。经过测试,这里面传递进来的是实现类RAMDirectory的实例。
— — —1.2.4.2、关于FieldInfos的第二个核心的write()方法
看第一个write()方法中:
IndexOutput output = d.createOutput(name);
返回了一个有name构造的RAMOutputStream输内存出流对象。
接着以output作为参数,调用了FieldInfos的第二个核心的write()方法,该方法实现如下所示:
public void write(IndexOutput output) throws IOException {
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;
output.writeString(fi.name);
output.writeByte(bits);
}
}
根据对该层次的详细叙述, output.writeVInt(size());已经将FieldInfos中FieldInfo的个数的数值写入到当前的缓冲区中了。接着看FieldInfos的第二个write()方法的继续执行:
通过一个for循环,根据指定的位置索引(FieldInfo的编号),遍历每个FieldInfo,对其一些属性进行处理后写入。
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;
output.writeString(fi.name);
output.writeByte(bits);
很容易,其实就是通过一个字节来识别各个属性(比如是否索引、是否存储词条向量、是否忽略norms、是否存储Payload信息),最后将FieldInfo的name和这些经过处理的属性写入到输出流中。
详细解析层次1.2.4.1.x
— — — —1.2.4.1.1、根据传进来的name,使用RAMDirectory的createOutput()方法创建一个输出流
即:IndexOutput output = d.createOutput(name);
这时,可以看到,接下来的一些操作都转入到RAMDirectory中去了。也就是根据一个name参数,我们看一下name在进入到RAMDirectory中的过程中发生了怎样的变化。
RAMDirectory的createOutput()方法定义如下所示:
public IndexOutput createOutput(String name) {
ensureOpen();
RAMFile file = new RAMFile(this);
synchronized (this) {
RAMFile existing = (RAMFile)fileMap.get(name);
if (existing!=null) {
sizeInBytes -= existing.sizeInBytes;
existing.directory = null;
}
fileMap.put(name, file);
}
return new RAMOutputStream(file);
}
RAMDirectory拥有一个成员变量fileMap(它是一个HashMap),在调用ensureOpen()方法时,通过fileMap来判断,如果fileMap=null,则抛出异常,终止程序流程。所以当fileMap!=null的时候说明RAMDirectory是处于打开状态的。
根据已经处于打开状态的this(即RAMDirectory),以其作为参数构造一个 RAMFile file。接着,以当前打开的RAMDirectory作为同步信号量,从fileMap中获取名称为name的一个RAMFile的实例existing,如果内存中不存在名称为name的RAMFile,则要把该RAMFile添加到fileMap中,即fileMap.put(name, file);,同时直接返回以当前RAMDirectory作为参数构造的RAMFile file;如果RAMFile existing存在,则以该name构造的RAMField作为参数,添加到RAMOutputStream内存输出流中。
这里RAMDirectory的sizeInBytes成员指定了当前RAMDirectory中操作的字节数,而对于每个RAMFile都与一个RAMDirectory相关,所以,当从内存中把一个RAMFile放到内存输出流RAMOutputStream中时,当前内存中的字节数便减少了该RAMFile所具有的字节数,即 sizeInBytes -= existing.sizeInBytes;。同时,这个RAMFile进入到了RAMOutputStream中,即表示该文件已经不与RAMDirectory相关了,所以上面有existing.directory = null;。
详细解析层次1.2.4.2.x
— — — —1.2.4.2.1、关于FieldInfos的size()方法
即: output.writeVInt(size());
先看到了size()方法,他是在FieldInfos类的一个方法,定义如下所示:
public int size() {
return byNumber.size();
}
byNumber是FieldInfos类的一个成员,它是一个ArrayList列表,该列表中添加的是一个个的FieldInfo对象,而每个FieldInfo都有一个编号number,可以根据指定的编号从byNumber列表中将FieldInfo取出来,例如,如果指定位置filedNumber,取出该位置上的FieldInfo对象可以使用byNumber.get(filedNumber)。
可见,size()方法返回的是FieldInfos中含有FieldInfo对象的个数。
— — — —1.2.4.2.2、关于IndexOutput的writeVInt()方法
方法定义如下所示:
public void writeVInt(int i) throws IOException {
while ((i & ~0x7F) != 0) {
writeByte((byte)((i & 0x7f) | 0x80));
i >>>= 7;
}
writeByte((byte)i);
}
该方法中,0x7F的值为127,按位取反后~0x7F的值为-128。
当0<=i<=127时,(i & ~0x7F) =0;当128<=i<=255时,(i & ~0x7F) =128;当256<=i<=511时,(i & ~0x7F) =256;以此类推。
也就是说,判断的条件不等于0成立 ,i的取值范围是i>=128。
调用了RAMOutputStream类的writeBytes()方法,写入的字节值为(i & 0x7f) | 0x80,因为,(i & ~0x7F)的值为0,128,256,384,512……,再与0x80(即128)做按位或运算,也就是当(i & 0x7f) 的值为0时,写入的字节是128,从而(i & 0x7f) | 0x80为写入的值:128,256,384,512……,没有0了。
详细解析层次1.2.4.2.2.x
— — — — —1.2.4.2.2.1、关于RAMOutputStream类的writeBytes()方法
output是RAMOutputStream类的一个实例,所以在操作字节时使用了RAMOutputStream类的writeBytes()方法,定义如下:
public void writeByte(byte b) throws IOException {
if (bufferPosition == bufferLength) {
currentBufferIndex++;
switchCurrentBuffer();
}
currentBuffer[bufferPosition++] = b;
}
如果bufferPosition == bufferLength,即缓冲区满,需要动态增加当前buffer的容量,索引位置增加1,当前索引位置在buffer的最后位置,然后调用switchCurrentBuffer()方法,实现字节缓冲区的动态分配:
private final void switchCurrentBuffer() throws IOException {
if (currentBufferIndex == file.buffers.size()) {
currentBuffer = file.addBuffer(BUFFER_SIZE);
} else {
currentBuffer = (byte[]) file.buffers.get(currentBufferIndex);
}
bufferPosition = 0;
bufferStart = BUFFER_SIZE * currentBufferIndex;
bufferLength = currentBuffer.length;
}
否则。缓冲区未满,可以写入。
把将待写入的经过处理的FieldInfo的个数数值,以128个字节为最小单位,写入到当前的缓冲区中,不够128个字节,填充到128个字节,不能截断数据。\\
复合索引文件格式(.cfs)是如何产生的?从这个问题出发,研究索引文件是如何合并的,这都是IndexWriter类中定义的一些重要的方法。
在建立索引过程中,生成的索引文件的格式有很多种。
在文章
Lucene-2.2.0 源代码阅读学习(4)
中测试的那个例子,没有对IndexWriter进行任何的客户化设置,完全使用Lucene 2.2.0默认的设置(以及,对Field的设置使用了Lucene自带的Demo中的设置)。
运行程序以后,在本地磁盘的索引目录中生成了一些.扩展名为.cfs的索引文件,即复合索引格式文件。如图(该图在文章
Lucene-2.2.0 源代码阅读学习(4)
中介绍过)所示:
从上面生成的那些.cfs复合索引文件可以看出,Lucene 2.2.0版本,IndexWriter索引的一个成员useCompoundFile的设置起了作用,可以在IndexWriter类的内部看到定义和默认设置:
private boolean useCompoundFile = true;
即,默认使用复合索引文件格式来存储索引文件。
在IndexWriter类的addDocument(Document doc, Analyzer analyzer)方法中可以看到,最后调用了 maybeFlushRamSegments()方法,这个方法的作用可是很大的,看它的定义:
protected final void maybeFlushRamSegments() throws CorruptIndexException, IOException {
if (ramSegmentInfos.size() >= minMergeDocs || numBufferedDeleteTerms >= maxBufferedDeleteTerms) {
flushRamSegments();
}
}
这里,minMergeDocs是指:决定了合并索引段文件时指定的最小的Document的数量,在IndexWriter类中默认值为10,可以在IndexWriter类中查看到:
private int minMergeDocs = DEFAULT_MAX_BUFFERED_DOCS;
public final static int DEFAULT_MAX_BUFFERED_DOCS = 10;
其中SegmentInfos ramSegmentInfos中保存了Document的数量的信息,如果Document的数量小于10,则调用flushRamSegments()方法进行处理,flushRamSegments()方法的定义如下所示:
private final synchronized void flushRamSegments() throws CorruptIndexException, IOException {
flushRamSegments(true);
}
在flushRamSegments()方法中又调用到了该方法的一个重载的方法,带一个boolean型参数。该重载的方法定义如下:
protected final synchronized void flushRamSegments(boolean triggerMerge)
throws CorruptIndexException, IOException {
if (ramSegmentInfos.size() > 0 || bufferedDeleteTerms.size() > 0) {
mergeSegments(ramSegmentInfos, 0, ramSegmentInfos.size());
if (triggerMerge) maybeMergeSegments(minMergeDocs);
}
}
同样,如果Document的数量小于10,则调用mergeSegments()方法,先看一下该方法的参数:
private final int mergeSegments(SegmentInfos sourceSegments, int minSegment, int end)
第一个参数指定了一个SegmentInfos(上面调用传递了ramSegmentInfos) ;第二个参数是minSegment是最小的索引段数量(上面调用传递了0,说明如果存在>=0个索引段文件时就开始合并索引文件);第三个参数是end,指要合并索引段文件的个数(上面调用传递了ramSegmentInfos.size(),即对所有的索引段文件都执行合并操作)。
继续看mergeSegments()方法的实现:
private final int mergeSegments(SegmentInfos sourceSegments, int minSegment, int end)
throws CorruptIndexException, IOException {
// doMerge决定了是否执行合并操作,根据end的值,如果end为0说明要合并的索引段文件为0个,即不需要合并,doMerge=false
boolean doMerge = end > 0;
/* 生成合并的索引段文件名称,即根据SegmentInfos的counter值,如果counter=0,则返回的文件名为_0(没有指定扩展名)
final synchronized String newSegmentName() {
return "_" + Integer.toString(segmentInfos.counter++, Character.MAX_RADIX);
}
*/
final String mergedName = newSegmentName();
SegmentMerger merger = null; // 声明一个SegmentMerger变量
final List ramSegmentsToDelete = new ArrayList(); // ramSegmentsToDelete列表用于存放可能要在合并结束后删除的索引段文件,因为合并的过程中需要删除掉合并完以后存在于内存中的这些索引段文件
SegmentInfo newSegment = null;
int mergedDocCount = 0;
boolean anyDeletes = (bufferedDeleteTerms.size() != 0);
// This is try/finally to make sure merger's readers are closed:
try {
if (doMerge) { // 如果doMerge=true,即end>0,也就是说至少有1个以上的索引段文件存在,才能谈得上合并
if (infoStream != null) infoStream.print("merging segments"); // infoStream是一个PrintStream输出流对象,合并完成后要向索引目录中写入合并后的索引段文件,必须有一个打开的输出流
merger = new SegmentMerger(this, mergedName); // 构造一个SegmentMerger对象,通过参数:当前的打开的索引器this和合并后的索引段名称mergedName(形如_N,其中N为数)关于SegmentMerger类会在后面文章学习
for (int i = minSegment; i < end; i++) { // 循环遍历,从SegmentInfos sourceSegments中迭代出每个SegmentInfo对象
SegmentInfo si = sourceSegments.info(i);
if (infoStream != null)
infoStream.print(" " + si.name + " (" + si.docCount + " docs)"); // SegmentInfo si的name在索引目录中是唯一的;这里打印出每个 SegmentInfo si的名称和在这个索引段文件中Document的数量
IndexReader reader = SegmentReader.get(si, MERGE_READ_BUFFER_SIZE); // 调用SegmentReader类的静态方法get(),根据每个SegmentInfo si获取一个索引输入流对象;在IndexWriter类中定义了成员MERGE_READ_BUFFER_SIZE=4096
merger.add(reader); // 将获取到的SegmentReader reader加入到SegmentMerger merger中
if (reader.directory() == this.ramDirectory) { // 如果SegmentReader reader是当前的索引目录,与当前的RAMDirectory ramDirectory是同一个索引目录
ramSegmentsToDelete.add(si);
// 将该SegmentInfo si加入到待删除的列表ramSegmentsToDelete中
}
}
}
SegmentInfos rollback = null;
boolean success = false;
// This is try/finally to rollback our internal state
// if we hit exception when doing the merge:
try {
if (doMerge) { // 如果doMerge=true
mergedDocCount = merger.merge(); // 通过SegmentMerger merger获取需要合并的索引段文件数量
if (infoStream != null) {
// 打印出合并后的索引段文件的名称,及其合并了索引段文件的数量
infoStream.println(" into "+mergedName+" ("+mergedDocCount+" docs)");
}
newSegment = new SegmentInfo(mergedName, mergedDocCount,
directory, false, true); // 实例化一个SegmentInfo对象
}
if (sourceSegments != ramSegmentInfos || anyDeletes) {
// 通过克隆,存储一个用来回滚用的SegmentInfos实例,以防合并过程中发生异常
rollback = (SegmentInfos) segmentInfos.clone();
}
if (doMerge) { // 如果doMerge=true
if (sourceSegments == ramSegmentInfos) { // 如果传进来的sourceSegments和内存中的ramSegmentInfos是同一个
segmentInfos.addElement(newSegment); // 将合并后的新的SegmentInfo newSegment加入到segmentInfos中进行管理,以便之后再对其操作
} else { // 如果传进来的sourceSegments和内存中的ramSegmentInfos不是同一个
for (int i = end-1; i > minSegment; i--) // 删除旧的信息,同时添加新的信息
sourceSegments.remove(i);
segmentInfos.set(minSegment, newSegment);
}
}
if (sourceSegments == ramSegmentInfos) { // 如果传进来的sourceSegments和内存中的ramSegmentInfos是同一个,因为参数设置的原因,可能需要删除合并以后原来旧的索引段文件
maybeApplyDeletes(doMerge); // 调用 maybeApplyDeletes()方法执行合并后的删除处理
doAfterFlush();
}
checkpoint(); // 调用该方法 checkpoint()检查,确认并提交更新
success = true; // 如果检查没有发现异常,则置success=true
} finally {
if (success) { // 如果success=true,表示提交成功,要清理内存
if (sourceSegments == ramSegmentInfos) {
ramSegmentInfos.removeAllElements();
}
} else { // 如果发生异常,则需要回滚操作
if (sourceSegments == ramSegmentInfos && !anyDeletes) {
if (newSegment != null &&
segmentInfos.size() > 0 &&
segmentInfos.info(segmentInfos.size()-1) == newSegment) {
segmentInfos.remove(segmentInfos.size()-1);
}
} else if (rollback != null) {
segmentInfos.clear();
segmentInfos.addAll(rollback);
}
// Delete any partially created and now unreferenced files:
deleter.refresh();
}
}
} finally {
// 关闭所有的输入流(readers),尝试删除过时的废弃文件
if (doMerge) merger.closeReaders();
}
// 删除RAM中的索引段文件
deleter.deleteDirect(ramDirectory, ramSegmentsToDelete);
// 一个检查点,允许一个IndexFileDeleter deleter有机会在该时间点上去删除文件
deleter.checkpoint(segmentInfos, autoCommit);
if (useCompoundFile && doMerge) { // 如果IndexWriter索引器设置了useCompoundFile=true
boolean success = false;
try {
merger.createCompoundFile(mergedName + ".cfs"); // 创建复合索引文件(.cfs),即_N.cfs文件
newSegment.setUseCompoundFile(true); // 设置SegmentInfo newSegment为复合索引文件的信息
checkpoint(); // 调用该方法 checkpoint()检查,确认并提交更新
success = true;
} finally { // 如果检查过程中发生异常,则回滚
if (!success) {
newSegment.setUseCompoundFile(false);
deleter.refresh();
}
}
// 一个检查点,允许一个IndexFileDeleter deleter有机会在该时间点上去删除文件
deleter.checkpoint(segmentInfos, autoCommit);
}
return mergedDocCount; // 返回需合并的索引段文件数量
}
在不带参数的flushRamSegments()方法中,调用了带参数的flushRamSegments(boolean triggerMerge),也就是说,默认情况下,Lucene指定triggerMerge=true,可以在不带参数的flushRamSegments()方法中看到对该参数的设置:
private final synchronized void flushRamSegments() throws CorruptIndexException, IOException {
flushRamSegments(true);
}
所以,在带参数的flushRamSegments(boolean triggerMerge)方法中,一定会执行maybeMergeSegments()这个合并索引的方法,如下所示:
if (triggerMerge) maybeMergeSegments(minMergeDocs);
这里,传递的参数minMergeDocs=10(Lucene默认值),那么就应该有一个maxMergeDocs的成员与之对应,在Lucene 2.2.0版本中,在IndexWriter类中定义了该maxMergeDocs成员的默认值:
private int maxMergeDocs = DEFAULT_MAX_MERGE_DOCS;
public final static int DEFAULT_MAX_MERGE_DOCS = Integer.MAX_VALUE;
public static final int MAX_VALUE = 0x7fffffff;
maxMergeDocs是合并的最大的Document的数量,定义为最大的Integer。
因为一个索引目录中的索引段文件的数量可能大于minMergeDocs=10,如果也要对所有的索引段文件进行合并,则指定合并最小数量minMergeDocs的Docment是不能满足要求的,即使用mergeSegments()方法。
因此,maybeMergeSegments()就能实现合并性能的改善,它的声明就是需要一个起始的参数,从而进行增量地合并索引段文件。该方法的实现如下所示:
/** Incremental segment merger. */
private final void maybeMergeSegments(int startUpperBound) throws CorruptIndexException, IOException {
long lowerBound = -1;
long upperBound = startUpperBound; // 使用upperBound存放传递进来的startUpperBound
while (upperBound < maxMergeDocs) { // 如果upperBound < maxMergeDocs,一般来说,这个应该总成立的
int minSegment = segmentInfos.size();
// 设置minSegment的值为当前的SegmentInfos segmentInfos 的大小
int maxSegment = -1;
// 查找能够合并的索引段文件
while (--minSegment >= 0) {
// 就是遍历SegmentInfos segmentInfos中的每个SegmentInfo si
SegmentInfo si = segmentInfos.info(minSegment);
// 从索引位置号最大的开始往外取
if (maxSegment == -1 && si.docCount > lowerBound && si.docCount <= upperBound) {
// maxSegment == -1;同时满足-1=lowerBound <(一个索引段文件中Dcoment的数量si.docCount)<=upperBound = startUpperBound
// start from the rightmost* segment whose doc count is in bounds
maxSegment = minSegment;
// 设置maxSegment的值为当前SegmentInfos的大小
} else if (si.docCount > upperBound) {
// 直到segment中Document的数量超过了上限upperBound,则退出循环
break;
}
}
// 该while循环只执行了一次,执行过程中,将maxSegment赋值为segmentInfos.size()-1
minSegment++; // 上面循环中一直执行--minSegment,则到这里minSegment=-1,设置其值为0
maxSegment++; // 因为maxSegment=segmentInfos.size()-1,则设置为maxSegment=segmentInfos.size()
int numSegments = maxSegment - minSegment; // numSegments = maxSegment - minSegment = segmentInfos.size()
if (numSegments < mergeFactor) {
/* mergeFactor是合并因子,IndexWriter的成员,默认设置为10,mergeFactor的值越大,则内存中驻留的Document就越多,向索引目录中写入segment的次数就越少,虽然占用内存较多,但是速度应该很快的。每向索引文件中加入mergeFactor=10个Document的时候,就会在索引目录中生成一个索引段文件(segment) */
break; // numSegments < mergeFactor则没有达到合并所需要的数量,不需要合并,直接退出
} else {
boolean exceedsUpperLimit = false; // 设置一个没有超过上限的boolean型标志(false)
// 能够合并的segments的数量>=mergeFactor时
while (numSegments >= mergeFactor) {
// 调用mergeSegments(即上面的学习到的那个合并的方法)方法,合并从minSegment开始的mergeFactor个segment
int docCount = mergeSegments(segmentInfos, minSegment, minSegment + mergeFactor);
numSegments -= mergeFactor; // mergeFactor个segment已经合并完成,剩下需要合并的数量要减去mergeFactor,在下一次循环的时候继续合并
if (docCount > upperBound) { // 如果上次合并返回的合并后的Document的数量大于上限
// 继续在该层次合并剩余的segment
minSegment++;
exceedsUpperLimit = true;
// 设置已经超过上限,不能再进行深一层次的的合并,即本轮合并就是最深层次的合并了
} else { // 如果上次合并返回的合并后的Document的数量没有超过上限
// 考虑进行更深层次的合并
numSegments++;
}
}
if (!exceedsUpperLimit) {// 如果上次合并返回的合并后的Document的数量大于上限,则终止执行本层次合并
break;
}
}
lowerBound = upperBound;
upperBound *= mergeFactor; // 如果一个层次的合并成功后,还可以进一步合并,则,上限变为原来的10倍
}
}
合并索引段文件就是这样实现的,并非只是在一个层次上合并:
第一层次合并时,每次只能将10个segment索引段文件合并为1个新的segment,假设在这一层生成了500个经过合并以后生成的索引段文件;
第二层次合并时,每次能合并10*mergeFactor=10*10=100个segment,经判断,上一层次生成了500个segment还可以进行第二层次的合并,现在每次100个segment文件才可能合并为1个,可见,只能合并生成5个新的segment;
第三层次合并时,每次能合并10*mergeFactor*mergeFactor=10*10*10=1000个segment,但是上一层次只是生成了5个,不够数量(1000个),不能继续合并了,到此终止。
就是上面的那种原理,实现索引段文件的合并。如果希望进行更深层次的合并,把mergeFactor的值设置的非常小就可以了,但是I/O操作过于频繁,速度会很慢很慢的。
提高合并的速度,是以内存空间开销为代价的。
通过第一个合并的方法可以看出,只有当为一个IndexWriter索引器设置了useCompoundFile=true的时候,才能生成复合索引文件_N.cfs,如下所示:
if (useCompoundFile && doMerge) { // 如果IndexWriter索引器设置了useCompoundFile=true
boolean success = false;
try {
merger.createCompoundFile(mergedName + ".cfs"); // 创建复合索引文件(.cfs),即_N.cfs文件
newSegment.setUseCompoundFile(true); // 设置SegmentInfo newSegment为复合索引文件的信息
checkpoint(); // 调用该方法 checkpoint()检查,确认并提交更新
success = true;
} finally { // 如果检查过程中发生异常,则回滚
if (!success) {
newSegment.setUseCompoundFile(false);
deleter.refresh();
}
}
// 一个检查点,允许一个IndexFileDeleter deleter有机会在该时间点上去删除文件
deleter.checkpoint(segmentInfos, autoCommit);
}
现在知道了,那些_N.cfs文件是合并的索引段文件。
如果在初始化一个IndexWriter索引器的时候,指定 useCompoundFile =false,则在指定的索引目录中生成的索引文件就不是.cfs复合索引文件。
通过这种方式生成的索引文件,它的不同格式表明了它锁存储的关于索引的不同内容。
至少,明确了在建立索引过程中,经过加工处理的数据究竟去向如何,能够加深对Lucene索引过程的理解。
通过在文章
Lucene-2.2.0 源代码阅读学习(4)
中的那个例子,可以运行主函数,观察到索引目录中生成了大量的不同扩展名的索引文件,当然它们不是复合索引文件,如图所示:
这些不同扩展名的索引文件都是有一定的含义的。
如果只是根据这些文件名来说明它的含义,让人感觉很抽象,那么就通过代码来看,它们到底都存储了一些什么内容。
_N.fnm文件
当向一个IndexWriter索引器实例添加Document的时候,调用了IndexWroter的addDocument()方法,在方法的内部调用如下:
buildSingleDocSegment() —> String segmentName = newRamSegmentName();
这时,调用newRamSegmentName()方法生成了一个segment的名称,形如_ram_N,这里N为36进制数。
这个新生成的segmentName作为参数值传递到DocumentWriter类的addDocument()方法中:
dw.addDocument(segmentName, doc);
在DocumentWriter类中,这个segmentName依然是_ram_N形式的,再次作为参数值传递:
fieldInfos.write(directory, segment + ".fnm");
这个时候,就要发生变化了,在FieldInfos类的第一个write()方法中输出System.out.println(name);,结果如下所示:
_ram_0.fnm
_ram_1.fnm
_ram_2.fnm
_ram_3.fnm
_ram_4.fnm
_ram_5.fnm
_ram_6.fnm
_ram_7.fnm
_ram_8.fnm
_ram_9.fnm
_0.fnm
_ram_a.fnm
_ram_b.fnm
_ram_c.fnm
_ram_d.fnm
_ram_e.fnm
_ram_f.fnm
_ram_g.fnm
_ram_h.fnm
_ram_i.fnm
_ram_j.fnm
_1.fnm
_ram_k.fnm
……
而且,可以从Directory看出究竟在这个过程中发生了怎样的切换过程,在FieldInfos类的第一个write()方法中执行:
if(d instanceof FSDirectory){
System.out.println("FSDirectory");
}
else{
System.out.println("----RAMDirectory");
}
输出结果如下所示:
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
FSDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
----RAMDirectory
FSDirectory
……
可以看出,每次处理过10个.fnm文件(文件全名_ram_N.fnm),是在RAMDirectory中,然后就切换到FSDirectory中,这时输出到本地磁盘的索引目录中的索引文件是_N.fnm,可以从上面的实例图中看到_0.fnm、_1.fnm等等。
真正执行向_N.fnm文件中写入内容是在FieldInfos类的第二个write()方法中,可以从该方法的实现来看到底都写入了哪些内容:
public void write(IndexOutput output) throws IOException {
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;
output.writeString(fi.name);
output.writeByte(bits);
}
}
从后两行代码可以看出,首先写入了一个Field的名称(name),然后写入了一个byte值。这个byte的值可以根据从该FieldInfos类定义的一些标志经过位运算得到,从而从FieldIno的实例中读取Field的信息,根据Field的一些信息(如:是否被索引、是否存储词条向量等等)来设置byte bits,这些标志的定义为:
static final byte IS_INDEXED = 0x1;
static final byte STORE_TERMVECTOR = 0x2;
static final byte STORE_POSITIONS_WITH_TERMVECTOR = 0x4;
static final byte STORE_OFFSET_WITH_TERMVECTOR = 0x8;
static final byte OMIT_NORMS = 0x10;
static final byte STORE_PAYLOADS = 0x20;
_N.fdt文件和_N.fdx文件
接着,在DocumentWriter类中的addDocumet()方法中,根据Directory实例、segment的名称、一个FieldInfos的实例构造了一个FieldsWriter类的实例:
FieldsWriter fieldsWriter = new FieldsWriter(directory, segment, fieldInfos);
可以从FieldsWriter类的构造方法可以看出,实际上,根据生成的segment的名称(_ram_N和_N)创建了两个输出流对象:
FieldsWriter(Directory d, String segment, FieldInfos fn) throws IOException {
fieldInfos = fn;
fieldsStream = d.createOutput(segment + ".fdt");
indexStream = d.createOutput(segment + ".fdx");
}
这时,_N.fdt和_N.fdx文件就要生成了。
继续看DocumentWriter类中的addDocument()方法:
fieldsWriter.addDocument(doc);
这时进入到FieldsWriter类中了,在addDocument()方法中提取Field的信息,写入到,_N.fdt和_N.fdx文件中。FieldsWriter类的addDocument()方法实现如下:
final void addDocument(Document doc) throws IOException {
indexStream.writeLong(fieldsStream.getFilePointer()); // 向indexStream中(即_N.fdx文件)中写入fieldsStream(_N.fdt文件)流中的当前位置,也就是写入这个Field信息的位置
int storedCount = 0;
Iterator fieldIterator = doc.getFields().iterator();
while (fieldIterator.hasNext()) { // 循环遍历该Document中所有Field,统计需要存储的Field的个数
Fieldable field = (Fieldable) fieldIterator.next();
if (field.isStored())
storedCount++;
}
fieldsStream.writeVInt(storedCount); // 存储Document中需要存储的的Field的个数,写入到_N.fdt文件
fieldIterator = doc.getFields().iterator();
while (fieldIterator.hasNext()) {
Fieldable field = (Fieldable) fieldIterator.next();
// if the field as an instanceof FieldsReader.FieldForMerge, we're in merge mode
// and field.binaryValue() already returns the compressed value for a field
// with isCompressed()==true, so we disable compression in that case
boolean disableCompression = (field instanceof FieldsReader.FieldForMerge);
if (field.isStored()) { // 如果Field需要存储,将该Field的编号写入到_N.fdt文件
fieldsStream.writeVInt(fieldInfos.fieldNumber(field.name()));
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); // 将Field的是否分词,或是否压缩,或是否以二进制流存储,这些信息都写入到_N.fdt文件
if (field.isCompressed()) {
// 如果当前Field可以被压缩
byte[] data = null;
if (disableCompression) {
// 已经被压缩过,科恩那个需要进行合并优化
data = field.binaryValue();
} else {
// 检查Field是否以二进制存储
if (field.isBinary()) {
data = compress(field.binaryValue());
}
else { // 设置编码方式,压缩存储处理
data = compress(field.stringValue().getBytes("UTF-8"));
}
}
final int len = data.length;
fieldsStream.writeVInt(len); // 写入Field名称(以二进制存储)的长度到_N.fdt文件
fieldsStream.writeBytes(data, len); // 通过字节流的方式,写入Field名称(以二进制存储)到_N.fdt文件
}
else {
// 如果当前这个Field不能进行压缩
if (field.isBinary()) {
byte[] data = field.binaryValue();
final int len = data.length;
fieldsStream.writeVInt(len);
fieldsStream.writeBytes(data, len);
}
else {
fieldsStream.writeString(field.stringValue()); // 如果Field不是以二进制存储,则以String的格式写入到_N.fdt文件
}
}
}
}
}
从该方法可以看出:
_N.fdx文件(即indexStream流)中写入的内容是:一个Field在_N.fdt文件中位置。
_N.fdt文件(即fieldsStream流)中写入的内容是:
(1) Document中需要存储的Field的数量;
(2) 每个Field在Document中的编号;
(3) 每个Field关于是否分词、是否压缩、是否以二进制存储这三个指标的一个组合值;
(4) 每个Field的长度;
(5) 每个Field的内容(binaryValue或stringValue);
_N.frq文件和_N.prx文件
仍然在DocumentWriter类的addDocument()方法中看:
writePostings(postings, segment);
因为在调用该方法之前,已经对Documeng进行了倒排,在倒排的过程中对Document中的Field进行了处理,如果Field指定了要进行分词,则在倒排的时候进行了分词处理,这时生成了词条。然后调用writePostings()方法,根据生成的segment的名称_ram_N,设置词条的频率、位置等信息,并写入到索引目录中。
在writePostings()方法中,首先创建了两个输出流:
freq = directory.createOutput(segment + ".frq");
prox = directory.createOutput(segment + ".prx");
这时,_N.frq文件和_N.prx文件就要在索引目录中生成了。
经过倒排,各个词条的重要信息都被存储到了Posting对象中,Posting类是为词条的信息服务的。因此,在writePostings()方法中可以遍历Posting[]数组中的各个Posting实例,读取并处理这些信息,然后输出到索引目录中。
设置_N.frq文件的起始写入内容:
int postingFreq = posting.freq;
if (postingFreq == 1)
// 如果该词条第一次出现造Document中
freq.writeVInt(1);
// 频率色绘制为1
else {
freq.writeVInt(0); // 如果不是第一次出现,对应的Document的编号0要写入到_N.frq文件
freq.writeVInt(postingFreq); // 设置一个词条在该Document中的频率值
}
再看prox输出流:
if (payloadLength == lastPayloadLength) {
// 其中,int lastPayloadLength = -1;
// the length of the current payload equals the length
// of the previous one. So we do not have to store the length
// again and we only shift the position delta by one bit
prox.writeVInt(delta * 2); //其中,int delta = position - lastPosition,int position = positions[j];
} else {
// the length of the current payload is different from the
// previous one. We shift the position delta, set the lowest
// bit and store the current payload length as VInt.
prox.writeVInt(delta * 2 + 1);
prox.writeVInt(payloadLength);
lastPayloadLength = payloadLength;
}
if (payloadLength > 0) {
// write current payload
prox.writeBytes(payload.data, payload.offset, payload.length);
}
} else {
// field does not store payloads, just write position delta as VInt
prox.writeVInt(delta);
}
一个Posting包含了关于一个词条在一个Document中出现的所有位置(用一个int[]数组来描述)、频率(int)、该词条对应的所有的Payload信息(用Payload[]来描述,因为一个词条具有了频率信息,自然就对应了多个Payload)。
关于Payload可以参考文章
Lucene-2.2.0 源代码阅读学习(23)
。
_N.prx文件文件写入的内容都是与位置相关的数据。
从上面可以看出:
_N.frq文件(即freq流)中写入的内容是:
(1) 一个词条所在的Document的编号;
(2) 每个词条在Document中频率(即:出现的次数);
_N.prx文件(即prox流)中写入的内容是:
其实主要就是Payload的信息,如:一个词条对应的Payload的长度信息、起始偏移量信息;
_N.nrm文件
在DocumentWriter类的addDocument()方法中可以看到调用了writeNorms()方法:
writeNorms(segment);
也是根据生成的segment的名称_ram_N来创建一个输出流,看writeNorms()方法的定义:
private final void writeNorms(String segment) throws IOException {
for(int n = 0; n < fieldInfos.size(); n++){
FieldInfo fi = fieldInfos.fieldInfo(n);
if(fi.isIndexed && !fi.omitNorms){
float norm = fieldBoosts[n] * similarity.lengthNorm(fi.name, fieldLengths[n]);
IndexOutput norms = directory.createOutput(segment + ".f" + n);
try {
norms.writeByte(Similarity.encodeNorm(norm));
} finally {
norms.close();
}
}
}
}
将一些标准化因子的信息,都写入到了_N.nrm文件。其中每个segment对应着一个_N.nrm文件。
关于标准化因子可以参考文章
Lucene-2.2.0 源代码阅读学习(19)
,或者直接参考Apache官方网站
http://lucene.apache.org/java/docs/fileformats.html#Normalization%20Factors
。
关于不同格式的索引文件的内容示例
为了直观,写一个简单的例子:
package org.shirdrn.lucene;
import java.io.IOException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
public class LuceneIndexFormat {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\myindex";
String maven = "Maven is a software project management and comprehension tool.";
String lucene = "Apache Lucene is a search engine written entirely in Java.";
Document doc1 = new Document();
doc1.add(new Field("maven",maven,Field.Store.YES,Field.Index.TOKENIZED));
Document doc2 = new Document();
doc2.add(new Field("lucene",lucene,Field.Store.YES,Field.Index.TOKENIZED));
try {
IndexWriter indexWriter = new IndexWriter(indexPath,new StandardAnalyzer(),true);
indexWriter.setUseCompoundFile(false);
indexWriter.addDocument(doc1);
indexWriter.addDocument(doc2);
indexWriter.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行主函数后,在指定的索引目录下生成了索引文件,而且是同一个索引段,如图所示:
使用UltraEdit-32打开_0.fnm文件,可以看到内容如下所示:
就是我们在程序中设置的,即:
doc.add(new Field("maven",maven,Field.Store.YES,Field.Index.TOKENIZED));
doc.add(new Field("lucene",lucene,Field.Store.YES,Field.Index.TOKENIZED));
就是这两个Field的name。
使用UltraEdit-32打开_0.fdt文件,可以看到内容如下所示:
其实,就是Field的内容。(上面的文本内容实际上存储在一行)
使用UltraEdit-32打开_0.fdx文件,可以看到内容如下所示:
其实,就是在_0.fdt文件中,两个Field的存放位置。
第一个Field是从0位置开始的,第二个是从42(这里是16进制,十进制为66)位置开始的。
使用UltraEdit-32打开_0.nrm文件,可以看到内容如下所示:
这里是标准化因子信息。
(关于标准化因子可以参考文章
Lucene-2.2.0 源代码阅读学习(19)
,或者直接参考Apache官方网站
http://lucene.apache.org/java/docs/fileformats.html#Normalization%20Factors
。)
关于Lucene的检索(IndexSearcher)的内容。
通过一个例子,然后从例子所涉及到的内容出发,一点点地仔细研究每个类的实现和用法。
先写一个简单的使用Lucene实现的能够检索的类,如下所示:
package org.shirdrn.lucene;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermsFilter;
public class MySearcher {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\myindex";
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
String keyword = "的";
Term term = new Term("contents",keyword);
IndexReader indexReader = IndexReader.open(indexPath);
int numberOfDocumentIncludingGivenTerm = indexReader.docFreq(term);
System.out.println("IndexReader的版本为 : "+indexReader.getVersion());
System.out.println("包含词条 ("+term.field()+","+term.text()+") 的Document的数量为 : "+numberOfDocumentIncludingGivenTerm);
Query query = new TermQuery(term);
Date startTime = new Date();
Hits hits = searcher.search(query);
System.out.println("********************************************************************");
int No = 1;
for(int i=0;i<hits.length();i++){
System.out.println("【 序号 】: " + No++);
TermDocs termDocs = searcher.getIndexReader().termDocs(term);
while(termDocs.next()){
if(termDocs.doc() == hits.id(i)){
System.out.println("Document的内部编号为 : "+hits.id(i));
Document doc = hits.doc(i);
List fieldList = doc.getFields();
//System.out.println("==========="+fieldList.size());
System.out.println("Document(编号) "+hits.id(i)+" 的Field的信息: ");
System.out.println(" ------------------------------------");
for(int j=0;j<fieldList.size();j++){
Fieldable field = (Fieldable)fieldList.get(j);
System.out.println(" Field的名称为 : "+field.name());
System.out.println(" Field的内容为 : "+field.stringValue());
System.out.println(" ------------------------------------");
}
System.out.println("Document的内容为 : "+doc);
System.out.println("Document的得分为 : "+hits.score(i));
System.out.println("搜索的该关键字【"+keyword+"】在Document(编号) "+hits.id(i)+" 中,出现过 "+termDocs.freq()+" 次");
}
}
System.out.println("********************************************************************");
}
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
首先要保证索引目录E:\\Lucene\\myindex下面已经存在索引文件,可以通过文章
Lucene-2.2.0 源代码阅读学习(4)
中一个使用Lucene的Demo中的递归建立索引的方法,将建立的索引文件存放到E:\\Lucene\\myindex目录之下。
执行上面的主函数,输出结果如下所示:
IndexReader的版本为 : 1207548172961
包含词条 (contents,的) 的Document的数量为 : 23
********************************************************************
【 序号 】: 1
Document的内部编号为 : 24
Document(编号) 24 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\FAQ.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200604130754
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\FAQ.txt> stored/uncompressed,indexed<modified:200604130754>>
Document的得分为 : 0.5279752
搜索的该关键字【的】在Document(编号) 24 中,出现过 291 次
********************************************************************
【 序号 】: 2
Document的内部编号为 : 5
Document(编号) 5 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\3实验题目.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200710300744
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\3实验题目.txt> stored/uncompressed,indexed<modified:200710300744>>
Document的得分为 : 0.5252467
搜索的该关键字【的】在Document(编号) 5 中,出现过 2 次
********************************************************************
【 序号 】: 3
Document的内部编号为 : 12
Document(编号) 12 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\CustomKeyInfo.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200406041814
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeyInfo.txt> stored/uncompressed,indexed<modified:200406041814>>
Document的得分为 : 0.51790017
搜索的该关键字【的】在Document(编号) 12 中,出现过 70 次
********************************************************************
【 序号 】: 4
Document的内部编号为 : 41
Document(编号) 41 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\Update.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200707050028
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Update.txt> stored/uncompressed,indexed<modified:200707050028>>
Document的得分为 : 0.5059122
搜索的该关键字【的】在Document(编号) 41 中,出现过 171 次
********************************************************************
【 序号 】: 5
Document的内部编号为 : 0
Document(编号) 0 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\120E升级包安装说明.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200803271123
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\120E升级包安装说明.txt> stored/uncompressed,indexed<modified:200803271123>>
Document的得分为 : 0.43770555
搜索的该关键字【的】在Document(编号) 0 中,出现过 2 次
********************************************************************
【 序号 】: 6
Document的内部编号为 : 3
Document(编号) 3 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\1实验题目.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200710160733
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\1实验题目.txt> stored/uncompressed,indexed<modified:200710160733>>
Document的得分为 : 0.4333064
搜索的该关键字【的】在Document(编号) 3 中,出现过 1 次
********************************************************************
【 序号 】: 7
Document的内部编号为 : 60
Document(编号) 60 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\猫吉又有个忙,需要大家帮忙一下.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200706161112
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\猫吉又有个忙,需要大家帮忙一下.txt> stored/uncompressed,indexed<modified:200706161112>>
Document的得分为 : 0.4106042
搜索的该关键字【的】在Document(编号) 60 中,出现过 11 次
********************************************************************
【 序号 】: 8
Document的内部编号为 : 59
Document(编号) 59 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\汉化说明.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200708210247
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\汉化说明.txt> stored/uncompressed,indexed<modified:200708210247>>
Document的得分为 : 0.39057708
搜索的该关键字【的】在Document(编号) 59 中,出现过 13 次
********************************************************************
【 序号 】: 9
Document的内部编号为 : 44
Document(编号) 44 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200801300512
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt> stored/uncompressed,indexed<modified:200801300512>>
Document的得分为 : 0.37525433
搜索的该关键字【的】在Document(编号) 44 中,出现过 3 次
********************************************************************
【 序号 】: 10
Document的内部编号为 : 56
Document(编号) 56 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\新1建 文本文档.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200710311142
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\新1建 文本文档.txt> stored/uncompressed,indexed<modified:200710311142>>
Document的得分为 : 0.36621076
搜索的该关键字【的】在Document(编号) 56 中,出现过 35 次
********************************************************************
【 序号 】: 11
Document的内部编号为 : 46
Document(编号) 46 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\使用技巧集萃.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200511210413
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\使用技巧集萃.txt> stored/uncompressed,indexed<modified:200511210413>>
Document的得分为 : 0.35693806
搜索的该关键字【的】在Document(编号) 46 中,出现过 133 次
********************************************************************
【 序号 】: 12
Document的内部编号为 : 30
Document(编号) 30 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\MyEclipse 注册码.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200712061059
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\MyEclipse 注册码.txt> stored/uncompressed,indexed<modified:200712061059>>
Document的得分为 : 0.3460366
搜索的该关键字【的】在Document(编号) 30 中,出现过 5 次
********************************************************************
【 序号 】: 13
Document的内部编号为 : 63
Document(编号) 63 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\疑问即时记录.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200711141408
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\疑问即时记录.txt> stored/uncompressed,indexed<modified:200711141408>>
Document的得分为 : 0.30325133
搜索的该关键字【的】在Document(编号) 63 中,出现过 6 次
********************************************************************
【 序号 】: 14
Document的内部编号为 : 37
Document(编号) 37 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\readme.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200803101314
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\readme.txt> stored/uncompressed,indexed<modified:200803101314>>
Document的得分为 : 0.26262334
搜索的该关键字【的】在Document(编号) 37 中,出现过 8 次
********************************************************************
【 序号 】: 15
Document的内部编号为 : 48
Document(编号) 48 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\剑心补丁使用说明(readme).txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200803101357
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\剑心补丁使用说明(readme).txt> stored/uncompressed,indexed<modified:200803101357>>
Document的得分为 : 0.26262334
搜索的该关键字【的】在Document(编号) 48 中,出现过 8 次
********************************************************************
【 序号 】: 16
Document的内部编号为 : 47
Document(编号) 47 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\关系记录.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200802201145
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\关系记录.txt> stored/uncompressed,indexed<modified:200802201145>>
Document的得分为 : 0.23161201
搜索的该关键字【的】在Document(编号) 47 中,出现过 14 次
********************************************************************
【 序号 】: 17
Document的内部编号为 : 40
Document(编号) 40 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\Struts之AddressBooks学习笔记.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200710131711
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Struts之AddressBooks学习笔记.txt> stored/uncompressed,indexed<modified:200710131711>>
Document的得分为 : 0.21885277
搜索的该关键字【的】在Document(编号) 40 中,出现过 8 次
********************************************************************
【 序号 】: 18
Document的内部编号为 : 51
Document(编号) 51 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\密码强度检验.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200712010901
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\密码强度检验.txt> stored/uncompressed,indexed<modified:200712010901>>
Document的得分为 : 0.12380183
搜索的该关键字【的】在Document(编号) 51 中,出现过 1 次
********************************************************************
【 序号 】: 19
Document的内部编号为 : 50
Document(编号) 50 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\史上最强天籁之声及欧美流行曲超级精选【 FLAC分轨】.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200712231241
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\史上最强天籁之声及欧美流行曲超级精选【 FLAC分轨】.txt> stored/uncompressed,indexed<modified:200712231241>>
Document的得分为 : 0.1083266
搜索的该关键字【的】在Document(编号) 50 中,出现过 1 次
********************************************************************
【 序号 】: 20
Document的内部编号为 : 57
Document(编号) 57 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\新建 文本文档.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200710270258
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\新建 文本文档.txt> stored/uncompressed,indexed<modified:200710270258>>
Document的得分为 : 0.09285137
搜索的该关键字【的】在Document(编号) 57 中,出现过 4 次
********************************************************************
【 序号 】: 21
Document的内部编号为 : 45
Document(编号) 45 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\书籍网站.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200708071255
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\书籍网站.txt> stored/uncompressed,indexed<modified:200708071255>>
Document的得分为 : 0.0670097
搜索的该关键字【的】在Document(编号) 45 中,出现过 3 次
********************************************************************
【 序号 】: 22
Document的内部编号为 : 61
Document(编号) 61 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\网络查询大全.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200111200655
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\网络查询大全.txt> stored/uncompressed,indexed<modified:200111200655>>
Document的得分为 : 0.065655835
搜索的该关键字【的】在Document(编号) 61 中,出现过 2 次
********************************************************************
【 序号 】: 23
Document的内部编号为 : 14
Document(编号) 14 的Field的信息:
------------------------------------
Field的名称为 : path
Field的内容为 : E:\Lucene\txt1\mytxt\CustomKeysSample.txt
------------------------------------
Field的名称为 : modified
Field的内容为 : 200610100451
------------------------------------
Document的内容为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeysSample.txt> stored/uncompressed,indexed<modified:200610100451>>
Document的得分为 : 0.051179506
搜索的该关键字【的】在Document(编号) 14 中,出现过 7 次
********************************************************************
本次搜索所用的时间为 187 ms
其中,IndexReader是一个用于读取索引文件的抽象类,可以参考该类的源代码。IndexReader类可以很方便地打开一个索引目录(即创建一个输入流),这要使用到它的静态(static)方法open()打开即可,然后就可以访问索引文件了,从而实现对索引文件的维护。
IndexReader类实现了各种打开索引文件的方式,由于定义为static的,所以非常方便地调用,如下所示:
public static IndexReader open(String path) throws CorruptIndexException, IOException { // 通过String的索引目录的路径
return open(FSDirectory.getDirectory(path), true, null);
}
public static IndexReader open(File path) throws CorruptIndexException, IOException { // 通过File构造的索引目录文件
return open(FSDirectory.getDirectory(path), true, null);
}
public static IndexReader open(final Directory directory) throws CorruptIndexException, IOException { // 直接通过Directory来打开
return open(directory, false, null);
}
public static IndexReader open(final Directory directory, IndexDeletionPolicy deletionPolicy) throws CorruptIndexException, IOException { // 直接通过Directory来打开,并指定一种索引文件删除策略,可以对索引文件进行维护(删除操作)
return open(directory, false, deletionPolicy);
}
其中,最核心的实现是在一个私有的open()方法中实现的,如下所示:
private static IndexReader open(final Directory directory, final boolean closeDirectory, final IndexDeletionPolicy deletionPolicy) throws CorruptIndexException, IOException {
return (IndexReader) new SegmentInfos.FindSegmentsFile(directory) {
protected Object doBody(String segmentFileName) throws CorruptIndexException, IOException {
SegmentInfos infos = new SegmentInfos();
infos.read(directory, segmentFileName);
IndexReader reader;
if (infos.size() == 1) {
// index is optimized
reader = SegmentReader.get(infos, infos.info(0), closeDirectory);
} else {
// To reduce the chance of hitting FileNotFound
// (and having to retry), we open segments in
// reverse because IndexWriter merges & deletes
// the newest segments first.
IndexReader[] readers = new IndexReader[infos.size()];
for (int i = infos.size()-1; i >= 0; i--) {
try {
readers[i] = SegmentReader.get(infos.info(i));
} catch (IOException e) {
// Close all readers we had opened:
for(i++;i<infos.size();i++) {
readers[i].close();
}
throw e;
}
}
reader = new MultiReader(directory, infos, closeDirectory, readers);
}
reader.deletionPolicy = deletionPolicy;
return reader;
}
}.run();
}
上面测试程序中,IndexSearcher类是实现检索的核心类。它提供了很多中不同的检索方式,返回的对象也可以适用于不同的需要,比如Hits、TopFieldDocs、TopDocs,而且,还可以指定排序Sort、权重Weight、过滤器Filter作为search()方法的参数,用起来的灵活、方便。
通过程序中,红色标注的代码行:
TermDocs termDocs = searcher.getIndexReader().termDocs(term);
其实,一个IndexSearcher实例化以后,可以通过它获取到一个IndexReader的实例,从而打开一个索引目录。
然后从就可以从创建的输入流中读取索引文件的详细信息:
1、每个Document的内部编号(是唯一的,可以通过这个编号对其进行维护);
2、每个Document中都有多个Field,可以读取Field的名称、路径、Field的内容等等。
上面的测试程序中,没有输出名称为“contents”的Field,是因为在索引文件中没有存储Fielde的内容(即文本信息)。因为Field的内容是根据从指定的数据源中获取,而数据源可能是数据量非常大的一些文件,如果直接将它们保存到索引文件中,会占用很大的磁盘空间。
其实,可以根据需要存储。上面之所以没有存储,可以追溯到Lucene自带的Demo中的设置,在org.apache.lucene.demo.FileDocument中创建Field,如下所示:
// 构造一个Field,这个Field可以从一个文件流中读取,必须保证由f所构造的文件流是打开的
doc.add(new Field("contents", new FileReader(f)));
然后,看Field的该构造方法的定义:
public Field(String name, Reader reader) {
this(name, reader, TermVector.NO);
}
这个构造方法指定了要为这个创建的Field进行分词、索引,但是不存储。
可以参考调用的另一个构造方法:
public Field(String name, Reader reader, TermVector termVector) {
if (name == null)
throw new NullPointerException("name cannot be null");
if (reader == null)
throw new NullPointerException("reader cannot be null");
this.name = name.intern();
// field names are interned
this.fieldsData = reader;
this.isStored = false;
// 指定不进行存储
this.isCompressed = false;
this.isIndexed = true;
// 要进行索引
this.isTokenized = true; // 要进行分词
this.isBinary = false;
setStoreTermVector(termVector);
}
上面测试程序中,Hits是一个内容非常丰富的实现类。
通过检索返回Hits类的对象,可以通过Hits类的实例来获取Document的id,以及计算该Document的得分,并且在search()执行之后,返回的结果按照得分的高低来排序输出,得分值高的在前面。
以此做个引子,之后再详细学习研究。
关于检索的核心IndexSearcher类。
IndexSearcher是Lucene的检索实现的最核心的实现类,它继承自抽象类Searcher,该抽象类中包含了用于检索的一些核心的方法的实现。而Searcher抽象类有实现了Searchable接口,Searchable接口是实现检索的抽象网络协议,可以基于此协议来实现对远程服务器上的索引目录的访问。这一点,可以从Searchable接口所继承的java.rmi.Remote接口来说明。
java.rmi.Remote接口在JDK中给出了说明,如下所示:
也就是说,继承java.rmi.Remote的接口具有的特性是:
1、远程接口用来识别那些继承java.rmi.Remote的接口类,这些接口被非本地虚拟机调用;
2、继承java.rmi.Remote的接口类具有远程可用的特性;
3、实现了java.rmi.Remote接口的子接口的实现类,可以对远程对象进行管理。
下面就对与检索相关的一些接口及一些抽象类做一个概览,有助于后面对这些接口的实现类进行学习研究:
Searchable接口类
Searchable接口的实现如下所示:
package org.apache.lucene.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.CorruptIndexException;
import java.io.IOException;
public interface Searchable extends java.rmi.Remote {
/* 用于检索的核心方法,指定了权重Weight和过滤器Filter参数。因为返回值为void类型,所以实际检索出来的Document都被存放在HitCollector中,该HitCollector类收集了那些得分大于0的Document。*/
void search(Weight weight, Filter filter, HitCollector results)
throws IOException;
// 释放一个IndexSearcher检索器所关联的资源
void close() throws IOException;
// 返回根据指定词条检索出来的Document的数量
int docFreq(Term term) throws IOException;
// 返回根据指定词条数组中所列词条检索出来的Document的数量的一个数组
int[] docFreqs(Term[] terms) throws IOException;
// 返回一个整数值:最大可能的Document的数量 + 1
int maxDoc() throws IOException;
// 检索的方法,返回检索出来的得分(Hits)排在前n位的Document
TopDocs search(Weight weight, Filter filter, int n) throws IOException;
// 获取编号为i的Document,(注意:是内部编号,可以在上面测试程序中执行System.out.println(searcher.doc(24));,打印出结果为Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\FAQ.txt> stored/uncompressed,indexed<modified:200604130754>>)
Document doc(int i) throws CorruptIndexException, IOException;
// 获取在位置n上的Document;FieldSelector接口类似于一个文件过滤器,它有一个方法FieldSelectorResult accept(String fieldName);
Document doc(int n, FieldSelector fieldSelector) throws CorruptIndexException, IOException;
// 重新设置Query(即,重写先前设定的Query)
Query rewrite(Query query) throws IOException;
// 返回一个Explanation,该Explanation用于计算得分
Explanation explain(Weight weight, int doc) throws IOException;
// 指定一种排序方式,在此基础上返回得分在前n位的Document
TopFieldDocs search(Weight weight, Filter filter, int n, Sort sort)
throws IOException;
}
Searcher抽象类
package org.apache.lucene.search;
import java.io.IOException;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.document.Document;
// 该抽象类实现了Searchable接口
public abstract class Searcher implements Searchable {
// 查询与指定Query匹配的Document,返回Hits实例,该Hits内容相当丰富
public final Hits search(Query query) throws IOException {
return search(query, (Filter)null);
// 调用下面的search()方法
}
public Hits search(Query query, Filter filter) throws IOException {
return new Hits(this, query, filter);
}
// 指定了Sort
public Hits search(Query query, Sort sort)
throws IOException {
return new Hits(this, query, null, sort);
}
// 指定了Filter和Sort
public Hits search(Query query, Filter filter, Sort sort)
throws IOException {
return new Hits(this, query, filter, sort);
}
// 实现了Searchable接口中方法,指定一种排序方式,在此基础上返回得分在前n位的Document
public TopFieldDocs search(Query query, Filter filter, int n,
Sort sort) throws IOException {
return search(createWeight(query), filter, n, sort);
// 调用abstract public TopDocs search(Weight weight, Filter filter, int n) throws IOException;
}
public void search(Query query, HitCollector results)
throws IOException {
search(query, (Filter)null, results);
}
public void search(Query query, Filter filter, HitCollector results)
throws IOException {
search(createWeight(query), filter, results);
}
public TopDocs search(Query query, Filter filter, int n)
throws IOException {
return search(createWeight(query), filter, n);
}
public Explanation explain(Query query, int doc) throws IOException {
return explain(createWeight(query), doc);
}
// 为一个Searcher设置一个Similarity
public void setSimilarity(Similarity similarity) {
this.similarity = similarity;
}
public Similarity getSimilarity() {
return this.similarity;
}
// 根据指定的Query,创建一个用于记录该Query状态的Weight
protected Weight createWeight(Query query) throws IOException {
return query.weight(this);
}
// 实现了接口Searchable中的方法
public int[] docFreqs(Term[] terms) throws IOException {
int[] result = new int[terms.length];
for (int i = 0; i < terms.length; i++) {
result[i] = docFreq(terms[i]);
}
return result;
}
// 一些abstract方法,在接口Searchable中列举过
abstract public void search(Weight weight, Filter filter, HitCollector results) throws IOException;
abstract public void close() throws IOException;
abstract public int docFreq(Term term) throws IOException;
abstract public int maxDoc() throws IOException;
abstract public TopDocs search(Weight weight, Filter filter, int n) throws IOException;
abstract public Document doc(int i) throws CorruptIndexException, IOException;
abstract public Query rewrite(Query query) throws IOException;
abstract public Explanation explain(Weight weight, int doc) throws IOException;
abstract public TopFieldDocs search(Weight weight, Filter filter, int n, Sort sort) throws IOException;
}
Weight接口类
创建一个Weight的目的是,使得一个已经定制的Query实例不在检索过程中被修改,以至于该Query实例可以被重用,而无需重复创建。
一个Query实例是独立于IndexSearcher检索器的。Query的这种独立的状态应该被记录在一个Weight中。
Weight接口的源代码如下所示:
package org.apache.lucene.search;
import java.io.IOException;
import org.apache.lucene.index.IndexReader;
public interface Weight extends java.io.Serializable {
// 获取该Weight所关联的Query实例
Query getQuery();
// 获取一个Query的Weight值
float getValue();
/** The sum of squared weights of contained query clauses. */
float sumOfSquaredWeights() throws IOException;
// 为一个Query设置标准化因子
void normalize(float norm);
// 为一个Weight创建一个Scorer(Scorer是与Document的得分相关的)
Scorer scorer(IndexReader reader) throws IOException;
// 为编号为i的Document计算得分,返回Explanation记录了该Document的得分
Explanation explain(IndexReader reader, int doc) throws IOException;
}
HitCollector抽象类
package org.apache.lucene.search;
// 抽象类用于收集检索出来的Document
public abstract class HitCollector {
// 根据Document的编号和得分,筛选符合条件的Document
public abstract void collect(int doc, float score);
}
Scorer抽象类
package org.apache.lucene.search;
import java.io.IOException;
// 用于管理与查询Query匹配的Document的得分
public abstract class Scorer {
private Similarity similarity;
// Constructs a Scorer.
protected Scorer(Similarity similarity) {
this.similarity = similarity;
}
public Similarity getSimilarity() {
return this.similarity;
}
// 遍历HitCollector,收集所有匹配的Document
public void score(HitCollector hc) throws IOException {
while (next()) {
hc.collect(doc(), score());
}
}
// 在指定范围内(编号<max的Document)收集匹配的Document
protected boolean score(HitCollector hc, int max) throws IOException {
while (doc() < max) {
hc.collect(doc(), score());
if (!next())
return false;
}
return true;
}
/** Advances to the next document matching the query. */
public abstract boolean next() throws IOException;
// 获取当前Document的编号
public abstract int doc();
// 获取当前匹配的Document的得分
public abstract float score() throws IOException;
/** Skips to the first match beyond the current whose document number is
* greater than or equal to a given target.
* <br>When this method is used the
{@link
#explain(int)} method should not be used.
* @param target The target document number.
* @return true iff there is such a match.
* <p>Behaves as if written: <pre>
* boolean skipTo(int target) {
* do {
* if (!next())
* return false;
* } while (target > doc());
* return true;
* }
* </pre>Most implementations are considerably more efficient than that.
*/
public abstract boolean skipTo(int target) throws IOException;
public abstract Explanation explain(int doc) throws IOException;
}
Similarity抽象类
关于该抽象类的说明,可以参考源代码说明,如下所示:
org.apache.lucene.search.Similarity
Expert: Scoring API.
Subclasses implement search scoring.
The score of query q
for document d
correlates to the cosine-distance or dot-product between document and query vectors in a
Vector Space Model (VSM) of Information Retrieval
. A document whose vector is closer to the query vector in that model is scored higher. The score is computed as follows:
where
-
tf(t in d)
correlates to the term's frequency, defined as the number of times term t appears in the currently scored document d. Documents that have more occurrences of a given term receive a higher score. The default computation for tf(t in d) in DefaultSimilarity is:
-
idf(t)
stands for Inverse Document Frequency. This value correlates to the inverse of docFreq (the number of documents in which the term t appears). This means rarer terms give higher contribution to the total score. The default computation for idf(t) in DefaultSimilarity is:
idf(t) =
|
1 + log (
|
numDocs
|
–––––––––
|
docFreq+1
|
|
)
|
-
coord(q,d)
is a score factor based on how many of the query terms are found in the specified document. Typically, a document that contains more of the query's terms will receive a higher score than another document with fewer query terms. This is a search time factor computed in coord(q,d) by the Similarity in effect at search time.
-
queryNorm(q)
is a normalizing factor used to make scores between queries comparable. This factor does not affect document ranking (since all ranked documents are multiplied by the same factor), but rather just attempts to make scores from different queries (or even different indexes) comparable. This is a search time factor computed by the Similarity in effect at search time. The default computation in DefaultSimilarity is:
queryNorm(q) = queryNorm(sumOfSquaredWeights) =
|
1
|
––––––––––––––
|
sumOfSquaredWeights½
|
|
The sum of squared weights (of the query terms) is computed by the query org.apache.lucene.search.Weight object. For example, a boolean query computes this value as:
-
t.getBoost()
is a search time boost of term t in the query q as specified in the query text (see
query syntax
), or as set by application calls to setBoost(). Notice that there is really no direct API for accessing a boost of one term in a multi term query, but rather multi terms are represented in a query as multi TermQuery objects, and so the boost of a term in the query is accessible by calling the sub-query getBoost().
-
norm(t,d)
encapsulates a few (indexing time) boost and length factors:
-
Document boost
- set by calling doc.setBoost() before adding the document to the index.
-
Field boost
- set by calling field.setBoost() before adding the field to a document.
-
lengthNorm(field) - computed when the document is added to the index in accordance with the number of tokens of this field in the document, so that shorter fields contribute more to the score. LengthNorm is computed by the Similarity class in effect at indexing.
When a document is added to the index, all the above factors are multiplied. If the document has multiple fields with the same name, all their boosts are multiplied together:
norm(t,d) = doc.getBoost() · lengthNorm(field) ·
|
∏
|
f.getBoost()
|
|
field f in d named as t
|
|
However the resulted norm value is encoded as a single byte before being stored. At search time, the norm byte value is read from the index directory and decoded back to a float norm value. This encoding/decoding, while reducing index size, comes with the price of precision loss - it is not guaranteed that decode(encode(x)) = x. For instance, decode(encode(0.89)) = 0.75. Also notice that search time is too late to modify this norm part of scoring, e.g. by using a different Similarity for search.
-
See Also:
-
setDefault(Similarity)
-
org.apache.lucene.index.IndexWriter.setSimilarity(Similarity)
-
Searcher.setSimilarity(Similarity)
该抽象类的源代码如下所示:
package org.apache.lucene.search;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.util.SmallFloat;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
public abstract class Similarity implements Serializable {
// DefaultSimilarity是Similarity的子类
private static Similarity defaultImpl = new DefaultSimilarity();
public static void setDefault(Similarity similarity) {
Similarity.defaultImpl = similarity;
}
public static Similarity getDefault() {
return Similarity.defaultImpl;
}
// 标准化因子列表
private static final float[] NORM_TABLE = new float[256];
static {
// 静态加载
for (int i = 0; i < 256; i++)
NORM_TABLE[i] = SmallFloat.byte315ToFloat((byte)i);
// 将Cache中的字节转化成浮点数
}
// 解码标准化因子(从byte变为float)
public static float decodeNorm(byte b) {
return NORM_TABLE[b & 0xFF]; // & 0xFF maps negative bytes to positive above 127
}
// 获取解码标准化因子列表
public static float[] getNormDecoder() {
return NORM_TABLE;
}
// 指定了名称为fieldName的Field,以及该Field中包含的词条的数量numTokens,计算该Field的标准化因子长度
public abstract float lengthNorm(String fieldName, int numTokens);
// 给定了一个Query的每个词条的Weight的平方值,计算一个Query的标准化因子
public abstract float queryNorm(float sumOfSquaredWeights);
// 为一个索引中存储的标准化因子解码(从float到byte)
public static byte encodeNorm(float f) {
return SmallFloat.floatToByte315(f);
}
// 计算一个Document中的词条的得分因子
public float tf(int freq) {
return tf((float)freq);
}
/** Computes the amount of a sloppy phrase match, based on an edit distance.
* This value is summed for each sloppy phrase match in a document to form
* the frequency that is passed to
{@link
#tf(float)}.
*
* <p>A phrase match with a small edit distance to a document passage more
* closely matches the document, so implementations of this method usually
* return larger values when the edit distance is small and smaller values
* when it is large.
*
* @see PhraseQuery#setSlop(int)
* @param distance the edit distance of this sloppy phrase match
* @return the frequency increment for this match
*/
public abstract float sloppyFreq(int distance);
/** Computes a score factor based on a term or phrase's frequency in a
* document. This value is multiplied by the
{@link
#idf(Term, Searcher)}
* factor for each term in the query and these products are then summed to
* form the initial score for a document.
*
* <p>Terms and phrases repeated in a document indicate the topic of the
* document, so implementations of this method usually return larger values
* when <code>freq</code> is large, and smaller values when <code>freq</code>
* is small.
*
* @param freq the frequency of a term within a document
* @return a score factor based on a term's within-document frequency
*/
public abstract float tf(float freq);
/** Computes a score factor for a simple term.
*
* <p>The default implementation is:<pre>
* return idf(searcher.docFreq(term), searcher.maxDoc());
* </pre>
*
* Note that
{@link
Searcher#maxDoc()} is used instead of
*
{@link
org.apache.lucene.index.IndexReader#numDocs()} because it is proportional to
*
{@link
Searcher#docFreq(Term)} , i.e., when one is inaccurate,
* so is the other, and in the same direction.
*
* @param term the term in question
* @param searcher the document collection being searched
* @return a score factor for the term
*/
public float idf(Term term, Searcher searcher) throws IOException {
return idf(searcher.docFreq(term), searcher.maxDoc());
}
// 为一个短语计算得分因子
public float idf(Collection terms, Searcher searcher) throws IOException {
float idf = 0.0f;
Iterator i = terms.iterator();
while (i.hasNext()) {
idf += idf((Term)i.next(), searcher);
}
return idf;
}
/** Computes a score factor based on a term's document frequency (the number
* of documents which contain the term). This value is multiplied by the
*
{@link
#tf(int)} factor for each term in the query and these products are
* then summed to form the initial score for a document.
*/
public abstract float idf(int docFreq, int numDocs);
/** Computes a score factor based on the fraction of all query terms that a
* document contains. This value is multiplied into scores.
*/
public abstract float coord(int overlap, int maxOverlap);
/**
* Calculate a scoring factor based on the data in the payload. Overriding implementations
* are responsible for interpreting what is in the payload. Lucene makes no assumptions about
* what is in the byte array.
*/
public float scorePayload(byte [] payload, int offset, int length)
{
//Do nothing
return 1;
}
}
关于IndexSearcher检索器。
在学习IndexSearcher检索器之前,先大致了解一下下面几项:
1、首先,要知道Weight(接口)存在的目的:
使得检索不改变一个Query,使得Query可以重用。所以就出现了Weight,一个Weight可以保存与某次检索相关的IndexSearcher检索器的独立状态值。其实Weight间接保存了IndexSearcher索引器的独立状态信息。
每次检索,即初始化一个IndexSearcher检索器,都需要一个Query,例如
Query query = new TermQuery(term);
Hits hits = searcher.search(query);
而Query抽象了用户的检索意向信息,可以使用Query的public Query rewrite(IndexReader reader)方法来实现对先前的检索意向信息的修改(重写)。
用户的一次检索,是与一个Weight对应的,当然可以不保存本次检索相关的IndexSearcher检索器的状态信息到一个Weight中,这样的坏处就是Query不能重用,每次都要重新实例化一个。
Weight接口定义了如下的内容:
public interface Weight extends java.io.Serializable {
Query getQuery(); // 通过一个Weight可以获取到一个Query实例
float getValue(); // Weight相关的Query的权重值
float sumOfSquaredWeights() throws IOException;
// 一个Query可以有很多子句(比如一个BooleanQuery可以包含多个TermQuery子句),获取到所有子句的权重值的平方
void normalize(float norm);
// 指派查询的标准化因子
Scorer scorer(IndexReader reader) throws IOException; // 根据一个IndexReader,通过Weight获取得分
Explanation explain(IndexReader reader, int doc) throws IOException; // 为编号为doc的Document设置计算得分的描述信息
}
2、其次,知道Sort类是为一次检索设定排序方式的。
这些排序的方式是在SortField类中定义的,一共定义了7种,当然包括客户化定制排序方式。
3、再次,知道Explanation类是关于某次检索中,封装了对某个Document的得分计算的描述。
4、接着,知道TopDocs类是关于某次实际的检索出来结果集的信息,包括Hits数量,及其最大得分的信息。TopDocs的子类TopFieldDocs类指定了排序方式(Sort),为Fields进行排序。
5、然后,知道FieldSelector是一个筛选器接口,将某个Document中的满足接受条件的Field返回。在FieldSelector中定义了FieldSelectorResult accept(String fieldName);方法。
6、最后,理解TopDocCollector类的用于IndexSearcher的目的。其实TopDocCollector内部定义了一个collect()方法,该方法可以实现根据Document的得分来排序。TopDocCollector类继承自HitCollector,而HitCollector抽象类定义了实现查询(queries)、排序(sorting)、过滤(filtering)的功能。
现在,可以通过IndexSearcher索引器的源代码来解读它具有哪些功能。其实已经很容易读了,在理解上面6项的基础上。IndexSearcher的源代码实现如下所示:
package org.apache.lucene.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import java.io.IOException;
import java.util.BitSet;
// IndexSearcher继承自Searcher抽象类,在Searcher抽象类中定义了一些search()方法,返回Hits。
public class IndexSearcher extends Searcher {
IndexReader reader;
private boolean closeReader;
// 实例化一个IndexSearcher检索器
public IndexSearcher(String path) throws CorruptIndexException, IOException {
this(IndexReader.open(path), true);
}
public IndexSearcher(Directory directory) throws CorruptIndexException, IOException {
this(IndexReader.open(directory), true);
}
public IndexSearcher(IndexReader r) {
this(r, false);
}
private IndexSearcher(IndexReader r, boolean closeReader) {
reader = r;
this.closeReader = closeReader;
}
public IndexReader getIndexReader() {
return reader;
}
// 一个检索器与一个IndexReader是密切相关的
public void close() throws IOException {
if(closeReader)
reader.close();
}
// 获取包含词条term的Document的数量
public int docFreq(Term term) throws IOException {
return reader.docFreq(term);
}
// 获取编号为i的Document
public Document doc(int i) throws CorruptIndexException, IOException {
return reader.document(i);
}
// 指定了一个筛选器FieldSelector(该筛选器要接受满足条件的某个Document中的Field,将不满足的过滤掉)
public Document doc(int i, FieldSelector fieldSelector) throws CorruptIndexException, IOException {
return reader.document(i, fieldSelector);
}
// 检索得到的最大可能的Document的数量 + 1
public int maxDoc() throws IOException {
return reader.maxDoc();
}
// 查询的核心方法,返回TopDocs,参数指定Weight、Filter、返回Document的数量
public TopDocs search(Weight weight, Filter filter, final int nDocs)
throws IOException {
if (nDocs <= 0)
throw new IllegalArgumentException("nDocs must be > 0");
TopDocCollector collector = new TopDocCollector(nDocs);
search(weight, filter, collector);
return collector.topDocs();
}
// 查询的方法,返回TopFieldDocs
public TopFieldDocs search(Weight weight, Filter filter, final int nDocs,
Sort sort)
throws IOException {
TopFieldDocCollector collector =
new TopFieldDocCollector(reader, sort, nDocs);
search(weight, filter, collector);
return (TopFieldDocs)collector.topDocs();
}
// 返回值是void,实际检索的结果集存放在HitCollector中
public void search(Weight weight, Filter filter,
final HitCollector results) throws IOException {
HitCollector collector = results;
if (filter != null) {
// Filter不为null的时候才执行下面代码
final BitSet bits = filter.bits(reader);
collector = new HitCollector() {
public final void collect(int doc, float score) {
if (bits.get(doc)) {
results.collect(doc, score);
}
}
};
}
Scorer scorer = weight.scorer(reader);
if (scorer == null)
return;
scorer.score(collector);
}
// 在先前创建Query并执行检索的基础上,重新改写这个Query,而不是重新实例化一个Query
public Query rewrite(Query original) throws IOException {
Query query = original;
for (Query rewrittenQuery = query.rewrite(reader); rewrittenQuery != query;
rewrittenQuery = query.rewrite(reader)) {
query = rewrittenQuery;
}
return query;
}
public Explanation explain(Weight weight, int doc) throws IOException {
return weight.explain(reader, doc);
}
}
在检索的时候,首先就是要实例化一个IndexSearcher检索器,而这个过程其实就是使用IndexReader打开一个索引目录。
然后通过提交的Query,就可以使用IndexSearcher的search()方法进行检索了。
从IndexSearcher的源代码来看,每个search()方法都需要一个Query实例。因为只有用户提交查询(根据提交的关键字构造一个Query),才能执行检索。也就是说,在检索中Query是非常重要的。实际上Query对于检索的实现具有很大的灵活性,主要是通过Query抽象类的炉体子类的实现来体现的。
关于Query的学习。
主要使用TermQuery和BooleanQuery,它们是最最基础的Query。
我感觉Query的灵活性太大了,这就使得它那么地具有魅力。
当用户提交了检索关键字以后,首先就是要根据这个关键字进行分析,因为不同的用户提交的关键词具有不同的特点,所以使用不同方式来构造Query是极其关键的,从而使提供的检索服务最大程度地满足用户的意愿。
先看看Query抽象类的继承关系,如图所示:
最简单最基础的就是构造一个TermQuery,根据词条本身直接来构造一个Query,从而进行检索。
Query类抽象类了进行检索所具有的共同特征,源代码实现如下所示:
package org.apache.lucene.search;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.lucene.index.IndexReader;
public abstract class Query implements java.io.Serializable, Cloneable {
// boost是一个非常重要的属性,它体现了检索到的Document的重要程度,Lucene默认值为1.0,当然可以自行设置
private float boost = 1.0f;
public void setBoost(float b) { boost = b; }
public float getBoost() { return boost; }
public abstract String toString(String field);
public String toString() {
return toString("");
}
// 一个Query与一个Weight相关
protected Weight createWeight(Searcher searcher) throws IOException {
throw new UnsupportedOperationException();
}
public Weight weight(Searcher searcher)
throws IOException {
Query query = searcher.rewrite(this);
Weight weight = query.createWeight(searcher);
float sum = weight.sumOfSquaredWeights();
float norm = getSimilarity(searcher).queryNorm(sum);
weight.normalize(norm);
return weight;
}
// 重写一个Query
public Query rewrite(IndexReader reader) throws IOException {
return this;
}
// 该方法主要是为复杂查询建立的,通过多个Query的合并来实现,比如,一个BooleanQuery可以是几个TermQuery的合并
public Query combine(Query[] queries) {
HashSet uniques = new HashSet();
for (int i = 0; i < queries.length; i++) {
Query query = queries[i];
BooleanClause[] clauses = null;
// 是否需要将一个Query分割成多个查询子句的标志
boolean splittable = (query instanceof BooleanQuery);
// 可见,这里是对BooleanQuery而言的
if(splittable){
BooleanQuery bq = (BooleanQuery) query;
splittable = bq.isCoordDisabled();
clauses = bq.getClauses();
for (int j = 0; splittable && j < clauses.length; j++) {
splittable = (clauses[j].getOccur() == BooleanClause.Occur.SHOULD);
}
}
if(splittable){
for (int j = 0; j < clauses.length; j++) {
uniques.add(clauses[j].getQuery());
}
} else {
uniques.add(query);
}
}
// 如果只有一个查询子句,直接返回
if(uniques.size() == 1){
return (Query)uniques.iterator().next();
}
Iterator it = uniques.iterator();
BooleanQuery result = new BooleanQuery(true);
while (it.hasNext())
result.add((Query) it.next(), BooleanClause.Occur.SHOULD);
return result;
}
// 从构造的Query中提取出与该查询关联的词条,即用户键入的检索关键字构造的词条
public void extractTerms(Set terms) {
// needs to be implemented by query subclasses
throw new UnsupportedOperationException();
}
// 合并多个BooleanQuery,构造复杂查询
public static Query mergeBooleanQueries(Query[] queries) {
HashSet allClauses = new HashSet();
for (int i = 0; i < queries.length; i++) {
BooleanClause[] clauses = ((BooleanQuery)queries[i]).getClauses();
for (int j = 0; j < clauses.length; j++) {
allClauses.add(clauses[j]);
}
}
boolean coordDisabled =
queries.length==0? false : ((BooleanQuery)queries[0]).isCoordDisabled();
BooleanQuery result = new BooleanQuery(coordDisabled);
Iterator i = allClauses.iterator();
while (i.hasNext()) {
result.add((BooleanClause)i.next());
}
return result;
}
// 获取与查询相关的Similarity(相似度)实例
public Similarity getSimilarity(Searcher searcher) {
return searcher.getSimilarity();
}
// Query是支持克隆的
public Object clone() {
try {
return (Query)super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Clone not supported: " + e.getMessage());
}
}
}
上面出现了BooleanQuery,从字面可以了解到这种Query是基于逻辑运算的。BooleanQuery可以是多个子句的逻辑运算。从BooleanQuery的代码中可以看到,它支持子句的最大数量为1024:
private static int maxClauseCount = 1024;
但是,并非越多子句参与逻辑运算就越好,这里有个效率问题,因为多个子句的合并,要通过各自的Query之后,然后再进行这种逻辑运算,有时时间开销是不可取的。
BooleanClause是在一个BooleanQuery中子句。该类中定义了一个静态最终内部类Occur定义了BooleanQuery的运算符:
public static final Occur MUST = new Occur("MUST");
// 与运算
public static final Occur SHOULD = new Occur("SHOULD");
// 或运算
public static final Occur MUST_NOT = new Occur("MUST_NOT"); // 非运算
可以通过上面三个算子对Query进行合并,实现复杂的查询操作。
编写一个例子,通过构造三个TermQuery,将他们添加(add)到一个BooleanQuery查询中,使用MUST与运算,如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import java.util.Iterator;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Hit;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
public class BloeanQuerySearcher {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\myindex";
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
String keywordA = "的";
Term termA = new Term("contents",keywordA);
Query tQueryA = new TermQuery(termA);
String keywordB = "是";
Term termB = new Term("contents",keywordB);
Query tQueryB = new TermQuery(termB);
String keywordC = "在";
Term termC = new Term("contents",keywordC);
Query tQueryC = new TermQuery(termC);
Term[] arrayTerm = new Term[]{null,null,null};
arrayTerm[0] = termA;
arrayTerm[1] = termB;
arrayTerm[2] = termC;
BooleanQuery bQuery = new BooleanQuery();
bQuery.add(tQueryA,BooleanClause.Occur.MUST);
bQuery.add(tQueryB,BooleanClause.Occur.MUST);
bQuery.add(tQueryC,BooleanClause.Occur.MUST);
Date startTime = new Date();
Hits hits = searcher.search(bQuery);
Iterator it = hits.iterator();
System.out.println("********************************************************************");
while(it.hasNext()){
Hit hit = (Hit)it.next();
System.out.println("Hit的ID 为 : "+hit.getId());
System.out.println("Hit的score 为 : "+hit.getScore());
System.out.println("Hit的boost 为 : "+hit.getBoost());
System.out.println("Hit的toString 为 : "+hit.toString());
System.out.println("Hit的Dcoment 为 : "+hit.getDocument());
System.out.println("Hit的Dcoment 的 Fields 为 : "+hit.getDocument().getFields());
for(int i=0;i<hit.getDocument().getFields().size();i++){
Field field = (Field)hit.getDocument().getFields().get(i);
System.out.println(" -------------------------------------------------------------");
System.out.println(" Field的Name为 : "+field.name());
System.out.println(" Field的stringValue为 : "+field.stringValue());
}
System.out.println("********************************************************************");
}
System.out.println("包含3个词条的Hits长度为 : "+hits.length());
for(int i=0;i<searcher.docFreqs(arrayTerm).length;i++){
System.out.println("包含3个词条的Document的数量为 : "+searcher.docFreqs(arrayTerm)[i]);
}
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
下面对各种逻辑运算的组合看一下效果:
1、第一种组合:
bQuery.add(tQueryA,BooleanClause.Occur.MUST);
bQuery.add(tQueryB,BooleanClause.Occur.MUST);
bQuery.add(tQueryC,BooleanClause.Occur.MUST);
查询结果即是,同时包含三个词条(词条的文本内容为:的、在、是)的所有文档被检索出来。可以猜想一下,上面撒个词条:“的”、“是”、“在”在汉语中应该是出现频率非常高的,预期结果应该会查询出来较多的符合下面与运算条件的结果:
bQuery.add(tQueryA,BooleanClause.Occur.MUST);
bQuery.add(tQueryB,BooleanClause.Occur.MUST);
bQuery.add(tQueryC,BooleanClause.Occur.MUST);
实际运行结果如下所示:
********************************************************************
Hit的ID 为 : 12
Hit的score 为 : 0.63582987
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[0] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeyInfo.txt> stored/uncompressed,indexed<modified:200406041814>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeyInfo.txt>, stored/uncompressed,indexed<modified:200406041814>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\CustomKeyInfo.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200406041814
********************************************************************
Hit的ID 为 : 24
Hit的score 为 : 0.6183762
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[1] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\FAQ.txt> stored/uncompressed,indexed<modified:200604130754>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\FAQ.txt>, stored/uncompressed,indexed<modified:200604130754>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\FAQ.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200604130754
********************************************************************
Hit的ID 为 : 63
Hit的score 为 : 0.53687334
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[2] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\疑问即时记录.txt> stored/uncompressed,indexed<modified:200711141408>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\疑问即时记录.txt>, stored/uncompressed,indexed<modified:200711141408>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\疑问即时记录.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200711141408
********************************************************************
Hit的ID 为 : 60
Hit的score 为 : 0.50429535
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[3] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\猫吉又有个忙,需要大家帮忙一下.txt> stored/uncompressed,indexed<modified:200706161112>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\猫吉又有个忙,需要大家帮忙一下.txt>, stored/uncompressed,indexed<modified:200706161112>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\猫吉又有个忙,需要大家帮忙一下.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200706161112
********************************************************************
Hit的ID 为 : 46
Hit的score 为 : 0.4266696
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[4] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\使用技巧集萃.txt> stored/uncompressed,indexed<modified:200511210413>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\使用技巧集萃.txt>, stored/uncompressed,indexed<modified:200511210413>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\使用技巧集萃.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200511210413
********************************************************************
Hit的ID 为 : 56
Hit的score 为 : 0.4056765
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[5] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\新1建 文本文档.txt> stored/uncompressed,indexed<modified:200710311142>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\新1建 文本文档.txt>, stored/uncompressed,indexed<modified:200710311142>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\新1建 文本文档.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200710311142
********************************************************************
Hit的ID 为 : 41
Hit的score 为 : 0.3852732
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[6] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Update.txt> stored/uncompressed,indexed<modified:200707050028>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Update.txt>, stored/uncompressed,indexed<modified:200707050028>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\Update.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200707050028
********************************************************************
Hit的ID 为 : 37
Hit的score 为 : 0.35885736
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[7] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\readme.txt> stored/uncompressed,indexed<modified:200803101314>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\readme.txt>, stored/uncompressed,indexed<modified:200803101314>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\readme.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200803101314
********************************************************************
Hit的ID 为 : 48
Hit的score 为 : 0.35885736
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[8] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\剑心补丁使用说明(readme).txt> stored/uncompressed,indexed<modified:200803101357>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\剑心补丁使用说明(readme).txt>, stored/uncompressed,indexed<modified:200803101357>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\剑心补丁使用说明(readme).txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200803101357
********************************************************************
Hit的ID 为 : 47
Hit的score 为 : 0.32808846
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[9] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\关系记录.txt> stored/uncompressed,indexed<modified:200802201145>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\关系记录.txt>, stored/uncompressed,indexed<modified:200802201145>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\关系记录.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200802201145
********************************************************************
包含3个词条的Hits长度为 : 10
包含3个词条的Document的数量为 : 23
包含3个词条的Document的数量为 : 12
包含3个词条的Document的数量为 : 14
本次搜索所用的时间为 203 ms
从上面测试可见,查询的记过具有10项,应该符合我们预期的假设的。
2、第二种组合:
bQuery.add(tQueryA,BooleanClause.Occur.MUST_NOT);
bQuery.add(tQueryB,BooleanClause.Occur.MUST);
bQuery.add(tQueryC,BooleanClause.Occur.MUST);
即,把不包含词条“的”,但是同时包含词条“是”和“在”,查询出来的结果应该不会太多,中文的文章中“的”出现的频率很高很高,上面指定了MUST_NOT,非逻辑运算符,结果如下所示:
********************************************************************
Hit的ID 为 : 54
Hit的score 为 : 0.22303896
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[0] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\指定时间内关闭网页.txt> stored/uncompressed,indexed<modified:200111200742>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\指定时间内关闭网页.txt>, stored/uncompressed,indexed<modified:200111200742>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\指定时间内关闭网页.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200111200742
********************************************************************
3个词条的Hits长度为 : 1
包含3个词条的Document的数量为 : 23
包含3个词条的Document的数量为 : 12
包含3个词条的Document的数量为 : 14
本次搜索所用的时间为 140 ms
符合查询条件的只有一项,只有记事本文件E:\Lucene\txt1\mytxt\指定时间内关闭网页.txt中满足查询条件,即该文件中一定没有词条“的”出现,但是同时包含词条“是”和“在”。
3、第三种组合:
bQuery.add(tQueryA,BooleanClause.Occur.SHOULD);
bQuery.add(tQueryB,BooleanClause.Occur.SHOULD);
bQuery.add(tQueryC,BooleanClause.Occur.SHOULD);
即或操作,可想而知,满足条件的结果项应该是最多的(我的测试是24项)。
4、第四种组合:
bQuery.add(tQueryA,BooleanClause.Occur.MUST_NOT);
bQuery.add(tQueryB,BooleanClause.Occur.MUST_NOT);
bQuery.add(tQueryC,BooleanClause.Occur.MUST_NOT);
不包含三个词条的查询部结果。如果是通常的文本,文本信息量较大,如果同时不包含“的”、“是”、“在”三个词条,结果是可想而知的,几乎检索不出来任何符合这一条件的结果集。
还有一点,用户通过键入关键字进行检索,不会有这样的用户:不想获取与自己键入关键字匹配的结果呢,其实这种组合没有意义的。
我的测试为:
********************************************************************
3个词条的Hits长度为 : 0
包含3个词条的Document的数量为 : 23
包含3个词条的Document的数量为 : 12
包含3个词条的Document的数量为 : 14
本次搜索所用的时间为 94 ms
如果你建立索引的数据源文件类似古代诗词曲那样的文本,可能检索出来的结果会很可观的。
5、第五种组合:
bQuery.add(tQueryA,BooleanClause.Occur.SHOULD);
bQuery.add(tQueryB,BooleanClause.Occur.MUST_NOT);
bQuery.add(tQueryC,BooleanClause.Occur.MUST_NOT);
bQuery.add(tQueryA,BooleanClause.Occur.MUST);
bQuery.add(tQueryB,BooleanClause.Occur.MUST_NOT);
bQuery.add(tQueryC,BooleanClause.Occur.MUST_NOT);
经过测试,可以得知,上面的这两种组合检索得到的结果集是一样的。也就是说,SHOULD在与MUST_NOT进行组合的时候,其实就是MUST在和MUST_NOT进行组合。
6、第六种组合:
bQuery.add(tQueryA,BooleanClause.Occur.SHOULD);
bQuery.add(tQueryB,BooleanClause.Occur.MUST);
bQuery.add(tQueryC,BooleanClause.Occur.MUST);
其实这种组合与SHOULD没有任何关系了,相当于下面的2个MUST的组合:
bQuery.add(tQueryB,BooleanClause.Occur.MUST);
bQuery.add(tQueryC,BooleanClause.Occur.MUST);
这两种组合检索出来的结果集是一样的。
总结
使用BooleanQuery进行查询,它工作的机制是这样的:
1、先对BooleanQuery中的每个子句分别进行查询,得到多个结果集;
2、对BooleanQuery的各个子句得到的结果集,进行集合的运算(交、并、非)。
最终,集合运算的结果就是显示给用户的,与用户查询条件匹配的记录。
关于前缀查询PrefixQuery(前缀查询)。
准备工作就是为指定的数据源文件建立索引。这里,我使用了ThesaurusAnalyzer分析器,该分析器有自己特定的词库,这个分词组件可以从网上下载。
PrefixQuery其实就是指定一个词条的前缀,不如以前缀“文件”作为前缀的词条有很多:文件系统、文件管理、文件类型等等。但,是在你要检索一个有指定的前缀构成的词条(只有一个前最也是一个词条)时,必须保证你在建立索引的时候,也就是分词生成的词条要有具有这个前缀构成的词条,否则什么也检索不出来。
Lucene中,指定某个前缀,检索过程中会以该前缀作为一个词条进行检索,比如“文件”前缀,如果词条文件中包含“文件”这个词条,而且有一个文件中只有一个句子:“我们要安全地管理好自己的文件。”使用PrefixQuery是也是可以检索出该文件的。
当然了,可以使用BooleanQuery对若干个查询子句进行组合,子句可以是TermQuery子句,可以是PrefixQuery子句,实现复杂查询。
先做个简单的例子,使用一下PrefixQuery。
测试主函数如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import java.util.Iterator;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hit;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
public class PrefixQuerySearcher {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\myindex";
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
String keywordPrefix = "文件"; // 就以“文件”作为前缀
Term prefixTerm = new Term("contents",keywordPrefix);
Query prefixQuery = new PrefixQuery(prefixTerm);
Date startTime = new Date();
Hits hits = searcher.search(prefixQuery);
Iterator it = hits.iterator();
System.out.println("********************************************************************");
while(it.hasNext()){
Hit hit = (Hit)it.next();
System.out.println("Hit的ID 为 : "+hit.getId());
System.out.println("Hit的score 为 : "+hit.getScore());
System.out.println("Hit的boost 为 : "+hit.getBoost());
System.out.println("Hit的toString 为 : "+hit.toString());
System.out.println("Hit的Dcoment 为 : "+hit.getDocument());
System.out.println("Hit的Dcoment 的 Fields 为 : "+hit.getDocument().getFields());
for(int i=0;i<hit.getDocument().getFields().size();i++){
Field field = (Field)hit.getDocument().getFields().get(i);
System.out.println(" -------------------------------------------------------------");
System.out.println(" Field的Name为 : "+field.name());
System.out.println(" Field的stringValue为 : "+field.stringValue());
}
System.out.println("********************************************************************");
}
System.out.println("满足指定前缀的Hits长度为 : "+hits.length());
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试结果输出如下所示:
********************************************************************
Hit的ID 为 : 41
Hit的score 为 : 0.3409751
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[0] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Update.txt> stored/uncompressed,indexed<modified:200707050028>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Update.txt>, stored/uncompressed,indexed<modified:200707050028>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\Update.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200707050028
********************************************************************
Hit的ID 为 : 46
Hit的score 为 : 0.3043366
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[1] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\使用技巧集萃.txt> stored/uncompressed,indexed<modified:200511210413>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\使用技巧集萃.txt>, stored/uncompressed,indexed<modified:200511210413>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\使用技巧集萃.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200511210413
********************************************************************
Hit的ID 为 : 24
Hit的score 为 : 0.25827435
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[2] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\FAQ.txt> stored/uncompressed,indexed<modified:200604130754>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\FAQ.txt>, stored/uncompressed,indexed<modified:200604130754>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\FAQ.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200604130754
********************************************************************
Hit的ID 为 : 44
Hit的score 为 : 0.23094007
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[3] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt> stored/uncompressed,indexed<modified:200801300512>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt>, stored/uncompressed,indexed<modified:200801300512>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200801300512
********************************************************************
Hit的ID 为 : 57
Hit的score 为 : 0.16743648
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[4] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\新建 文本文档.txt> stored/uncompressed,indexed<modified:200710270258>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\新建 文本文档.txt>, stored/uncompressed,indexed<modified:200710270258>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\新建 文本文档.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200710270258
********************************************************************
Hit的ID 为 : 12
Hit的score 为 : 0.14527147
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[5] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeyInfo.txt> stored/uncompressed,indexed<modified:200406041814>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeyInfo.txt>, stored/uncompressed,indexed<modified:200406041814>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\CustomKeyInfo.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200406041814
********************************************************************
Hit的ID 为 : 63
Hit的score 为 : 0.091877736
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[6] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\疑问即时记录.txt> stored/uncompressed,indexed<modified:200711141408>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\疑问即时记录.txt>, stored/uncompressed,indexed<modified:200711141408>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\疑问即时记录.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200711141408
********************************************************************
Hit的ID 为 : 59
Hit的score 为 : 0.08039302
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[7] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\汉化说明.txt> stored/uncompressed,indexed<modified:200708210247>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\汉化说明.txt>, stored/uncompressed,indexed<modified:200708210247>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\汉化说明.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200708210247
********************************************************************
Hit的ID 为 : 14
Hit的score 为 : 0.020302303
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@1a05308
[8] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeysSample.txt> stored/uncompressed,indexed<modified:200610100451>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\CustomKeysSample.txt>, stored/uncompressed,indexed<modified:200610100451>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\CustomKeysSample.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200610100451
********************************************************************
满足指定前缀的Hits长度为 : 9
本次搜索所用的时间为 297 ms
可以看出,包含前缀“文件”的查询结果,一共检索出9项符合条件。
关于以“文件”作为前缀(包含前缀“文件”),在分析器ThesaurusAnalyzer分词组件的词库中具有下面的一些词条:
文件
文件匯編
文件名
文件夹
文件夾
文件尾
文件汇编
文件精神
假如有这样一种需求:想要检索全部以“文件”作为前缀的词条,而不想要单独出现的以“文件”作为词条的结果。
这时,可以指定一个TermQuery子句,再使用BooleanQuery实现。
在上面的测试主函数的基础上,添加如下代码:
String keyword = "文件";
Term term = new Term("contents",keyword);
Query tQuery = new TermQuery(term);
BooleanQuery bQuery = new BooleanQuery();
bQuery.add(tQuery,BooleanClause.Occur.MUST_NOT);
bQuery.add(prefixQuery,BooleanClause.Occur.MUST);
修改Hits hits = searcher.search(prefixQuery);为:
Hits hits = searcher.search(bQuery);
由于不包含单独的以“文件”作为词条的结果,所以使用MUST_NOT逻辑非运算符。
执行查询后,只匹配出一项,如下所示:
********************************************************************
Hit的ID 为 : 44
Hit的score 为 : 0.23393866
Hit的boost 为 : 1.0
Hit的toString 为 : Hit<
org.apache.lucene.search.Hits@ab50cd
[0] resolved>
Hit的Dcoment 为 : Document<stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt> stored/uncompressed,indexed<modified:200801300512>>
Hit的Dcoment 的 Fields 为 : [stored/uncompressed,indexed<path:E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt>, stored/uncompressed,indexed<modified:200801300512>]
-------------------------------------------------------------
Field的Name为 : path
Field的stringValue为 : E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt
-------------------------------------------------------------
Field的Name为 : modified
Field的stringValue为 : 200801300512
********************************************************************
满足指定前缀的Hits长度为 : 1
本次搜索所用的时间为 187 ms
现在看一下PrefixQuery实现的源代码。在PrefixQuery中,只给出了一种构造方法:
private Term prefix;
public PrefixQuery(Term prefix) {
this.prefix = prefix;
}
它是通过一个Term作为参数构造的,非常容易掌握。
在PrefixQuery中有一个重要的rewrite()方法:
public Query rewrite(IndexReader reader) throws IOException {
BooleanQuery query = new BooleanQuery(true);
TermEnum enumerator = reader.terms(prefix);
try {
String prefixText = prefix.text();
String prefixField = prefix.field();
do {
Term term = enumerator.term();
if (term != null &&
term.text().startsWith(prefixText) &&
term.field() == prefixField)
{
TermQuery tq = new TermQuery(term);
tq.setBoost(getBoost());
query.add(tq, BooleanClause.Occur.SHOULD); // 构造了一个BooleanQuery,向其中添加子句,个子句是逻辑或运算
} else {
break;
}
} while (enumerator.next());
} finally {
enumerator.close();
}
return query;
}
该方法通过打开一个IndexReader输入流,使用IndexReader的terms()方法获取到,以“给定前缀构造的词条”的所有词条。然后,以返回的这些词条构造多个TermQuery子句,再将这些子句添加到BooleanQuery中,返回一个新的Query(就是BooleanQuery),这个BooleanQuery中的各个子句是逻辑或的关系,最后使用这个包含了多个子句的BooleanQuery实现复杂查询。
实际上,执行了多个TermQuery,然后将得到的结果集做SHOULD运算。
Lucene中,允许最大的子句上限是1024个,如果超过这个上限就会抛出异常。使用PrefixQuery的主要思想就是向一个BooleanQuery中添加多个参与SHOULD逻辑运算的TermQuery子句,感觉这里面有一个效率问题:对每个子句都进行执行的时候,如果子句的数量小效率还是不错,但是,如果有1000000个甚至更多的TermQuery子句被添加到BooleanQuery中,结果不会很乐观,而且需要重新设定Lucene中默认的最大子句上限,效率应该不能很好。
关于SpanQuery(跨度搜索),它是Query的子类,但是SpanQuery仍然是一个抽象类,它有6个直接子类实现类。继承关系如图所示:
其中SpanTermQuery是一个最基础的跨度搜索实现类,SpanTermQuery与SpanQuery的关系,就如同TermQuery与Query的关系:SpanTermQuery是为SpanQuery其它的具体实现子类服务的,其实TermQuery也是为Query的具体子类实现类服务的,例如构造一个BooleanQuery查询,可以向其中添加多个TermQuery查询子句。
SpanTermQuery跨度搜索
SpanTermQuery的应用与TermQuery的用法是一样的,获取到的检索结果集也是相同的。
这里,SpanTermQuery是SpanQuery的子实现类,所有从跨度搜索的角度来说,他的跨度值就是使用SpanTermQuery的唯一的一个构造方法所用的一个Term的跨度值。也就是说,它的起始位置和结束位置都是一个固定的值(其实就是一个固定跨度)。
SpanFirstQuery跨度搜索
SpanFirstQuery搜索是基于SpanTermQuery的,在实例化一个SpanFirstQuery的时候,是通过一个SpanTermQuery的实例作为参数来构造的。
该SpanFirstQuery只有唯一的一个构造方法:
public SpanFirstQuery(SpanQuery match, int end) {
this.match = match;
this.end = end;
}
上面的end指定了在查询时,从起始位置开始(起始位置为0,这点可以在后面的测试中得知。因为名称中First已经表达了这层含义),在小于end的位置之前的文本中,与match进行匹配。
先使用ThesaurusAnalyzer分析器来实现分词,为指定的数据建立索引,如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
public class MySearchEngineUsingThesaurusAnalyzer {
public static void main(String[] args){
String indexPath = "E:\\Lucene\\index";
IndexWriter writer;
try {
writer = new IndexWriter(indexPath,new ThesaurusAnalyzer(),true);
Field fieldA = new Field("contents","今天是我们地球的生日,对于我们每个人,在我们的宇宙中,一场空前关于我们熟悉的宇宙论的辩论激烈地展开了。",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB1 = new Field("contents","谁知道宇宙空间的奥秘,在我们这些人当中?",Field.Store.YES,Field.Index.TOKENIZED);
Field fieldB2 = new Field("contents","宇宙飞船。",Field.Store.YES,Field.Index.TOKENIZED);
Field fieldB3 = new Field("contents","我们的太空宇宙。",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB1);
docB.add(fieldB2);
docB.add(fieldB3);
Field fieldC = new Field("contents","我们宇宙学家对地球的重要性。",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
首先要把ThesaurusAnalyzer分析器的jar包加入到CLASSPATH中,然后运行上面的主函数,建立索引。
建立的索引文件在本地磁盘指定的索引目录E:\Lucene\index下生成,这时候可以测试SpanFirstQuery的使用了。
因为ThesaurusAnalyzer分析器自带了一个词库,该词库中有词条“我们”,我们就通过“我们”来构造SpanQuery,进行查询。
编写一个SpanFirstQuerySearcher测试类,带主函数,如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.spans.SpanFirstQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.search.spans.SpanTermQuery;
public class SpanFirstQuerySearcher {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\index";
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
String keyword = "我们";
Term term = new Term("contents",keyword);
SpanTermQuery spanTermQuery = new SpanTermQuery(term);
int end = 1;
SpanQuery spanFirstQuery = new SpanFirstQuery(spanTermQuery,end);
System.out.println("####################################################################");
System.out.println("SpanFirstQuery中end指定值为 : "+end);
System.out.println("####################################################################");
Date startTime = new Date();
Hits hits = searcher.search(spanFirstQuery);
for(int i=0;i<hits.length();i++){
TermDocs termDocs = searcher.getIndexReader().termDocs(term);
while(termDocs.next()){
if(termDocs.doc() == hits.id(i)){
System.out.println("Document的内部编号为 : "+hits.id(i));
Document doc = hits.doc(i);
System.out.println("Document的得分为 : "+hits.score(i));
List fieldList = doc.getFields();
System.out.println("Document(编号) "+hits.id(i)+" 的Field的信息: ");
for(int j=0;j<fieldList.size();j++){
Field field = (Field)fieldList.get(j);
System.out.println(" Field的name : "+field.name());
System.out.println(" Field的stringValue : "+field.stringValue());
System.out.println(" ------------------------------------");
}
System.out.println("搜索的该关键字【"+keyword+"】在Document(编号) "+hits.id(i)+" 中,出现过 "+termDocs.freq()+" 次");
}
}
}
System.out.println("********************************************************************");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
当end=1时,也就是从具有一个词条的跨度,运行结果如下所示:
####################################################################
SpanFirstQuery中end指定值为 : 1
####################################################################
Document的内部编号为 : 2
Document的得分为 : 0.18888181
Document(编号) 2 的Field的信息:
Field的name : contents
Field的stringValue : 我们宇宙学家对地球的重要性。
------------------------------------
搜索的该关键字【我们】在Document(编号) 2 中,出现过 1 次
********************************************************************
本次搜索所用的时间为 78 ms
这里docB没有被检索出来。
当end=5时,增大了跨度,执行结果如下所示:
####################################################################
SpanFirstQuery中end指定值为 : 5
####################################################################
Document的内部编号为 : 2
Document的得分为 : 0.18888181
Document(编号) 2 的Field的信息:
Field的name : contents
Field的stringValue : 我们宇宙学家对地球的重要性。
------------------------------------
搜索的该关键字【我们】在Document(编号) 2 中,出现过 1 次
Document的内部编号为 : 0
Document的得分为 : 0.09444091
Document(编号) 0 的Field的信息:
Field的name : contents
Field的stringValue : 今天是我们地球的生日,对于我们每个人,在我们的宇宙中,一场空前关于我们熟悉的宇宙论的辩论激烈地展开了。
------------------------------------
搜索的该关键字【我们】在Document(编号) 0 中,出现过 4 次
********************************************************************
本次搜索所用的时间为 62 ms
当end=10的时候,可以看到3个Document都被检索到,如下所示:
####################################################################
SpanFirstQuery中end指定值为 : 10
####################################################################
Document的内部编号为 : 2
Document的得分为 : 0.18888181
Document(编号) 2 的Field的信息:
Field的name : contents
Field的stringValue : 我们宇宙学家对地球的重要性。
------------------------------------
搜索的该关键字【我们】在Document(编号) 2 中,出现过 1 次
Document的内部编号为 : 0
Document的得分为 : 0.13355961
Document(编号) 0 的Field的信息:
Field的name : contents
Field的stringValue : 今天是我们地球的生日,对于我们每个人,在我们的宇宙中,一场空前关于我们熟悉的宇宙论的辩论激烈地展开了。
------------------------------------
搜索的该关键字【我们】在Document(编号) 0 中,出现过 4 次
Document的内部编号为 : 1
Document的得分为 : 0.1259212
Document(编号) 1 的Field的信息:
Field的name : contents
Field的stringValue : 谁知道宇宙空间的奥秘,在我们这些人当中?
------------------------------------
Field的name : contents
Field的stringValue : 宇宙飞船。
------------------------------------
Field的name : contents
Field的stringValue : 我们的太空宇宙。
------------------------------------
搜索的该关键字【我们】在Document(编号) 1 中,出现过 2 次
********************************************************************
本次搜索所用的时间为 234 ms
SpanNearQuery跨度搜索
SpanNearQuery只有一个构造方法,可以从SpanNearQuery的构造方法来看:
public SpanNearQuery(SpanQuery[] clauses, int slop, boolean inOrder) {
this.clauses = new ArrayList(clauses.length);
for (int i = 0; i < clauses.length; i++) {
SpanQuery clause = clauses[i];
if (i == 0) {
field = clause.getField();
} else if (!clause.getField().equals(field)) {
throw new IllegalArgumentException("Clauses must have same field.");
}
this.clauses.add(clause);
}
this.slop = slop;
this.inOrder = inOrder;
}
从方法的声明来看,各个参数如下:
clauses是指:一个SpanQuery的子句的数组;
slop是指:对于每个SpanQuery都由一个Term构造而成,在一段文本中,可能在出现的这两个词条之间由若干个其它不相关的词条,slop指定了一个整数值,从而可以忽略这些不相关的词条(忽略的个数<=slop),如果slop=0,则说明clauses中的SpanQuery查询的词条必须是相连着的;
inOrder是指:是否clauses子句们按照有序的方式实现搜索,当inOrder为true时,必须按照各个子句中检索的词条的前后顺序进行匹配,逆序的就被淘汰。
依然用上面建立的索引文件测试。
测试通过先构造一个SpanTermQuery(词条内容为“我们”)和一个SpanFirstQuery(词条内容为“宇宙”),再构造一个SpanNearQuery,测试代码如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.spans.SpanFirstQuery;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.search.spans.SpanTermQuery;
public class SpanNearQuerySearcher {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\index";
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
String keywordA = "我们";
Term termA = new Term("contents",keywordA);
SpanTermQuery spanTermQueryA = new SpanTermQuery(termA);
int end = 10;
System.out.println("####################################################################");
System.out.println("SpanFirstQuery中end指定值为 : "+end);
System.out.println("####################################################################");
SpanQuery spanFirstQuery = new SpanFirstQuery(spanTermQueryA,end);
String keywordB = "宇宙";
Term termB = new Term("contents",keywordA);
SpanTermQuery spanTermQueryB = new SpanTermQuery(termB);
int slop = 2;
System.out.println("####################################################################");
System.out.println("SpanNearQuery中slop指定值为 : "+slop);
System.out.println("####################################################################");
SpanNearQuery spanNearQuery = new SpanNearQuery(new SpanQuery[]{spanFirstQuery,spanTermQueryB},slop,true);
Date startTime = new Date();
Hits hits = searcher.search(spanNearQuery);
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
Document doc = hits.doc(i);
System.out.println("Document的得分为 : "+hits.score(i));
List fieldList = doc.getFields();
System.out.println("Document(编号) "+hits.id(i)+" 的Field的信息: ");
for(int j=0;j<fieldList.size();j++){
Field field = (Field)fieldList.get(j);
System.out.println(" Field的name : "+field.name());
System.out.println(" Field的stringValue : "+field.stringValue());
System.out.println(" ------------------------------------");
}
}
System.out.println("********************************************************************");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里,指定了slop=2,inOrder=true,即:“我们”和“宇宙”是按先后顺序在Document中进行匹配的。
运行测试程序,结果如下:
####################################################################
SpanFirstQuery中end指定值为 : 10
####################################################################
####################################################################
SpanNearQuery中slop指定值为 : 2
####################################################################
Document的内部编号为 : 0
Document的得分为 : 0.059729677
Document(编号) 0 的Field的信息:
Field的name : contents
Field的stringValue : 今天是我们地球的生日,对于我们每个人,在我们的宇宙中,一场空前关于我们熟悉的宇宙论的辩论激烈地展开了。
------------------------------------
********************************************************************
本次搜索所用的时间为 93 ms
其实,我们指定了SpanFirstQuery足够大的跨度,但是在SpanNearQuery中指定的slop的值很小,在进行匹配的时候,只是允许两个词条之间可以有2个无关的其它词条,再加上指定了inOrder为true,严格有序,所以只检索到了编号为0的Document。
现在,将slop改为10,因为slop比较关键,决定了两个检索词条之间的间隙大小,这时可以看到检索结果如下所示:
####################################################################
SpanFirstQuery中end指定值为 : 10
####################################################################
####################################################################
SpanNearQuery中slop指定值为 : 10
####################################################################
Document的内部编号为 : 0
Document的得分为 : 0.078204505
Document(编号) 0 的Field的信息:
Field的name : contents
Field的stringValue : 今天是我们地球的生日,对于我们每个人,在我们的宇宙中,一场空前关于我们熟悉的宇宙论的辩论激烈地展开了。
------------------------------------
Document的内部编号为 : 1
Document的得分为 : 0.06730772
Document(编号) 1 的Field的信息:
Field的name : contents
Field的stringValue : 谁知道宇宙空间的奥秘,在我们这些人当中?
------------------------------------
Field的name : contents
Field的stringValue : 宇宙飞船。
------------------------------------
Field的name : contents
Field的stringValue : 我们的太空宇宙。
------------------------------------
********************************************************************
本次搜索所用的时间为 125 ms
SpanNearQuery的构造方法给了一个SpanQuery[] clauses子句数组,可以使用任何继承了SpanQuery的具体实现类,当然也包括SpanNearQuery,将它们添加到子句的数组中,实现复杂的搜索。
SpanNotQuery跨度搜索
依然从构造方法看:
public SpanNotQuery(SpanQuery include, SpanQuery exclude) {
this.include = include;
this.exclude = exclude;
if (!include.getField().equals(exclude.getField()))
throw new IllegalArgumentException("Clauses must have same field.");
}
该SpanNotQuery指定了一个SpanQuery include,该include子句查询会得到一个结果的集合,设为集合A;SpanQuery exclude也可以得到一个结果的集合,设为集合B,则SpanNotQuery检索结果的集合表示为:
A-B
很好理解,就是集合的差运算。
SpanOrQuery跨度搜索
这个也很好理解,就是集合的并运算,它的构造方法如下所示:
public SpanOrQuery(SpanQuery[] clauses) {
this.clauses = new ArrayList(clauses.length);
for (int i = 0; i < clauses.length; i++) {
SpanQuery clause = clauses[i];
if (i == 0) {
field = clause.getField();
} else if (!clause.getField().equals(field)) {
throw new IllegalArgumentException("Clauses must have same field.");
}
this.clauses.add(clause);
}
}
只要把你想要检索的SpanQuery子句构造好以后,添加到SpanQuery[] clauses数组中,谈后执行SpanOrQuery跨度搜索的时候,会把每个子句得到的结果合并起来,得到一个很庞大的检索结果集。
SpanRegexQuery跨度搜索
构造该SpanQuery也很容易:
public SpanRegexQuery(Term term) {
this.term = term;
}
只需要一个Term作为参数即可。从该SpanRegexQuery的名称来看,就知道它和正则表达式有一定的联系。其实在构造好一个SpanRegexQuery以后,可以为其设置一个正则表达式,这要看你对正则表达式的运用的熟练程度如何了。
在SpanRegexQuery中定义了两个成员变量:
private RegexCapabilities regexImpl = new JavaUtilRegexCapabilities();
private Term term;
而且SpanRegexQuery实现了RegexQueryCapable接口:
public class SpanRegexQuery extends SpanQuery implements RegexQueryCapable
如果你想使用SpanRegexQuery实现跨度搜索,可以研究一下与SpanRegexQuery相关的JavaUtilRegexCapabilities类,在JavaUtilRegexCapabilities中涉及到了java.util.regex.Pattern,它可不是Lucene定义的,是第三方提供的。
关于范围查询RangeQuery。
RangeQuery是由两个词条作为上界和下界进行查询,同时指定了一个Boolean型参数,表示是否包括边界,这可以从
RangeQuery的构造方法看到:
public RangeQuery(Term lowerTerm, Term upperTerm, boolean inclusive)
{
if (lowerTerm == null && upperTerm == null)
{
throw new IllegalArgumentException("At least one term must be non-null");
}
if (lowerTerm != null && upperTerm != null && lowerTerm.field() != upperTerm.field())
{
throw new IllegalArgumentException("Both terms must be for the same field");
}
// if we have a lowerTerm, start there. otherwise, start at beginning
if (lowerTerm != null) {
this.lowerTerm = lowerTerm;
}
else {
this.lowerTerm = new Term(upperTerm.field(), "");
}
this.upperTerm = upperTerm;
this.inclusive = inclusive;
}
在构造一个RangeQuery的时候,不能使Term lowerTerm和Term upperTerm都为null,因为这样构造没有意义的。使用RangeQuery对于时间、数字序号等类似特征的词条具有很好的效果,但是作为普通词条意义不大,它会按照字母序来确定搜索范围。
可以指定Term lowerTerm和Term upperTerm中的一个为null,如果指定了Term upperTerm为null,会以Term lowerTerm为上界(即>=),同理,如果Term lowerTerm为null,会以Term upperTerm作为下界(即<=)。
依然使用文章
Lucene-2.2.0 源代码阅读学习(32)
使用的索引文件,建立测试文件,代码如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.RangeQuery;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
public class RangeQuerySearcher {
public static void main(String[] args) {
String indexPath = "E:\\Lucene\\index";
try {
IndexSearcher searcher = new IndexSearcher(indexPath);
String keywordA = "这些";
Term termA = new Term("contents",keywordA);
String keywordB = "辩论";
Term termB = new Term("contents",keywordB);
RangeQuery rangeQuery = new RangeQuery(termA,termA,true);
Date startTime = new Date();
Hits hits = searcher.search(rangeQuery);
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
Document doc = hits.doc(i);
System.out.println("Document的得分为 : "+hits.score(i));
List fieldList = doc.getFields();
System.out.println("Document(编号) "+hits.id(i)+" 的Field的信息: ");
for(int j=0;j<fieldList.size();j++){
Field field = (Field)fieldList.get(j);
System.out.println(" Field的name : "+field.name());
System.out.println(" Field的stringValue : "+field.stringValue());
System.out.println(" ------------------------------------");
}
}
System.out.println("********************************************************************");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
看上面构造RangeQuery的时候,使用的是同一个Term作为上界和下界,而且inclusive指定为true,这时其实检索的就是termA,结果如下所示:
Document的内部编号为 : 1
Document的得分为 : 0.35136628
Document(编号) 1 的Field的信息:
Field的name : contents
Field的stringValue : 谁知道宇宙空间的奥秘,在我们这些人当中?
------------------------------------
Field的name : contents
Field的stringValue : 宇宙飞船。
------------------------------------
Field的name : contents
Field的stringValue : 我们的太空宇宙。
------------------------------------
********************************************************************
本次搜索所用的时间为 93 ms
含有词条“这些”的Document只有编号为1的满足条件。如果上面程序中inclusive指定为false,表示检索的termA作为上界和下界的开区间,很容易想到,检索的检索一定是空集。
如果修改RangeQuery的构造如下:
RangeQuery rangeQuery = new RangeQuery(termA,termB,false);
查询结果还是空集,要知道:“这些”在字母排序时要在“辩论”之后,而且取的是开区间。
如果修改为:
RangeQuery rangeQuery = new RangeQuery(termA,termB,true);
则只是对两个边界进行检索,结果可能会存在,我的测试结果如下所示:
Document的内部编号为 : 1
Document的得分为 : 0.35136628
Document(编号) 1 的Field的信息:
Field的name : contents
Field的stringValue : 谁知道宇宙空间的奥秘,在我们这些人当中?
------------------------------------
Field的name : contents
Field的stringValue : 宇宙飞船。
------------------------------------
Field的name : contents
Field的stringValue : 我们的太空宇宙。
------------------------------------
********************************************************************
本次搜索所用的时间为 93 ms
关于PhraseQuery。
PhraseQuery查询是将多个短语进行合并,得到一个新的词条,从索引库中检索出这个复杂的词条所对应的目标数据文件。
举个例子:假如用户输入关键字“网络安全”,如果索引库中没有单独的“网络安全”这个词条,但是具有“网络”和“安全”这两个词条,我们可以使用PhraseQuery进行查询,将“网络”和“安全”这两个词条合并后能够检索出匹配“网络安全”的所有词条对应的结果集。
现在,使用StandardAnalyzer分析器,对目标数据进行建立索引,也就是,把单独的每个汉字都作为一个词条,存储到索引文件中。可想而知,建立索引花费的时间可能会比较多,因为要对单个汉字进行Tokenizer。
测试程序使用“文件”这个词条,因为使用StandardAnalyzer分析器,索引库中没有词条“文件”,我们使用PhraseQuery来构造实现检索关键字“文件”。
测试主函数如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PhraseQuery;
public class PhraseQuerySearcher {
public static void main(String[] args) {
String path = "E:\\Lucene\\myindex";
String keywordA = "文";
Term termA = new Term("contents",keywordA);
String keywordB = "件";
Term termB = new Term("contents",keywordB);
// 根据上面搜索关键字构造的两个词条,将它们添加到PhraseQuery中,进行检索
PhraseQuery phraseQuery = new PhraseQuery();
phraseQuery.add(termA);
phraseQuery.add(termB);
try {
Date startTime = new Date();
IndexSearcher searcher = new IndexSearcher(path);
Hits hits = searcher.search(phraseQuery);
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
Document doc = hits.doc(i);
System.out.println("Document的得分为 : "+hits.score(i));
List fieldList = doc.getFields();
System.out.println("Document(编号) "+hits.id(i)+" 的Field的信息: ");
for(int j=0;j<fieldList.size();j++){
Field field = (Field)fieldList.get(j);
System.out.println(" Field的name : "+field.name());
System.out.println(" Field的stringValue : "+field.stringValue());
System.out.println(" ------------------------------------");
}
}
System.out.println("********************************************************************");
System.out.println("共检索出符合条件的Document "+hits.length()+" 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试结果如下所示:
Document的内部编号为 : 56
Document的得分为 : 1.0
Document(编号) 56 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\文件.txt
------------------------------------
Field的name : modified
Field的stringValue : 200804200649
------------------------------------
Document的内部编号为 : 41
Document的得分为 : 0.57587546
Document(编号) 41 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\Update.txt
------------------------------------
Field的name : modified
Field的stringValue : 200707050028
------------------------------------
Document的内部编号为 : 46
Document的得分为 : 0.5728219
Document(编号) 46 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\使用技巧集萃.txt
------------------------------------
Field的name : modified
Field的stringValue : 200511210413
------------------------------------
Document的内部编号为 : 24
Document的得分为 : 0.45140085
Document(编号) 24 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\FAQ.txt
------------------------------------
Field的name : modified
Field的stringValue : 200604130754
------------------------------------
Document的内部编号为 : 44
Document的得分为 : 0.4285714
Document(编号) 44 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\Visual Studio 2005注册升级.txt
------------------------------------
Field的name : modified
Field的stringValue : 200801300512
------------------------------------
Document的内部编号为 : 12
Document的得分为 : 0.39528468
Document(编号) 12 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\CustomKeyInfo.txt
------------------------------------
Field的name : modified
Field的stringValue : 200406041814
------------------------------------
Document的内部编号为 : 58
Document的得分为 : 0.33881545
Document(编号) 58 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\新建 文本文档.txt
------------------------------------
Field的name : modified
Field的stringValue : 200710270258
------------------------------------
Document的内部编号为 : 64
Document的得分为 : 0.28571427
Document(编号) 64 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\疑问即时记录.txt
------------------------------------
Field的name : modified
Field的stringValue : 200711141408
------------------------------------
Document的内部编号为 : 60
Document的得分为 : 0.17857142
Document(编号) 60 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\汉化说明.txt
------------------------------------
Field的name : modified
Field的stringValue : 200708210247
------------------------------------
Document的内部编号为 : 14
Document的得分为 : 0.06313453
Document(编号) 14 的Field的信息:
Field的name : path
Field的stringValue : E:\Lucene\txt1\mytxt\CustomKeysSample.txt
------------------------------------
Field的name : modified
Field的stringValue : 200610100451
------------------------------------
********************************************************************
共检索出符合条件的Document 10 个。
本次搜索所用的时间为 640 ms
可见一共检索出10个Document满足条件,即10个Document中都存在与词条“文件”匹配的文件,当然是Field的contents。
PhraseQuery仅仅提供了一个构造方法:
public PhraseQuery() {}
没有参数,没有方法体内容,但是,在使用的时候要用到PhraseQuery的add方法,将由关键字构造的多个词条添加到构造的这个PhraseQuery实例中,实现复杂的检索。
add方法有两个重载的方法,含有一个参数Term的只是把构造的简单词条添加到PhraseQuery中,另一个含有两个参数:
public void add(Term term, int position)
其中,position指定了多个根据用户提交的检索关键字进行分词,分成多个简单的词条,这些词条之间可以存在position个空位,比如用户输入“天地”,如果使用StandardAnalyzer分析器实现后台分词,并且指定了position=1,则目标文件中含有“惊天动地”、“天高地厚”等等词语都能被检索出来。
另外,PhraseQuery还提供了下面方法:
public void setSlop(int s) { slop = s; }
slop 默认值为0,即表示单个简单的词条严格按照顺序组合成新的词条进行检索,亦即:它们之间没有空隙,如果设置为3,表示这些简单的词条之间可以“漏掉”或者“多添”了至多3个无关的字。它与
public void add(Term term, int position)
中的position不同,position是严格按照position个空缺位置检索。
而slop 是>=slop 个空缺都都可以,它可以包含0,1,……,slop-1,是一个空缺长度不同的范围。
于MultiPhraseQuery(多短语查询)。
MultiPhraseQuery可以通过多个短语的拼接来实现复杂查询。
举个例子:现在使用StandardAnalyzer分析器建立索引,索引中是将单个的汉字作为一个一个地词条。使用这个分析器,因为没有像“今天”这样两个汉字组成词条,所以要想单独按照索引中的词条进行检索是不可能查询出任何结果的。
当然,有很多方案可以选择,其中MultiPhraseQuery就能够实现:
它可以指定一个前缀,比如“今”,而后缀是一个Term[]数组,可以是{new Term("年"),new Term("天")},则查询的时候只要含有“今年”和“今天”短语的Document都会查询出来。
而且,也可以指定一个后缀,多个前缀,还可以设定一个slop,指定前缀和后缀之间可以允许有多少个间隔。
下面分别测试一下使用MultiPhraseQuery的效果。
我总结了四种情形:
一个前缀,多个后缀
主函数如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MultiPhraseQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class MultiPhraseQuerySearcher {
private String path = "E:\\Lucene\\index";
private MultiPhraseQuery multiPhraseQuery;
public MultiPhraseQuerySearcher(){
multiPhraseQuery =new MultiPhraseQuery();
}
public void createIndex(){
// 建立索引
String indexPath = "E:\\Lucene\\index";
IndexWriter writer;
try {
//writer = new IndexWriter(indexPath,new ThesaurusAnalyzer(),true);
writer = new IndexWriter(indexPath,new StandardAnalyzer(),true);
Field fieldA = new Field("contents","今天是我们地球的生日。",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB1 = new Field("contents","今晚的辩题很道地:谁知道宇宙空间的奥秘,在我们这些人当中?",Field.Store.YES,Field.Index.TOKENIZED);
Field fieldB2 = new Field("contents","我认为电影《今朝》是一部不错的影片,尤其是在今天,天涯海角到哪里找啊。",Field.Store.YES,Field.Index.TOKENIZED);
Field fieldB3 = new Field("contents","长今到底是啥意思呢?",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB1);
docB.add(fieldB2);
docB.add(fieldB3);
Field fieldC1 = new Field("contents","宇宙学家对地球的重要性,今非昔比。",Field.Store.YES,Field.Index.TOKENIZED);
Field fieldC2 = new Field("contents","衣带渐宽终不悔,为伊消得人憔悴。",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC1);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public void useMultiPrefixExample(){
// 含有多个前缀的情形
Term termA = new Term("contents","道");
Term termB = new Term("contents","对");
multiPhraseQuery.add(new Term[]{termA,termB});
Term termC = new Term("contents","地");
multiPhraseQuery.add(termC);
}
public void useMultiSuffixExample(){
// 含有多个后缀的情形
Term termA = new Term("contents","今");
multiPhraseQuery.add(termA);
Term termB = new Term("contents","天");
Term termC = new Term("contents","晚");
Term termD = new Term("contents","非");
multiPhraseQuery.add(new Term[]{termB,termC,termD});
}
public void useMultiPrefixAndMultiSuffixExample(){ // 含有多个前缀、多个后缀的情形
Term termA = new Term("contents","生");
Term termB = new Term("contents","今");
multiPhraseQuery.add(new Term[]{termA,termB});
Term termC = new Term("contents","非");
Term termD = new Term("contents","日");
Term termE = new Term("contents","朝");
multiPhraseQuery.add(new Term[]{termC,termD,termE});
}
public void useSetSlopExample(){
// 设定slop的情形
Term termA = new Term("contents","我");
multiPhraseQuery.add(termA);
Term termB = new Term("contents","影");
Term termC = new Term("contents","球");
multiPhraseQuery.add(new Term[]{termB,termC});
multiPhraseQuery.setSlop(5);
}
public static void main(String[] args) {
MultiPhraseQuerySearcher mpqs = new MultiPhraseQuerySearcher();
mpqs.createIndex();
mpqs.useMultiSuffixExample();
// 调用含有多个后缀的实现
try {
Date startTime = new Date();
IndexSearcher searcher = new IndexSearcher(mpqs.path);
Hits hits = searcher.search(mpqs.multiPhraseQuery);
System.out.println("********************************************************************");
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
System.out.println("Document内容为 : "+hits.doc(i));
System.out.println("Document的得分为 : "+hits.score(i));
}
System.out.println("********************************************************************");
System.out.println("共检索出符合条件的Document "+hits.length()+" 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
检索的结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今天是我们地球的生日。>>
Document的得分为 : 1.0
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:宇宙学家对地球的重要性,今非昔比。>>
Document的得分为 : 0.79999995
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今晚的辩题很道地:谁知道宇宙空间的奥秘,在我们这些人当中?> stored/uncompressed,indexed,tokenized<contents:我认为电影《今朝》是一部不错的影片,尤其是在今天,天涯海角到哪里找啊。> stored/uncompressed,indexed,tokenized<contents:长今到底是啥意思呢?>>
Document的得分为 : 0.5656854
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 141 ms
由上面的:
public void useMultiSuffixExample(){
// 含有多个后缀的情形
Term termA = new Term("contents","今");
multiPhraseQuery.add(termA);
Term termB = new Term("contents","天");
Term termC = new Term("contents","晚");
Term termD = new Term("contents","非");
multiPhraseQuery.add(new Term[]{termB,termC,termD});
}
可知,检索的是以“今”为唯一的前缀,后缀可以是“天”、“晚”、“非”,由检索结果可以看出:还有“今天”、“今晚”、“今非”的都被检索出来了。
多个前缀,一个后缀
调用useMultiPrefixExample()方法测试。
public void MultiPrefixExample(){
Term termA = new Term("contents","道");
Term termB = new Term("contents","对");
multiPhraseQuery.add(new Term[]{termA,termB});
Term termC = new Term("contents","地");
multiPhraseQuery.add(termC);
}
修改主函数中mpqs.useMultiSuffixExample();为mpqs.useMultiPrefixExample();,测试结果如下所示:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:宇宙学家对地球的重要性,今非昔比。>>
Document的得分为 : 0.88081205
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今晚的辩题很道地:谁知道宇宙空间的奥秘,在我们这些人当中?> stored/uncompressed,indexed,tokenized<contents:我认为电影《今朝》是一部不错的影片,尤其是在今天,天涯海角到哪里找啊。> stored/uncompressed,indexed,tokenized<contents:长今到底是啥意思呢?>>
Document的得分为 : 0.44040602
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 94 ms
我们测试的目的是检索出含有“道地”和“对地”的Document,检索结果和我们的预期想法是一致的。
多个前缀,多个后缀
其实就是对前缀Term[]数组与后缀Term[]数组进行匹配,即:对前缀Term[]数组中的每个Term都与后缀Term[]数组中每个Term进行组合匹配,进行查询。
public void useMultiPrefixAndMultiSuffixExample(){
Term termA = new Term("contents","生");
Term termB = new Term("contents","今");
multiPhraseQuery.add(new Term[]{termA,termB});
Term termC = new Term("contents","非");
Term termD = new Term("contents","日");
Term termE = new Term("contents","朝");
multiPhraseQuery.add(new Term[]{termC,termD,termE});
}
这里,一种有6种组合:“生非”、“生日”、“生朝”、“今非”、“今日”、“今朝”。从索引文件中进行匹配,如果含有上面某个组合的短语,就为实际检索的结果。
修改主函数中mpqs.useMultiSuffixExample();为mpqs.useMultiPrefixAndMultiSuffixExample();,测试结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今天是我们地球的生日。>>
Document的得分为 : 1.0
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:宇宙学家对地球的重要性,今非昔比。>>
Document的得分为 : 0.8
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今晚的辩题很道地:谁知道宇宙空间的奥秘,在我们这些人当中?> stored/uncompressed,indexed,tokenized<contents:我认为电影《今朝》是一部不错的影片,尤其是在今天,天涯海角到哪里找啊。> stored/uncompressed,indexed,tokenized<contents:长今到底是啥意思呢?>>
Document的得分为 : 0.4
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 110 ms
设定slop间隔范围
默认的slop值为0,即表示多个词条直接连接构成短语进行检索。
设定slop后,只要间隔小于等于(<=)slop值都呗认为是满足条件的检索。
调用下面的方法:
public void useSetSlopExample(){
Term termA = new Term("contents","我");
multiPhraseQuery.add(termA);
Term termB = new Term("contents","影");
Term termC = new Term("contents","球");
multiPhraseQuery.add(new Term[]{termB,termC});
multiPhraseQuery.setSlop(5);
}
也就是,满足下面组合的都为检索结果:
我球、我■球、我■■球、我■■■球、我■■■■球、我■■■■■球
我影、我■影、我■■影、我■■■影、我■■■■影、我■■■■■影
其中,一个“■”表示与检索无关的一个词条,即间隔。
进行测试,结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今天是我们地球的生日。>>
Document的得分为 : 0.6144207
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:今晚的辩题很道地:谁知道宇宙空间的奥秘,在我们这些人当中?> stored/uncompressed,indexed,tokenized<contents:我认为电影《今朝》是一部不错的影片,尤其是在今天,天涯海角到哪里找啊。> stored/uncompressed,indexed,tokenized<contents:长今到底是啥意思呢?>>
Document的得分为 : 0.21284157
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 109 ms
总结
从上面的几种情况可以看出MultiPhraseQuery的用法很灵活,而且很方便,要根据具体是应用进行选择。
关于MultiTermQuery查询。
这里研究FuzzyQuery查询。
MultiTermQuery是一个抽象类,继承自它的一种有3个,分别为:FuzzyQuery、WildcardQuery、RegexQuery,其中RegexQuery使用了第三方提供的服务,可以使用正则表达式,如果你对正则表达式很熟悉,可以尝试着使用RegexQuery查询。
FuzzyQuery查询,即模糊查询。
在FuzzyQuery类定义中定义了两个成员变量:
private float minimumSimilarity;
private int prefixLength;
minimumSimilarity是最小相似度,取值范围为0.0~1.0,包含0.0但不包含1.0,默认值为0.5。
prefixLength是前缀长度,默认为0。
其实,在Fuzzy数学中,模糊度被定义为0.5是最模糊的程度,这里说的模糊度是德莱卡模糊度,D(F)=0表示不模糊,即为普通集合;D(F)=05表示最模糊的程度。
使用FuzzyQuery要从的构造方法开始,该类给出3种构造方式:
第一种:
public FuzzyQuery(Term term, float minimumSimilarity, int prefixLength) throws IllegalArgumentException {
super(term);
if (minimumSimilarity >= 1.0f)
throw new IllegalArgumentException("minimumSimilarity >= 1");
else if (minimumSimilarity < 0.0f)
throw new IllegalArgumentException("minimumSimilarity < 0");
if (prefixLength < 0)
throw new IllegalArgumentException("prefixLength < 0");
this.minimumSimilarity = minimumSimilarity;
this.prefixLength = prefixLength;
}
第二种:
public FuzzyQuery(Term term, float minimumSimilarity) throws IllegalArgumentException {
this(term, minimumSimilarity, defaultPrefixLength);
}
第三种:
public FuzzyQuery(Term term) {
this(term, defaultMinSimilarity, defaultPrefixLength);
}
可见,后两种都是使用默认的定义,即minimumSimilarity或者prefixLength使用默认值,最后还是通过第一个构造方法来构造一个FuzzyQuery的实例。
1、使用public FuzzyQuery(Term term)构造查询
实际是这样构造的:FuzzyQuery(term, 0.5f, 0);进行构造。
使用静态定义的具有默认值的两个成员:
minimumSimilarity = defaultMinSimilarity = 0.5f;
prefixLength = defaultPrefixLength = 0;
其实,minimumSimilarity = defaultMinSimilarity = 0.5f并不同于Fuzzy数学中定义的模糊度,minimumSimilarity 表示的应该是一种匹配的严格程度,minimumSimilarity越大表示查询匹配时越严格,通过测试可以看出,如下所示:
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.LockObtainFailedException;
public class FuzzyQuerySearcher {
private String path = "E:\\Lucene\\index";
private FuzzyQuery fuzzyQuery;
public void createIndex(){
IndexWriter writer;
try {
writer = new IndexWriter(path,new ThesaurusAnalyzer(),true); // 使用ThesaurusAnalyzer 中文分析器
//writer = new IndexWriter(path,new StandardAnalyzer(),true);
Field fieldA = new Field("contents","文件夹",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB = new Field("contents","文件名",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents","文件精神",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents","文人",Field.Store.YES,Field.Index.TOKENIZED);
Document docD = new Document();
docD.add(fieldD);
Field fieldE = new Field("contents","整饬",Field.Store.YES,Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
/*Field fieldA = new Field("contents","come",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB = new Field("contents","cope",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents","compleat",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents","complete",Field.Store.YES,Field.Index.TOKENIZED);
Document docD = new Document();
docD.add(fieldD);
Field fieldE = new Field("contents","compile",Field.Store.YES,Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
Field fieldF = new Field("contents","compiler",Field.Store.YES,Field.Index.TOKENIZED);
Document docF = new Document();
docF.add(fieldF);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
writer.addDocument(docF);*/
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
FuzzyQuerySearcher fqs = new FuzzyQuerySearcher();
fqs.createIndex();
Term term = new Term("contents","文件夹");
fqs.fuzzyQuery = new FuzzyQuery(term);
try {
Date startTime = new Date();
IndexSearcher searcher = new IndexSearcher(fqs.path);
Hits hits = searcher.search(fqs.fuzzyQuery);
System.out.println("********************************************************************");
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
System.out.println("Document内容为 : "+hits.doc(i));
System.out.println("Document的得分为 : "+hits.score(i));
}
System.out.println("********************************************************************");
System.out.println("共检索出符合条件的Document "+hits.length()+" 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:上面对中文分词使用了ThesaurusAnalyzer中文分析器,其中构造的那些Field都是词库中一个词条。
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 1.0
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件名>>
Document的得分为 : 0.33333322
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 250 ms
在检索的过程中,进行模糊匹配遵循的原则就是词条长度相等,而且相似,这是在中文检索中,我们看下在英文中检索的结果会是怎样。
首先,在建立索引的方法中,打开建立索引函数中的注释部分,将中文分词部分注释掉;并且,使用StandardAnalyzer分析器分词,修改:
Term term = new Term("contents","compiler");
执行主函数,检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.71428573
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compleat>>
Document的得分为 : 0.25
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:complete>>
Document的得分为 : 0.25
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 125 ms
对于构造的6个Document中,只有两个不能达到最小相似度0.5的要求。
可见,对于中文和英文来说,都能够体现出类似Fuzzy的思想。
2、使用 public FuzzyQuery(Term term, float minimumSimilarity)构造查询
现在,使用该构造方法进行构造,可以对minimumSimilarity进行设置。因为0<=minimumSimilarity<1.0,我们设置只能在这个范围之内。,分别对中文和英文测试一下。
(1) 设置minimumSimilarity = 0.98
◆ 对于中文的情形:
Term term = new Term("contents","文件夹");
fqs.fuzzyQuery = new FuzzyQuery(term,0.98f);
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 78 ms
可见,使用中文,设置minimumSimilarity = 0.98是接近精确匹配的检索结果。
◆ 对于英文的情形:
Term term = new Term("contents","compiler");
fqs.fuzzyQuery = new FuzzyQuery(term,0.98f);
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 125 ms
可见,对于英文,minimumSimilarity的值越大,匹配越精确。
综上所述,minimumSimilarity的值越大,检索时匹配越精确,获得的检索结果就越少。
(2) 设置minimumSimilarity = 0.75
◆ 对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 140 ms
◆ 对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.42857143
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 219 ms
(3) 设置minimumSimilarity = 0.60
◆ 对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 1.0
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件名>>
Document的得分为 : 0.16666652
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 219 ms
◆ 对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.64285713
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compleat>>
Document的得分为 : 0.06249995
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:complete>>
Document的得分为 : 0.06249995
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 328 ms
(4) 设置minimumSimilarity = 0.40
◆ 对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 0.99999994
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件名>>
Document的得分为 : 0.44444436
********************************************************************
共检索出符合条件的Document 2 个。
◆ 对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 0.99999994
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.7619048
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compleat>>
Document的得分为 : 0.375
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:complete>>
Document的得分为 : 0.375
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 453 ms
(5) 设置minimumSimilarity = 0.25
对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 1.0
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件名>>
Document的得分为 : 0.5555556
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件精神>>
Document的得分为 : 0.1111111
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 172 ms
对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.8095239
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compleat>>
Document的得分为 : 0.5
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:complete>>
Document的得分为 : 0.5
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 328 ms
(6) 设置minimumSimilarity = 0.00
对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件夹>>
Document的得分为 : 1.0
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件名>>
Document的得分为 : 0.6666666
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文件精神>>
Document的得分为 : 0.3333333
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 234 ms
对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.85714287
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compleat>>
Document的得分为 : 0.62499994
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:complete>>
Document的得分为 : 0.62499994
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 328 ms
从上面的检索结果可以看出,minimumSimilarity没有Fuzzy数学中的那种对称性,而是递减的,即:minimumSimilarity的值越大,检索出的结果越少,但是越精确。
3、使用 public FuzzyQuery(Term term, float minimumSimilarity, int prefixLength)构造查询
这里,对中文的测试,准备工作很重要:分别使用ThesaurusAnalyzer分析器和StandardAnalyzer分析器建立索引,使得索引目录中既包含ThesaurusAnalyzer分析器的词库,又包含使用StandardAnalyzer分析器分词得到的单个汉字作为一个词条。
不要使用StandardAnalyzer分析器对下面除了“武”以外的词条进行分词,只使用StandardAnalyzer分析器对“武”进行分词,因为要保证只有一个Document中含有“武”这个词条。
建立索引的函数修改为:
Field fieldA = new Field("contents","武",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB = new Field("contents","文修武偃",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents","文东武西",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents","不使用武力",Field.Store.YES,Field.Index.TOKENIZED);
Document docD = new Document();
docD.add(fieldD);
Field fieldE = new Field("contents","不文不武",Field.Store.YES,Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
对于中文,即:
Term term = new Term("contents","文东武西");
fqs.fuzzyQuery = new FuzzyQuery(term,0.00f,10);
对于英文,即:
Term term = new Term("contents","compiler");
fqs.fuzzyQuery = new FuzzyQuery(term,0.00f,0);
(1) 设置minimumSimilarity = 0.00,prefixLength =0
对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 0.99999994
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文修武偃>>
Document的得分为 : 0.49999997
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:不文不武>>
Document的得分为 : 0.24999999
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 343 ms
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compile>>
Document的得分为 : 0.85714287
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compleat>>
Document的得分为 : 0.62499994
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:complete>>
Document的得分为 : 0.62499994
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 375 ms
(2) 设置minimumSimilarity = 0.00,prefixLength =10
对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 297 ms
对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 328 ms
(3) 设置minimumSimilarity = 0.98,prefixLength =0
对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 313 ms
对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 344 ms
(4) 设置minimumSimilarity = 0.98,prefixLength =10
对于中文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 313 ms
对于英文:
检索结果如下所示:
********************************************************************
Document的内部编号为 : 5
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:compiler>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 359 ms
总结
minimumSimilarity越小,模糊度越大,检索出的结果越少,但是越精确;
prefixLength越小,模糊度越到,检索出的结果越少,但是越精确。
关于MultiTermQuery查询。
这里研究继承自MultiTermQuery的WildcardQuery查询。
WildcardQuery查询,就是使用通配符进行查询,通配符可以使用“*”和“?”这两种:“*”可以代表0~N个字符串,“?”只能代表一个字符串,而且它们可以在一个词条Term的任何位置出现,从WildcardQuery的构造方法中可以看出:
public WildcardQuery(Term term) {
super(term);
this.termContainsWildcard = (term.text().indexOf('*') != -1) || (term.text().indexOf('?') != -1);
}
使用通配符,是在构造完词条以后进行通配,然后根据使用通配符构造的词条,再构造一个WildcardQuery实例,接着就可以用这个WildcardQuery实例进行检索了。
WildcardQuery的使用非常简单,测试也非常容易。
1、使用“*”通配符
package org.apache.lucene.shirdrn.main;
import java.io.IOException;
import java.util.Date;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class WildcardQuerySearcher {
private String path = "E:\\Lucene\\index";
private WildcardQuery wildcardQuery;
public void createIndex(){
IndexWriter writer;
try {
writer = new IndexWriter(path,new ThesaurusAnalyzer(),true);
Field fieldA = new Field("contents","文人",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB = new Field("contents","文修武偃",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents","文东武西",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents","不使用武力",Field.Store.YES,Field.Index.TOKENIZED);
Document docD = new Document();
docD.add(fieldD);
Field fieldE = new Field("contents","不文不武",Field.Store.YES,Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public void useStarMatchExample(){
// 使用“*”通配符
Term term = new Term("contents","文*");
wildcardQuery = new WildcardQuery(term);
}
public void useCompositeMatchExample(){
Term term = new Term("contents","?*武*"); // 使用“*”和“?”组合的通配符
wildcardQuery = new WildcardQuery(term);
}
public static void main(String[] args) {
WildcardQuerySearcher wqs = new WildcardQuerySearcher();
wqs.createIndex();
wqs.useStarMatchExample(); // 调用使用“*”通配符设置的方法
try {
Date startTime = new Date();
IndexSearcher searcher = new IndexSearcher(wqs.path);
Hits hits = searcher.search(wqs.wildcardQuery);
System.out.println("********************************************************************");
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
System.out.println("Document内容为 : "+hits.doc(i));
System.out.println("Document的得分为 : "+hits.score(i));
}
System.out.println("********************************************************************");
System.out.println("共检索出符合条件的Document "+hits.length()+" 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
构造 WildcardQuery是在useStarMatchExample()方法中:
public void useStarMatchExample(){
// 使用“*”通配符
Term term = new Term("contents","文*");
wildcardQuery = new WildcardQuery(term);
}
检索结果自己也能猜想到,如下所示:
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文人>>
Document的得分为 : 1.0
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文修武偃>>
Document的得分为 : 1.0
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 313 ms
2、使用“*”和“?”组合通配符
在方法useCompositeMatchExample()中进行构造:
public void useCompositeMatchExample(){
Term term = new Term("contents","?*武*"); // 使用“*”和“?”组合的通配符
wildcardQuery = new WildcardQuery(term);
}
使用“*”和“?”组合的通配符进行构造Term,只要将上面测试函数中的wqs.useStarMatchExample();替换成为:
wqs.useCompositeMatchExample();
执行检索,结果可想而知:
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文修武偃>>
Document的得分为 : 0.9581454
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 0.9581454
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:不使用武力>>
Document的得分为 : 0.9581454
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:不文不武>>
Document的得分为 : 0.9581454
********************************************************************
共检索出符合条件的Document 4 个。
本次搜索所用的时间为 281 ms
简单总结
在以下的7篇文章中:
Lucene-2.2.0 源代码阅读学习(30)
、
Lucene-2.2.0 源代码阅读学习(31)
、
Lucene-2.2.0 源代码阅读学习(32)
、
Lucene-2.2.0 源代码阅读学习(33)
、
Lucene-2.2.0 源代码阅读学习(34)
、
Lucene-2.2.0 源代码阅读学习(35)
、
Lucene-2.2.0 源代码阅读学习(36)
以及在本文,学习了Query的一些重要的基础的实现查询的工具类,在熟练运用的基础上,综合各种查询,一定能够构造出一种非常复杂的查询,来满足实际的需求。
单独的一种Query是不可能满足用户的要求的。
关于QueryParser。
QueryParser是用来解析用户输入的查询的,将用户的输入的短语进行分析,从而提交Query查询来实现检索。
QueryParser一共有三个构造方法,我们通过使用如下的构造方法:
public QueryParser(String f, Analyzer a) {
this(new FastCharStream(new StringReader("")));
analyzer = a;
field = f;
}
指定一个检索的关键字(String类型)和一个分析器。
当然,要使得:建立索引所使用的Analyzer的集合包含使用上面构造方法使用的Analyzer,这样才能在解析出用户意图,得到词条之后,通过IndexSearcher检索索引库,将查询结果返回给用户。
先看一个简单的例子:
package org.shirdrn.lucene.learn;
import java.io.IOException;
import java.util.Date;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.LockObtainFailedException;
public class QueryParserTest {
private String path = "E:\\Lucene\\index";
public void createIndex(){
IndexWriter writer;
try {
writer = new IndexWriter(path,new ThesaurusAnalyzer(),true);
//writer = new IndexWriter(path,new StandardAnalyzer(),true);
String valueA = "在新安全策略中配置的设置可能导致应用程序或服务出现兼容性问题。因此,在将新的安全策略应用于生产服务器之前,应对其进行全面的测试。";
Field fieldA1 = new Field("contents",valueA,Field.Store.YES,Field.Index.TOKENIZED);
Field fieldA2 = new Field("date","2008",Field.Store.YES,Field.Index.UN_TOKENIZED);
Document docA = new Document();
docA.add(fieldA1);
docA.add(fieldA2);
String valueB = "强烈建议您确保用于创建安全策略的原型计算机与要在服务级进行配置的目标服务器相匹配。";
Field fieldB1 = new Field("contents",valueB,Field.Store.YES,Field.Index.TOKENIZED);
Field fieldB2 = new Field("date","2002",Field.Store.YES,Field.Index.UN_TOKENIZED);
Document docB = new Document();
docB.add(fieldB1);
docB.add(fieldB2);
String valueC = "通过创建专为服务器的特定角色而设计的安全策略,SCW 有助于减小服务器的受攻击面。管理员可以通过识别执行相同或类似任务的服务器组来简化策略的创建和分发。";
Field fieldC1 = new Field("contents",valueC,Field.Store.YES,Field.Index.TOKENIZED);
Field fieldC2 = new Field("date","2006",Field.Store.YES,Field.Index.UN_TOKENIZED);
Document docC = new Document();
docC.add(fieldC1);
docC.add(fieldC2);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
QueryParserTest queryParserTest = new QueryParserTest();
queryParserTest.createIndex();
// 调用createIndex()方法建立索引
try {
String userKeyword = "服务器 date:2006";
QueryParser queryParser = new QueryParser("contents",new ThesaurusAnalyzer());
//QueryParser queryParser = new QueryParser("contents",new StandardAnalyzer());
queryParser.setDefaultOperator(QueryParser.AND_OPERATOR); // 使用了AND_OPERATOR
Query query = queryParser.parse(userKeyword);
System.out.println("解析用户输入关键字 : "+query.toString());
IndexSearcher searcher = new IndexSearcher(queryParserTest.path);
Date startTime = new Date();
Hits hits = searcher.search(query);
System.out.println("********************************************************************");
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
System.out.println("Document内容为 : "+hits.doc(i));
System.out.println("Document的得分为 : "+hits.score(i));
}
System.out.println("********************************************************************");
System.out.println("共检索出符合条件的Document "+hits.length()+" 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (ParseException e) {
e.printStackTrace();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
从上面的程序中可以看出,String userKeyword = "服务器 date:2006";定义的就是用户提交的关键字,假设建立索引对应的是一些文章,包括文章内容和发表时间。
我们对这个假想的用户的检索意图进行分析:
他的服务器可能不是最新的也不是最早的服务器产品,“服务器”作为一个关键字字段,而且选择这样的文章的时间不能是最新的,也不能是最早的,中和一下选择2006年。
queryParser.setDefaultOperator(QueryParser.AND_OPERATOR);设置了将解析的用户查询进行AND组合,即取交集。
另外,对于时间,我们假设让用户指定Field的字段date,其实这也是很容易能办到的。
而且,建立索引和构造QueryParser的时候都使用了ThesaurusAnalyzer分析器。
运行程序,检索结果如下所示:
词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3703毫秒;
共添加195574个词语。
解析用户输入关键字 : +contents:服务器 +date:2006
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:通过创建专为服务器的特定角色而设计的安全策略,SCW 有助于减小服务器的受攻击面。管理员可以通过识别执行相同或类似任务的服务器组来简化策略的创建和分发。> stored/uncompressed,indexed<date:2006>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 250 ms
可见,QueryParser根据这样的理解去解析是非常符合用户的检索意向的,而且检索结果很精确。
同样,如果AND_OPERATOR操作符改为OR_OPERATOR操作符,可想而知,检索结果应该将含有关键字“服务器”或者关键字“2006”的结果检索出来,但是这样无疑将检索结果的范围扩大了,返回的结果一般来说较多。
如果将用户的检索关键字设置为:String userKeyword = "服务器安全配置";,用户因该是想对服务器进行配置,保证安全性,使用上面的程序检索,结果如下:
初始化词库结束。用时:4016毫秒;
共添加195574个词语。
解析用户输入关键字 : contents:"服务器 安全 配置"
********************************************************************
********************************************************************
共检索出符合条件的Document 0 个。
本次搜索所用的时间为 93 ms
可见,实际上建立索引的Document中含有这些关键字,却并不能检索出相应的结果。
用户在输入关键字的时候,如果想要表达这样一层含义:通过键入多个关键字,比如键入了“服务器 安全 配置 策略”,通过空格来间隔各个关键字,一般来说,这样的用户的检索意图是文章中包含这些键入的关键词,即各个关键词是AND关系,这样提交查询,会把所有的包含这些关键字的文章检索出来。
只要我们建立的索引库中具有丰富的能够满足用户检索意向的词条,都是能实现的。
例如,将用户键入的关键词设置为:
String userKeyword = "服务器 安全 配置 策略";
运行程序,检索结果如下所示:
词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3703毫秒;
共添加195574个词语。
解析用户输入关键字 : +contents:服务器 +contents:安全 +contents:配置 +contents:策略
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:强烈建议您确保用于创建安全策略的原型计算机与要在服务级进行配置的目标服务器相匹配。> stored/uncompressed,indexed<date:2002>>
Document的得分为 : 0.29777634
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:在新安全策略中配置的设置可能导致应用程序或服务出现兼容性问题。因此,在将新的安全策略应用于生产服务器之前,应对其进行全面的测试。> stored/uncompressed,indexed<date:2008>>
Document的得分为 : 0.28950247
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 94 ms
可见,QueryParser的理解是:
+contents:服务器 +contents:安全 +contents:配置 +contents:策略
用户就是要检索全部包含这四个关键字的文章,最重要的程序中指定了:
queryParser.setDefaultOperator(QueryParser.AND_OPERATOR);
各个关键字之间是AND的关系,因为Lucene默认值为OR关系,倘若不设置为AND_OPERATOR,只要文章中出现上面的莫个关键字,就被认为是检索结果,例如使用上面的.setDefaultOperator()设置为OR_OPERATOR或者不设置使用默认的就是这种设置,检索结果如下:
词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3672毫秒;
共添加195574个词语。
解析用户输入关键字 : contents:服务器 contents:安全 contents:配置 contents:策略
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:强烈建议您确保用于创建安全策略的原型计算机与要在服务级进行配置的目标服务器相匹配。> stored/uncompressed,indexed<date:2002>>
Document的得分为 : 0.29777637
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:在新安全策略中配置的设置可能导致应用程序或服务出现兼容性问题。因此,在将新的安全策略应用于生产服务器之前,应对其进行全面的测试。> stored/uncompressed,indexed<date:2008>>
Document的得分为 : 0.28950244
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:通过创建专为服务器的特定角色而设计的安全策略,SCW 有助于减小服务器的受攻击面。管理员可以通过识别执行相同或类似任务的服务器组来简化策略的创建和分发。> stored/uncompressed,indexed<date:2006>>
Document的得分为 : 0.15523766
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 125 ms
使用QueryParser解析后得到的Query的值为:
解析用户输入关键字 : contents:服务器 contents:安全 contents:配置 contents:策略
在内部通过空格指定各个Field的关系说明是OR关系,检索结果一般来说较多。
通过上面的测试,我们可以得出这样一个结论:
用户在输入多个关键字的时候,使用“空格”将多个关键字间隔开,事实上这个“空格”实际指定了一个内部操作符,关键是在内部如何对QueryParser进行设置,如一般简单地设置为AND_OPERATOR,OR_OPERATOR。
其实,在Lucene中定义了多种操作符,在QueryParser内部实现中,主以通过一些getter方法来看出,例如关于Wildcard通配符查询的:
protected Query getWildcardQuery(String field, String termStr) throws ParseException
{
if ("*".equals(field)) {
if ("*".equals(termStr)) return new MatchAllDocsQuery();
}
if (!allowLeadingWildcard && (termStr.startsWith("*") || termStr.startsWith("?")))
throw new ParseException("'*' or '?' not allowed as first character in WildcardQuery");
if (lowercaseExpandedTerms) {
termStr = termStr.toLowerCase();
}
Term t = new Term(field, termStr);
return new WildcardQuery(t);
}
可以在键入关键字的时候,指定通配符,QueryParser能够解析出用户使用通配符的检索意图。比如,使用下面关键字检索:
String userKeyword = "文?武?";
将前面的测试程序中createIndex()建立索引的方法修改为如下:
public void createIndex(){
IndexWriter writer;
try {
writer = new IndexWriter(path,new ThesaurusAnalyzer(),true);
Field fieldA = new Field("contents","文人",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB = new Field("contents","文修武偃",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents","文东武西",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents","不使用武力",Field.Store.YES,Field.Index.TOKENIZED);
Document docD = new Document();
docD.add(fieldD);
Field fieldE = new Field("contents","不文不武",Field.Store.YES,Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
然后,运行程序,检索结果如下所示:
解析用户输入关键字 : contents:文?武?
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文修武偃>>
Document的得分为 : 1.0
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 2 个。
本次搜索所用的时间为 94 ms
从检索结果可以看出:
解析用户输入关键字 : contents:文?武?
QueryParser的解析结果认为,这需要使用WildcardQuery。
QueryParser也能解析Fuzzy查询,例如,检索关键字为:
String userKeyword = "文东武西~0.01";
检索结果如下所示:
解析用户输入关键字 : contents:文东武西~0.01
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 0.99999994
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文修武偃>>
Document的得分为 : 0.4949495
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:不文不武>>
Document的得分为 : 0.24242423
********************************************************************
共检索出符合条件的Document 3 个。
本次搜索所用的时间为 109 ms
由于使用的最小严格程度接近于0,表示极其不严格匹配,所以Fuzzy检索结果较多。提高最小严格度,可以提高检索匹配的精度,比如使用:
String userKeyword = "文东武西~0.62";
检索结果如下:
解析用户输入关键字 : contents:文东武西~0.62
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:文东武西>>
Document的得分为 : 1.0
********************************************************************
共检索出符合条件的Document 1 个。
本次搜索所用的时间为 156 ms
在前面的使用QueryParser的例子中,出现这样的字样:
“词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3688毫秒;
共添加195574个词语。
”
说明,对用户输入的关键字进行了分词,而后面的测试中,我们输入的就是词库中已经存在的词条,并没有对用户输入的关键字做任何分词处理,亦即,在下面构造QueryParser的时候:
QueryParser queryParser = new QueryParser("contents",new ThesaurusAnalyzer());
实例化的ThesaurusAnalyzer分析器实例并没有执行分词操作,根本不需要分词。
关于Lucene得分的计算。
在IndexSearcher类中有一个管理Lucene得分情况的方法,如下所示:
public Explanation explain(Weight weight, int doc) throws IOException {
return weight.explain(reader, doc);
}
返回的这个Explanation的实例解释了Lucene中Document的得分情况。我们可以测试一下,直观地感觉一下到底这个Explanation的实例都记录了一个Document的哪些信息。
写一个测试类,如下所示:
package org.shirdrn.lucene.learn;
import java.io.IOException;
import java.util.Date;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class AboutLuceneScore {
private String path = "E:\\Lucene\\index";
public void createIndex(){
IndexWriter writer;
try {
writer = new IndexWriter(path,new ThesaurusAnalyzer(),true);
Field fieldA = new Field("contents","一人",Field.Store.YES,Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
Field fieldB = new Field("contents","一人 之交 一人之交",Field.Store.YES,Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents","一人 之下 一人之下",Field.Store.YES,Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents","一人 做事 一人当 一人做事一人当",Field.Store.YES,Field.Index.TOKENIZED);
Document docD = new Document();
docD.add(fieldD);
Field fieldE = new Field("contents","一人 做事 一人當 一人做事一人當",Field.Store.YES,Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
AboutLuceneScore aus = new AboutLuceneScore();
aus.createIndex();
// 建立索引
try {
String keyword = "一人";
Term term = new Term("contents",keyword);
Query query = new TermQuery(term);
IndexSearcher searcher = new IndexSearcher(aus.path);
Date startTime = new Date();
Hits hits = searcher.search(query);
TermDocs termDocs = searcher.getIndexReader().termDocs(term);
while(termDocs.next()){
System.out.print("搜索关键字<"+keyword+">在编号为 "+termDocs.doc());
System.out.println(" 的Document中出现过 "+termDocs.freq()+" 次");
}
System.out.println("********************************************************************");
for(int i=0;i<hits.length();i++){
System.out.println("Document的内部编号为 : "+hits.id(i));
System.out.println("Document内容为 : "+hits.doc(i));
System.out.println("Document得分为 : "+hits.score(i));
Explanation e = searcher.explain(query, hits.id(i));
System.out.println("Explanation为 : \n"+e);
System.out.println("Document对应的Explanation的一些参数值如下: ");
System.out.println("Explanation的getValue()为 : "+e.getValue());
System.out.println("Explanation的getDescription()为 : "+e.getDescription());
System.out.println("********************************************************************");
}
System.out.println("共检索出符合条件的Document "+hits.length()+" 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 "+timeOfSearch+" ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
该测试类中实现了一个建立索引的方法createIndex()方法;然后通过检索一个关键字“一人”,获取到与它相关的Document的信息。
打印出结果的第一部分为:这个检索关键字“一人”在每个Document中出现的次数。
打印出结果的第二部分为:相关的Explanation及其得分情况的信息。
测试结果输出如下所示:
搜索关键字<一人>在编号为 0 的Document中出现过 1 次
搜索关键字<一人>在编号为 1 的Document中出现过 1 次
搜索关键字<一人>在编号为 2 的Document中出现过 1 次
搜索关键字<一人>在编号为 3 的Document中出现过 2 次
搜索关键字<一人>在编号为 4 的Document中出现过 2 次
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人>>
Document得分为 : 0.81767845
Explanation为 :
0.81767845 = (MATCH) fieldWeight(contents:一人 in 0), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
1.0 = fieldNorm(field=contents, doc=0)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.81767845
Explanation的getDescription()为 : fieldWeight(contents:一人 in 0), product of:
********************************************************************
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 做事 一人当 一人做事一人当>>
Document得分为 : 0.5059127
Explanation为 :
0.5059127 = (MATCH) fieldWeight(contents:一人 in 3), product of:
1.4142135 = tf(termFreq(contents:一人)=2)
0.81767845 = idf(docFreq=5)
0.4375 = fieldNorm(field=contents, doc=3)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.5059127
Explanation的getDescription()为 : fieldWeight(contents:一人 in 3), product of:
********************************************************************
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 做事 一人當 一人做事一人當>>
Document得分为 : 0.5059127
Explanation为 :
0.5059127 = (MATCH) fieldWeight(contents:一人 in 4), product of:
1.4142135 = tf(termFreq(contents:一人)=2)
0.81767845 = idf(docFreq=5)
0.4375 = fieldNorm(field=contents, doc=4)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.5059127
Explanation的getDescription()为 : fieldWeight(contents:一人 in 4), product of:
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 之交 一人之交>>
Document得分为 : 0.40883923
Explanation为 :
0.40883923 = (MATCH) fieldWeight(contents:一人 in 1), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.5 = fieldNorm(field=contents, doc=1)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.40883923
Explanation的getDescription()为 : fieldWeight(contents:一人 in 1), product of:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 之下 一人之下>>
Document得分为 : 0.40883923
Explanation为 :
0.40883923 = (MATCH) fieldWeight(contents:一人 in 2), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.5 = fieldNorm(field=contents, doc=2)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.40883923
Explanation的getDescription()为 : fieldWeight(contents:一人 in 2), product of:
********************************************************************
共检索出符合条件的Document 5 个。
本次搜索所用的时间为 79 ms
先从测试的输出结果进行分析,可以获得到如下信息:
■ 测试类中hits.score(i)的值与Explanation的getValue()的值是一样的,即Lucene默认使用的得分;
■ 默认情况下,Lucene按照Document的得分进行排序检索结果;
■ 默认情况下,如果两个Document的得分相同,按照Document的内部编号进行排序,比如上面编号为(3和4)、(1和2)是两组得分相同的Document,结果排序时按照Document的编号进行了排序;
通过从IndexSearcher类中的explain方法:
public Explanation explain(Weight weight, int doc) throws IOException {
return weight.explain(reader, doc);
}
可以看出,实际上是调用了Weight接口类中的explain()方法,而Weight是与一个Query相关的,它记录了一次查询构造的Query的情况,从而保证一个Query实例可以重用。
具体地,可以在实现Weight接口的具体类TermWeight中追溯到explain()方法,而TermWeight类是一个内部类,定义在TermQuery类内部。TermWeight类的explain()方法如下所示:
public Explanation explain(IndexReader reader, int doc)
throws IOException {
ComplexExplanation result = new ComplexExplanation();
result.setDescription("weight("+getQuery()+" in "+doc+"), product of:");
Explanation idfExpl = new Explanation(idf, "idf(docFreq=" + reader.docFreq(term) + ")");
// explain query weight
Explanation queryExpl = new Explanation();
queryExpl.setDescription("queryWeight(" + getQuery() + "), product of:");
Explanation boostExpl = new Explanation(getBoost(), "boost");
if (getBoost() != 1.0f)
queryExpl.addDetail(boostExpl);
queryExpl.addDetail(idfExpl);
Explanation queryNormExpl = new Explanation(queryNorm,"queryNorm");
queryExpl.addDetail(queryNormExpl);
queryExpl.setValue(boostExpl.getValue() *idfExpl.getValue() *queryNormExpl.getValue());
result.addDetail(queryExpl);
// 说明Field的权重
String field = term.field();
ComplexExplanation fieldExpl = new ComplexExplanation();
fieldExpl.setDescription("fieldWeight("+term+" in "+doc+"), product of:");
Explanation tfExpl = scorer(reader).explain(doc);
fieldExpl.addDetail(tfExpl);
fieldExpl.addDetail(idfExpl);
Explanation fieldNormExpl = new Explanation();
byte[] fieldNorms = reader.norms(field);
float fieldNorm =
fieldNorms!=null ? Similarity.decodeNorm(fieldNorms[doc]) : 0.0f;
fieldNormExpl.setValue(fieldNorm);
fieldNormExpl.setDescription("fieldNorm(field="+field+", doc="+doc+")");
fieldExpl.addDetail(fieldNormExpl);
fieldExpl.setMatch(Boolean.valueOf(tfExpl.isMatch()));
fieldExpl.setValue(tfExpl.getValue() *idfExpl.getValue() *fieldNormExpl.getValue());
result.addDetail(fieldExpl);
result.setMatch(fieldExpl.getMatch());
// combine them
result.setValue(queryExpl.getValue() * fieldExpl.getValue());
if (queryExpl.getValue() == 1.0f)
return fieldExpl;
return result;
}
根据检索结果,以及上面的TermWeight类的explain()方法,可以看出输出的字符串部分正好一一对应,比如:idf(Inverse Document Frequency,即反转文档频率)、fieldNorm、fieldWeight。
检索结果的第一个Document的信息:
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人>>
Document得分为 : 0.81767845
Explanation为 :
0.81767845 = (MATCH) fieldWeight(contents:一人 in 0), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
1.0 = fieldNorm(field=contents, doc=0)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.81767845
Explanation的getDescription()为 : fieldWeight(contents:一人 in 0), product of:
tf的计算
上面的tf值Term Frequency,即词条频率,可以在org.apache.lucene.search.Similarity类中看到具体地说明。在Lucene中,并不是直接使用的词条的频率,而实际使用的词条频率的平方根,即:
这是使用org.apache.lucene.search.Similarity类的子类DefaultSimilarity中的方法计算的,如下:
/** Implemented as <code>sqrt(freq)</code>. */
public float tf(float freq) {
return (float)Math.sqrt(freq);
}
即:某个Document的tf = 检索的词条在该Document中出现次数freq取平方根值
也就是freq的平方根。
例如,从我们的检索结果来看:
搜索关键字<一人>在编号为 0 的Document中出现过 1 次
搜索关键字<一人>在编号为 1 的Document中出现过 1 次
搜索关键字<一人>在编号为 2 的Document中出现过 1 次
搜索关键字<一人>在编号为 3 的Document中出现过 2 次
搜索关键字<一人>在编号为 4 的Document中出现过 2 次
各个Document的tf计算如下所示:
编号为0的Document的 tf 为: (float)Math.sqrt(1) = 1.0;
编号为1的Document的 tf 为: (float)Math.sqrt(1) = 1.0;
编号为2的Document的 tf 为: (float)Math.sqrt(1) = 1.0;
编号为3的Document的 tf 为: (float)Math.sqrt(2) = 1.4142135;
编号为4的Document的 tf 为: (float)Math.sqrt(2) = 1.4142135;
idf的计算
检索结果中,每个检索出来的Document的都对应一个idf,在DefaultSimilarity类中可以看到idf计算的实现方法,如下:
/** Implemented as <code>log(numDocs/(docFreq+1)) + 1</code>. */
public float idf(int docFreq, int numDocs) {
return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
}
其中,docFreq是根据指定关键字进行检索,检索到的Document的数量,我们测试的docFreq=5;numDocs是指索引文件中总共的Document的数量,我们的测试比较特殊,将全部的Document都检索出来了,我们测试的numDocs=5。
各个Document的idf的计算如下所示:
编号为0的Document的 idf 为:(float)(Math.log(5/(double)(5+1)) + 1.0) = 0.81767845;
编号为1的Document的 idf 为:(float)(Math.log(5/(double)(5+1)) + 1.0) = 0.81767845;
编号为2的Document的 idf 为:(float)(Math.log(5/(double)(5+1)) + 1.0) = 0.81767845;
编号为3的Document的 idf 为:(float)(Math.log(5/(double)(5+1)) + 1.0) = 0.81767845;
编号为4的Document的 idf 为:(float)(Math.log(5/(double)(5+1)) + 1.0) = 0.81767845;
lengthNorm的计算
在DefaultSimilarity类中可以看到lengthNorm计算的实现方法,如下:
public float lengthNorm(String fieldName, int numTerms) {
return (float)(1.0 / Math.sqrt(numTerms));
}
各个Document的lengthNorm的计算如下所示:
编号为0的Document的 lengthNorm 为:(float)(1.0 / Math.sqrt(1)) = 1.0/1.0 = 1.0;
编号为1的Document的 lengthNorm 为:(float)(1.0 / Math.sqrt(1)) = 1.0/1.0 = 1.0;
编号为2的Document的 lengthNorm 为:(float)(1.0 / Math.sqrt(1)) = 1.0/1.0 = 1.0;
编号为3的Document的 lengthNorm 为:(float)(1.0 / Math.sqrt(2)) = 1.0/1.4142135 = 0.7071068;
编号为4的Document的 lengthNorm 为:(float)(1.0 / Math.sqrt(2)) = 1.0/1.4142135 = 0.7071068;
关于fieldNorm
fieldNorm是在建立索引的时候写入的,而检索的时候需要从索引文件中读取,然后通过解码,得到fieldNorm的float型值,用于计算Document的得分。
在org.apache.lucene.search.TermQuery.TermWeight类中,explain方法通过打开的IndexReader流读取fieldNorm,写入索引文件的是byte[]类型,需要解码,如下所示:
byte[] fieldNorms = reader.norms(field);
float fieldNorm = fieldNorms!=null ? Similarity.decodeNorm(fieldNorms[doc]) : 0.0f;
调用Similarity类的decodeNorm方法,将byte[]类型值转化为float浮点值:
public static float decodeNorm(byte b) {
return NORM_TABLE[b & 0xFF]; // & 0xFF maps negative bytes to positive above 127
}
这样,一个浮点型的fieldNorm的值就被读取出来了,可以参加一些运算,最终实现Lucene的Document的得分的计算。
queryWeight的计算
queryWeight的计算可以在org.apache.lucene.search.TermQuery.TermWeight类中的sumOfSquaredWeights方法中看到计算的实现:
public float sumOfSquaredWeights() {
queryWeight = idf * getBoost();
// compute query weight
return queryWeight * queryWeight;
// square it
}
其实默认情况下,queryWeight = idf,因为Lucune中默认的激励因子boost = 1.0。
各个Document的queryWeight的计算如下所示:
queryWeight = 0.81767845 * 0.81767845 = 0.6685980475944025;
queryNorm的计算
queryNorm的计算在DefaultSimilarity类中实现,如下所示:
/** Implemented as <code>1/sqrt(sumOfSquaredWeights)</code>. */
public float queryNorm(float sumOfSquaredWeights) {
return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
}
这里,sumOfSquaredWeights的计算是在org.apache.lucene.search.TermQuery.TermWeight类中的sumOfSquaredWeights方法实现:
public float sumOfSquaredWeights() {
queryWeight = idf * getBoost();
// compute query weight
return queryWeight * queryWeight;
// square it
}
其实默认情况下,sumOfSquaredWeights = idf * idf,因为Lucune中默认的激励因子boost = 1.0。
上面测试例子中sumOfSquaredWeights的计算如下所示:
sumOfSquaredWeights = 0.81767845*0.81767845 = 0.6685980475944025;
然后,就可以计算queryNorm的值了,计算如下所示:
queryNorm = (float)(1.0 / Math.sqrt(0.6685980475944025) = 1.2229746301862302962735534977105;
value的计算
org.apache.lucene.search.TermQuery.TermWeight类类中还定义了一个value成员:
private float value;
关于value的计算,可以在它的子类org.apache.lucene.search.TermQuery.TermWeight类中看到计算的实现:
public void normalize(float queryNorm) {
this.queryNorm = queryNorm;
queryWeight *= queryNorm; // normalize query weight
value = queryWeight * idf; // idf for document
}
这里,使用normalize方法计算value的值,即:
value = queryNorm * queryWeight * idf;
上面测试例子中value的值计算如下:
value = 1.2229746301862302962735534977105 * 0.6685980475944025 * 0.81767845 = 0.66859804759440249999999999999973;
关于fieldWeight
从检索结果中,可以看到:
0.81767845 = (MATCH) fieldWeight(contents:一人 in 0), product of:
字符串"(MATCH) "的输在ComplexExplanation类中的getSummary方法中可以看到:
protected String getSummary() {
if (null == getMatch())
return super.getSummary();
return getValue() + " = "
+ (isMatch() ? "(MATCH) " : "(NON-MATCH) ")
+ getDescription();
}
这个fieldWeight的值其实和Document的得分是相等的,先看这个fieldWeight是如何计算出来的,在org.apache.lucene.search.TermQuery.TermWeight类中的explain方法中可以看到:
ComplexExplanation fieldExpl = new ComplexExplanation();
fieldExpl.setDescription("fieldWeight("+term+" in "+doc+
"), product of:");
Explanation tfExpl = scorer(reader).explain(doc);
fieldExpl.addDetail(tfExpl);
fieldExpl.addDetail(idfExpl);
Explanation fieldNormExpl = new Explanation();
byte[] fieldNorms = reader.norms(field);
float fieldNorm =
fieldNorms!=null ? Similarity.decodeNorm(fieldNorms[doc]) : 0.0f;
fieldNormExpl.setValue(fieldNorm);
fieldNormExpl.setDescription("fieldNorm(field="+field+", doc="+doc+")");
fieldExpl.addDetail(fieldNormExpl);
fieldExpl.setMatch(Boolean.valueOf(tfExpl.isMatch()));
fieldExpl.setValue(tfExpl.getValue() *
idfExpl.getValue() *
fieldNormExpl.getValue());
result.addDetail(fieldExpl);
result.setMatch(fieldExpl.getMatch());
// combine them
result.setValue(queryExpl.getValue() * fieldExpl.getValue());
if (queryExpl.getValue() == 1.0f)
return fieldExpl;
上面,ComplexExplanation fieldExpl被设置了很多项内容,我们就从这里来获取fieldWeight的计算的实现。
关键是在下面进行了计算:
fieldExpl.setValue(tfExpl.getValue() *
idfExpl.getValue() *
fieldNormExpl.getValue());
使用计算式表示就是
fieldWeight = tf * idf * fieldNorm
fieldNorm的值因为是在建立索引的时候写入到索引文件中的,索引只需要从上面的测试结果中取来,进行如下关于Document的分数的计算的验证。
使用我们这个例子来计算检索出来的Docuyment的fieldWeight,需要用到前面计算出来的结果,如下所示:
编号为0的Document的 fieldWeight 为:1.0 * 0.81767845 * 1.0 = 0.81767845;
编号为1的Document的 fieldWeight 为:1.0 * 0.81767845 * 0.5 = 0.408839225;
编号为2的Document的 fieldWeight 为:1.0 * 0.81767845 * 0.5 = 0.408839225;
编号为3的Document的 fieldWeight 为:1.4142135 * 0.81767845 * 0.4375 = 0.5059127074089703125;
编号为4的Document的 fieldWeight 为:1.4142135 * 0.81767845 * 0.4375 = 0.5059127074089703125;
对比一下,其实检索结果中Document的得分就是这个fieldWeight的值,验证后,正好相符(注意:我这里没有进行舍入运算)。
总结说明
上面的计算得分是按照Lucene默认设置的情况下进行的,比如激励因子的默认值为1.0,它体现的是一个Document的重要性,即所谓的fieldWeight。
不仅可以通过为一个Document设置激励因子boost,而且可以通过为一个Document中的Field设置boost,因为一个Document的权重体现在它当中的Field上,即上面计算出来的fieldWeight与Document的得分是相等的。
提高一个Document的激励因子boost,可以使该Document被检索出来的默认排序靠前,即说明比较重要。也就是说,修改激励因子boost能够改变检索结果的排序。
关于Lucene检索结果的排序问题。
已经知道,Lucene的默认排序是按照Document的得分进行排序的。当检索结果集中的两个Document的具有相同的得分时,默认按照Document的ID对结果进行排序。
下面研究几种设置/改变检索结果排序的方法。
■ 改变Document的boost(激励因子)
改变boost的值实现改变检索结果集的排序,是最简单的方法,只需要在建立索引的过程中,设置指定的Document的boost值,来改变排序结果中Document位置的提前或者靠后。
根据在文章
Lucene-2.2.0 源代码阅读学习(39)
中说明的关于Lucene得分的计算,实际上改变boost的大小,会导致Document的得分的改变,从而按照Lucene默认的对检索结果集的排序方式,改变检索结果中Document的排序的提前或者靠后。在计算得分的时候,使用到了boost的值,默认boost的值为1.0,也就说默认情况下Document的得分与boost的无关的。一旦改变了默认的boost的值,也就从Document的得分与boost无关,变为相关了:boost值越大,Document的得分越高。
下面这个例子在文章
Lucene-2.2.0 源代码阅读学习(39)
中测试过,这里,在建立索引的时候,设置了一下Document的boost,看看排序结果的改变情况:
package org.shirdrn.lucene.learn.sort;
import java.io.IOException;
import java.util.Date;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class AboutLuceneDefaultSort {
private String path = "F:\\index";
public void createIndex() {
IndexWriter writer;
try {
writer = new IndexWriter(path, new ThesaurusAnalyzer(), true);
Field fieldA = new Field("contents", "一人", Field.Store.YES,
Field.Index.TOKENIZED);
Document docA = new Document();
docA.add(fieldA);
docA.setBoost(0.1f); // 减小boost值
Field fieldB = new Field("contents", "一人 之交 一人之交", Field.Store.YES,
Field.Index.TOKENIZED);
Document docB = new Document();
docB.add(fieldB);
Field fieldC = new Field("contents", "一人 之下 一人之下", Field.Store.YES,
Field.Index.TOKENIZED);
Document docC = new Document();
docC.add(fieldC);
Field fieldD = new Field("contents", "一人 做事 一人当 一人做事一人当",
Field.Store.YES, Field.Index.TOKENIZED);
Document docD = new Document();
docD.setBoost(2.0f);
// 提高boost值
docD.add(fieldD);
Field fieldE = new Field("contents", "一人 做事 一人當 一人做事一人當",
Field.Store.YES, Field.Index.TOKENIZED);
Document docE = new Document();
docE.add(fieldE);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
AboutLuceneDefaultSort aus = new AboutLuceneDefaultSort();
aus.createIndex(); // 建立索引
try {
String keyword = "一人";
Term term = new Term("contents", keyword);
Query query = new TermQuery(term);
IndexSearcher searcher = new IndexSearcher(aus.path);
Date startTime = new Date();
Hits hits = searcher.search(query);
TermDocs termDocs = searcher.getIndexReader().termDocs(term);
while (termDocs.next()) {
System.out
.print("搜索关键字<" + keyword + ">在编号为 " + termDocs.doc());
System.out.println(" 的Document中出现过 " + termDocs.freq() + " 次");
}
System.out
.println("********************************************************************");
for (int i = 0; i < hits.length(); i++) {
System.out.println("Document的内部编号为 : " + hits.id(i));
System.out.println("Document内容为 : " + hits.doc(i));
System.out.println("Document得分为 : " + hits.score(i));
Explanation e = searcher.explain(query, hits.id(i));
System.out.println("Explanation为 : \n" + e);
System.out.println("Document对应的Explanation的一些参数值如下: ");
System.out.println("Explanation的getValue()为 : " + e.getValue());
System.out.println("Explanation的getDescription()为 : "
+ e.getDescription());
System.out
.println("********************************************************************");
}
System.out.println("共检索出符合条件的Document " + hits.length() + " 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 " + timeOfSearch + " ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果不设置docA.setBoost(0.1f);和docD.setBoost(2.0f);,则按照默认进行排序,即boost激励因子的值为1.0,执行后,检索结果如下所示:
词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3766毫秒;
共添加195574个词语。
搜索关键字<一人>在编号为 0 的Document中出现过 1 次
搜索关键字<一人>在编号为 1 的Document中出现过 1 次
搜索关键字<一人>在编号为 2 的Document中出现过 1 次
搜索关键字<一人>在编号为 3 的Document中出现过 2 次
搜索关键字<一人>在编号为 4 的Document中出现过 2 次
********************************************************************
Document的内部编号为 :
0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人>>
Document得分为 : 0.81767845
Explanation为 :
0.81767845 = (MATCH) fieldWeight(contents:一人 in 0), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
1.0 = fieldNorm(field=contents, doc=0)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.81767845
Explanation的getDescription()为 : fieldWeight(contents:一人 in 0), product of:
********************************************************************
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 做事 一人当 一人做事一人当>>
Document得分为 : 0.5059127
Explanation为 :
0.5059127 = (MATCH) fieldWeight(contents:一人 in 3), product of:
1.4142135 = tf(termFreq(contents:一人)=2)
0.81767845 = idf(docFreq=5)
0.4375 = fieldNorm(field=contents, doc=3)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.5059127
Explanation的getDescription()为 : fieldWeight(contents:一人 in 3), product of:
********************************************************************
Document的内部编号为 :
4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 做事 一人當 一人做事一人當>>
Document得分为 : 0.5059127
Explanation为 :
0.5059127 = (MATCH) fieldWeight(contents:一人 in 4), product of:
1.4142135 = tf(termFreq(contents:一人)=2)
0.81767845 = idf(docFreq=5)
0.4375 = fieldNorm(field=contents, doc=4)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.5059127
Explanation的getDescription()为 : fieldWeight(contents:一人 in 4), product of:
********************************************************************
Document的内部编号为 :
1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 之交 一人之交>>
Document得分为 : 0.40883923
Explanation为 :
0.40883923 = (MATCH) fieldWeight(contents:一人 in 1), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.5 = fieldNorm(field=contents, doc=1)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.40883923
Explanation的getDescription()为 : fieldWeight(contents:一人 in 1), product of:
********************************************************************
Document的内部编号为 :
2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 之下 一人之下>>
Document得分为 : 0.40883923
Explanation为 :
0.40883923 = (MATCH) fieldWeight(contents:一人 in 2), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.5 = fieldNorm(field=contents, doc=2)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.40883923
Explanation的getDescription()为 : fieldWeight(contents:一人 in 2), product of:
********************************************************************
共检索出符合条件的Document 5 个。
本次搜索所用的时间为 188 ms
检索结果排序为:0——3——4——1——2
如果,我们认为ID为4的Document比较重要,而ID为0的Document不重要,希望在检索的时候,ID为4的Document位置靠前一些,因为它重要,ID为0的Document靠后一些,因为它不如其它的重要,可以通过如下设置:
docA.setBoost(0.1f);
docD.setBoost(2.0f);
来改变指定的Document的boost值,从而改变这两个Document的得分,进而获取所期望的排序位置。这样设置以后,排序结果就改变了,如下所示:
词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3641毫秒;
共添加195574个词语。
搜索关键字<一人>在编号为 0 的Document中出现过 1 次
搜索关键字<一人>在编号为 1 的Document中出现过 1 次
搜索关键字<一人>在编号为 2 的Document中出现过 1 次
搜索关键字<一人>在编号为 3 的Document中出现过 2 次
搜索关键字<一人>在编号为 4 的Document中出现过 2 次
********************************************************************
Document的内部编号为 :
3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 做事 一人当 一人做事一人当>>
Document得分为 : 1.0
Explanation为 :
1.0118254 = (MATCH) fieldWeight(contents:一人 in 3), product of:
1.4142135 = tf(termFreq(contents:一人)=2)
0.81767845 = idf(docFreq=5)
0.875 = fieldNorm(field=contents, doc=3)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 1.0118254
Explanation的getDescription()为 : fieldWeight(contents:一人 in 3), product of:
********************************************************************
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 做事 一人當 一人做事一人當>>
Document得分为 : 0.5
Explanation为 :
0.5059127 = (MATCH) fieldWeight(contents:一人 in 4), product of:
1.4142135 = tf(termFreq(contents:一人)=2)
0.81767845 = idf(docFreq=5)
0.4375 = fieldNorm(field=contents, doc=4)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.5059127
Explanation的getDescription()为 : fieldWeight(contents:一人 in 4), product of:
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 之交 一人之交>>
Document得分为 : 0.40406102
Explanation为 :
0.40883923 = (MATCH) fieldWeight(contents:一人 in 1), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.5 = fieldNorm(field=contents, doc=1)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.40883923
Explanation的getDescription()为 : fieldWeight(contents:一人 in 1), product of:
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人 之下 一人之下>>
Document得分为 : 0.40406102
Explanation为 :
0.40883923 = (MATCH) fieldWeight(contents:一人 in 2), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.5 = fieldNorm(field=contents, doc=2)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.40883923
Explanation的getDescription()为 : fieldWeight(contents:一人 in 2), product of:
********************************************************************
Document的内部编号为 :
0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人>>
Document得分为 : 0.075761445
Explanation为 :
0.076657355 = (MATCH) fieldWeight(contents:一人 in 0), product of:
1.0 = tf(termFreq(contents:一人)=1)
0.81767845 = idf(docFreq=5)
0.09375 = fieldNorm(field=contents, doc=0)
Document对应的Explanation的一些参数值如下:
Explanation的getValue()为 : 0.076657355
Explanation的getDescription()为 : fieldWeight(contents:一人 in 0), product of:
********************************************************************
共检索出符合条件的Document 5 个。
本次搜索所用的时间为 140 ms
这时,检索结果排序变为:3——4——1——2——0
可见,改变了检索结果集中Document的排序位置。
■ 改变Field的boost(激励因子)
改变Field的boost值,和改变Document的boost值是一样的。因为Document的boost是通过添加到Docuemnt中Field体现的,所以改变Field的boost值,可以改变Document的boost值。设置如下所示:
fieldA.setBoost(0.1f);
fieldD.setBoost(2.0f);
排序结果与上面设置:
docA.setBoost(0.1f);
docD.setBoost(2.0f);
对排序结果排序的改变是相同的:
3——4——1——2——0
■ 使用Sort排序工具实现排序
Lucene在查询的时候,可以通过以一个Sort作为参数构造一个检索器IndexSearcher,在构造Sort的时候,指定排序规则,例如下面的测试类:
package org.shirdrn.lucene.learn.sort;
import java.io.IOException;
import java.util.Date;
import net.teamhot.lucene.ThesaurusAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class AboutLuceneSort {
private String path = "F:\\index";
public void createIndex() {
IndexWriter writer;
try {
writer = new IndexWriter(path, new ThesaurusAnalyzer(), true);
Field fieldA1 = new Field("contents", "孤身一人闯天宫,居然像旅游一下轻松,还有谁能做到这样啊?一人!", Field.Store.YES,Field.Index.TOKENIZED);
Field fieldA2 = new Field("count", "27", Field.Store.YES,Field.Index.UN_TOKENIZED);
Document docA = new Document();
docA.add(fieldA1);
docA.add(fieldA2);
Field fieldB1 = new Field("contents", "一人之交与万人之交,一人。", Field.Store.YES, Field.Index.TOKENIZED);
Field fieldB2 = new Field("count", "12", Field.Store.YES, Field.Index.UN_TOKENIZED);
Document docB = new Document();
docB.add(fieldB1);
docB.add(fieldB2);
Field fieldC1 = new Field("contents", "一人之见:千里之行,始于足下。", Field.Store.YES, Field.Index.TOKENIZED);
Field fieldC2 = new Field("count", "12", Field.Store.YES, Field.Index.UN_TOKENIZED);
Document docC = new Document();
docC.add(fieldC1);
docC.add(fieldC2);
Field fieldD1 = new Field("contents", "一人做事一人当,一人。",Field.Store.YES, Field.Index.TOKENIZED);
Field fieldD2 = new Field("count", "9", Field.Store.YES, Field.Index.UN_TOKENIZED);
Document docD = new Document();
docD.add(fieldD1);
docD.add(fieldD2);
Field fieldE1 = new Field("contents", "两人、一人、然后怎么数下去呀——晕~。",Field.Store.YES, Field.Index.TOKENIZED);
Field fieldE2 = new Field("count", "13", Field.Store.YES, Field.Index.UN_TOKENIZED);
Document docE = new Document();
docE.add(fieldE1);
docE.add(fieldE2);
writer.addDocument(docA);
writer.addDocument(docB);
writer.addDocument(docC);
writer.addDocument(docD);
writer.addDocument(docE);
writer.optimize();
writer.close();
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (LockObtainFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
AboutLuceneSort aus = new AboutLuceneSort();
aus.createIndex(); // 建立索引
try {
String keyword = "一人";
Term term = new Term("contents", keyword);
Query query = new TermQuery(term);
IndexSearcher searcher = new IndexSearcher(aus.path);
Date startTime = new Date();
Sort sort = new Sort("count");
// 根据名称为count的Field进行排序
Hits hits = searcher.search(query,sort);
TermDocs termDocs = searcher.getIndexReader().termDocs(term);
while (termDocs.next()) {
System.out
.print("搜索关键字<" + keyword + ">在编号为 " + termDocs.doc());
System.out.println(" 的Document中出现过 " + termDocs.freq() + " 次");
}
System.out
.println("********************************************************************");
for (int i = 0; i < hits.length(); i++) {
System.out.println("Document的内部编号为 : " + hits.id(i));
System.out.println("Document内容为 : " + hits.doc(i));
System.out.println("Document得分为 : " + hits.score(i));
for(int j=0;j<hits.doc(i).getFields().size();j++){
Field field = (Field)hits.doc(i).getFields().get(j);
System.out.println("--- ---Field的name为 : " + field.name());
System.out.println("--- ---Field的StringValue为 : " + field.stringValue());
}
System.out.println("********************************************************************");
}
System.out.println("共检索出符合条件的Document " + hits.length() + " 个。");
Date finishTime = new Date();
long timeOfSearch = finishTime.getTime() - startTime.getTime();
System.out.println("本次搜索所用的时间为 " + timeOfSearch + " ms");
} catch (CorruptIndexException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用了Sort类的一个只有一个参数的构造方法:
public final void setSort(String field) {
setSort(field, false);
}
可见,实际上调用了 setSort(field, false);方法,第一个field为指定一个Field的名称,按照给Field进行排序,第二个为boolean型值,该值指定是否按照降序进行排序,默认情况下为false,表示按照升序排序,即如果按照指定的field排序是作为第一排序的,而且是按照升序排序的,第二排序默认按照Document的ID号码(编号)进行升序排序。
setSort(field, false)方法定义:
public void setSort(String field, boolean reverse) {
SortField[] nfields = new SortField[] {
new SortField(field, SortField.AUTO, reverse), SortField.FIELD_DOC };
fields = nfields;
}
在setSort(field, false)方法中,可以看到,实际上使用了SortField类实现了排序,SortField类具有更加丰富的关于排序的规则和内容。
指定:根据Field名称为“count”进行排序,这里,count是字数的意思,因此在分词的时候没有对其进行分词。期望的排序结果是,根据count,即字数进行排序,而不是根据Document的得分来排序。
运行结果如下所示:
词库尚未被初始化,开始初始化词库.
初始化词库结束。用时:3656毫秒;
共添加195574个词语。
搜索关键字<一人>在编号为 0 的Document中出现过 1 次
搜索关键字<一人>在编号为 1 的Document中出现过 1 次
搜索关键字<一人>在编号为 2 的Document中出现过 1 次
搜索关键字<一人>在编号为 3 的Document中出现过 1 次
搜索关键字<一人>在编号为 4 的Document中出现过 1 次
********************************************************************
Document的内部编号为 : 3
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人做事一人当,一人。> stored/uncompressed,indexed<count:9>>
Document得分为 : 0.51104903
--- ---Field的name为 : contents
--- ---Field的StringValue为 : 一人做事一人当,一人。
--- ---Field的name为 : count
--- ---Field的StringValue为 :
9
********************************************************************
Document的内部编号为 : 1
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人之交与万人之交,一人。> stored/uncompressed,indexed<count:12>>
Document得分为 : 0.35773432
--- ---Field的name为 : contents
--- ---Field的StringValue为 : 一人之交与万人之交,一人。
--- ---Field的name为 : count
--- ---Field的StringValue为 :
12
********************************************************************
Document的内部编号为 : 2
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:一人之见:千里之行,始于足下。> stored/uncompressed,indexed<count:12>>
Document得分为 : 0.40883923
--- ---Field的name为 : contents
--- ---Field的StringValue为 : 一人之见:千里之行,始于足下。
--- ---Field的name为 : count
--- ---Field的StringValue为 : 12
********************************************************************
Document的内部编号为 : 4
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:两人、一人、然后怎么数下去呀——晕~。> stored/uncompressed,indexed<count:13>>
Document得分为 : 0.25552452
--- ---Field的name为 : contents
--- ---Field的StringValue为 : 两人、一人、然后怎么数下去呀——晕~。
--- ---Field的name为 : count
--- ---Field的StringValue为 : 13
********************************************************************
Document的内部编号为 : 0
Document内容为 : Document<stored/uncompressed,indexed,tokenized<contents:孤身一人闯天宫,居然像旅游一下轻松,还有谁能做到这样啊?一人!> stored/uncompressed,indexed<count:27>>
Document得分为 : 0.20441961
--- ---Field的name为 : contents
--- ---Field的StringValue为 : 孤身一人闯天宫,居然像旅游一下轻松,还有谁能做到这样啊?一人!
--- ---Field的name为 : count
--- ---Field的StringValue为 : 27
********************************************************************
共检索出符合条件的Document 5 个。
本次搜索所用的时间为 125 ms
可见,是按照字数count来进行排序的:9——12——12——13——27
而此时检索结构的对应的得分分别为:0.51104903——0.35773432——0.40883923——0.25552452——0.20441961
可见,并不是按照得分的情况来进行排序的,而且,如果count的值相等,则使用默认的第二排序规则,即按照Document的ID号来排序,从上面的count=12结果可以看出。
关于Sort类,在其内部定义了6种构造方法:
public Sort()
public Sort(SortField field)
public Sort(SortField[] fields)
public Sort(String field)
public Sort(String field, boolean reverse)
public Sort(String[] fields)
可以根据不同需要指定排序的规则,按照某个或某几个Field进行排序。不带参数的构造方法public Sort(),在实例化一个Sort之后,可以非常方便的通过调用setSort方法设定排序规则,setSort有5个重载的方法:
public void setSort(SortField field)
public void setSort(SortField[] fields)
public final void setSort(String field)
public void setSort(String field, boolean reverse)
public void setSort(String[] fieldnames)
当然,public final void setSort(String field)在外部不允许直接调用了,是默认的内部使用的设置排序规则的方法。
■ 直接使用SortField实现排序
首先看一下SortField类的源代码:
package org.apache.lucene.search;
import java.io.Serializable;
import java.util.Locale;
public class SortField
implements Serializable {
// 按照Document的得分对检索结果进行排序,得分高的排序靠前
public static final int SCORE = 0;
// 按照Document的编号(ID)对检索结果进行排序,编号小的排序靠前
public static final int DOC = 1;
// 自动检测,自动选择最佳的排序方式,即按照整数类型
public static final int AUTO = 2;
// 根据词条的String串排序
public static final int STRING = 3;
// 将词条解码为整数,按照整数排序
public static final int INT = 4;
// 将词条解码为浮点数,按照浮点数排序
public static final int FLOAT = 5;
// 根据定制的排序器,实现客户化排序
public static final int CUSTOM = 9;
// IMPLEMENTATION NOTE: the FieldCache.STRING_INDEX is in the same "namespace"
// as the above static int values. Any new values must not have the same value
// as FieldCache.STRING_INDEX.
/** 根据Document的得分构造一个SortField实例 */
public static final SortField FIELD_SCORE = new SortField (null, SCORE);
/** 根据Document的编号构造一个SortField实例 */
public static final SortField FIELD_DOC = new SortField (null, DOC);
private String field;
private int type = AUTO; // defaults to determining type dynamically
private Locale locale;
// defaults to "natural order" (no Locale)
boolean reverse = false; // defaults to natural order
private SortComparatorSource factory;
//★ 下面定义了8种构造SortField的方法
★
// 以一个Field的名字的Sing串作为参数构造一个SortField
public SortField (String field) {
this.field = field.intern();
}
public SortField (String field, boolean reverse) {
this.field = field.intern();
this.reverse = reverse;
}
public SortField (String field, int type) {
this.field = (field != null) ? field.intern() : field;
this.type = type;
}
public SortField (String field, int type, boolean reverse) {
this.field = (field != null) ? field.intern() : field;
this.type = type;
this.reverse = reverse;
}
public SortField (String field, Locale locale) {
this.field = field.intern();
this.type = STRING;
this.locale = locale;
}
public SortField (String field, Locale locale, boolean reverse) {
this.field = field.intern();
this.type = STRING;
this.locale = locale;
this.reverse = reverse;
}
public SortField (String field, SortComparatorSource comparator) {
this.field = (field != null) ? field.intern() : field;
this.type = CUSTOM;
this.factory = comparator;
}
public SortField (String field, SortComparatorSource comparator, boolean reverse) {
this.field = (field != null) ? field.intern() : field;
this.type = CUSTOM;
this.reverse = reverse;
this.factory = comparator;
}
public String getField() {
return field;
}
public int getType() {
return type;
}
public Locale getLocale() {
return locale;
}
public boolean getReverse() {
return reverse;
}
public SortComparatorSource getFactory() {
return factory;
}
public String toString() {
StringBuffer buffer = new StringBuffer();
switch (type) {
case SCORE: buffer.append("<score>");
break;
case DOC: buffer.append("<doc>");
break;
case CUSTOM: buffer.append ("<custom:\"" + field + "\": "
+ factory + ">");
break;
default: buffer.append("\"" + field + "\"");
break;
}
if (locale != null) buffer.append ("("+locale+")");
if (reverse) buffer.append('!');
return buffer.toString();
}
}
从上面代码中,可以看出,指定了一种排序的type,这个type对排序的效率是至关重要的,涉及到一个比较的问题。从代码中:
private int type = AUTO;
指定了默认的类型,而AUTO定义如下:
public static final int AUTO = 2;
即按照整数类型,使用整数类型作为排序的type,在进行排序时,效率远远比String类型要高得多。
构造一个SortField实例之后,通过Sort类的setSort方法可以设定详细的排序规则,从而实现对检索结果的排序。
例如:
1、构造一个没有参数的Sort:Sort sort = new Sort();
2、构造一个SortField:SortField sf = new SortField("count",SortField.AUTO,true);
3、使用setSort方法:sort .setSort(sf);
4、构造一个检索器:IndexSearcher is = new IndexSearcher("F:\\index");
5、调用带Sort参数的search检索方法:Hits hits = is.search(query,sort);
这是一个最简单的设置排序的步骤,可以根据SortField的构造方法,构造更加复杂的排序规则。
当执行Hits htis = search(query);这一行代码的时候,到底中间经过了怎样的过程,最终使得我们获取到了含有检索结果的集合Hits hits呢?
这里,以最简单的检索为例,追踪并理解Lucene(2.2.0版本)获取到检索结果的过程。
1、
IndexSearcher继承自Searcher类的最简单的search方法,如下所示:
public final Hits search(Query query) throws IOException {
return search(query, (Filter)null);
}
这里,调用了Searcher类的带有两个参数的search方法,返回Hits,如下所示:
public Hits search(Query query, Filter filter) throws IOException {
return new Hits(this, query, filter);
}
这时,是通过根据传递进来的Query、Filter(简单地考虑,Filter=null)和实例化的Searcher来构造一个Hits的,其实,没有那么简单,想也能想得到,一定是在Hits类中,通过Searcher实例进行了具体的检索过程。
Hits类拥有如下成员:
private Weight weight;
private Searcher searcher;
private Filter filter = null;
private Sort sort = null;
private int length; // 检索到的匹配的Document的个数
private Vector hitDocs = new Vector(); // 最终将检索命中的Document(封装到Hit中,包含了一个Document的丰富信息)放到hitDocs中,取出来呈现给检索的用户
private HitDoc first; // head of LRU cache
private HitDoc last; // tail of LRU cache
private int numDocs = 0; // number cached
private int maxDocs = 200; // max to cache
2、
在Hits类中,只有构造好了含有检索结果的Hits才是最终的检索目标,如下所示:
Hits(Searcher s, Query q, Filter f) throws IOException {
weight = q.weight(s);
searcher = s;
filter = f;
getMoreDocs(50); // 缓存检索结果50*2=100个
}
因为一个Query实例需要被多次使用,故而使用Weight来记录Query的状态。重点看在Hits类调用getMoreDocs方法,该方法实现如下:
private final void getMoreDocs(int min) throws IOException {
if (hitDocs.size() > min) { // hitDocs.size() <min=50,说明缓存的结果数量小于预计要加入到缓存中的结果的数量,因此不执行该if分支,无需改变min(缓存容量)的大小
min = hitDocs.size();
}
int n = min * 2;
// 由50变成100,目的是:直接扩充缓存为原来的2倍,缓存双倍的检索结果以备用户直接从缓存中提取检索结果;间接地一次性为HitQueue(优先级队列)分配大小,防止频繁分配增加系统开销
TopDocs topDocs = (sort == null) ? searcher.search(weight, filter, n) : searcher.search(weight, filter, n, sort);
length = topDocs.totalHits;
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
float scoreNorm = 1.0f;
if (length > 0 && topDocs.getMaxScore() > 1.0f) {
scoreNorm = 1.0f / topDocs.getMaxScore();
}
int end = scoreDocs.length < length ? scoreDocs.length : length;
for (int i = hitDocs.size(); i < end; i++) {
hitDocs.addElement(new HitDoc(scoreDocs[i].score * scoreNorm,
scoreDocs[i].doc));
}
}
第一次调用该方法的时候,作为Hits类的一个成员private Vector hitDocs = new Vector();,它是用作缓存检索结果的。
当sort == null)的时候,执行searcher.search(weight, filter, n),这时,又回到了IndexSearcher类中,执行含有三个参数的重载search方法(见下文),返回检索结果存放于TopDocs topDocs中。关于TopDocs有必要熟悉一下:
public class TopDocs implements java.io.Serializable {
public int totalHits;
public ScoreDoc[] scoreDocs;
private float maxScore;
public float getMaxScore() {
return maxScore;
}
public void setMaxScore(float maxScore) {
this.maxScore=maxScore;
}
TopDocs(int totalHits, ScoreDoc[] scoreDocs, float maxScore) {
this.totalHits = totalHits;
this.scoreDocs = scoreDocs;
this.maxScore = maxScore;
}
}
totalHits成员是本次查询Query检索到的Hits的总数;maxScore成员是最大分数。
ScoreDoc[]成员就是从所有满足查询Query的得分不为0的Document的数组,根据要求选择前n个Docunent返回,它包含Document的编号和得分这两个成员:
package org.apache.lucene.search;
public class ScoreDoc implements java.io.Serializable {
public float score;
public int doc;
public ScoreDoc(int doc, float score) {
this.doc = doc;
this.score = score;
}
}
接着,根据返回的TopDocs topDocs取得其长度length = topDocs.totalHits;用来计算得分标准化因子,以对检索结果按照得分进行排序,并把相关的检索结果缓存到hitDocs中。
3、
在IndexSearcher类中,根据searcher.search(weight, filter, n),执行public TopDocs search(Weight weight, Filter filter, final int nDocs)方法,如下所示:
public TopDocs search(Weight weight, Filter filter, final int nDocs)
throws IOException {
if (nDocs <= 0)
// nDocs=n=2*50=100
throw new IllegalArgumentException("nDocs must be > 0");
TopDocCollector collector = new TopDocCollector(nDocs);
search(weight, filter, collector);
return collector.topDocs();
}
上面,构造了一个TopDocCollector实例,使用如下构造方法:
public TopDocCollector(int numHits) {
this(numHits, new HitQueue(numHits));
}
调用重载的构造方法TopDocCollector(int numHits, PriorityQueue hq),初始化一个Hit的优先级队列,大小为100。其实,关于Hit的优先级队列就是这样的:对于ScoreDoc hitA和ScoreDoc hitB,如果hitA.score>=hitB.score,则选择hitA入队列。
根据构造的TopDocCollector collector,再次调用IndexSearcher类的另一个不同的重载的search方法search(weight, filter, collector);,将检索结果存放到了TopDocCollector collector中,最后根据TopDocCollector collector返回TopDocs。
关于search(weight, filter, collector);调用,实现如下所示:
public void search(Weight weight, Filter filter,
final HitCollector results) throws IOException {
HitCollector collector = results;
if (filter != null) {
// 我们一直设定Filter=null,该分支不执行
final BitSet bits = filter.bits(reader);
collector = new HitCollector() {
public final void collect(int doc, float score) {
if (bits.get(doc)) { // skip docs not in bits
results.collect(doc, score);
}
}
};
}
Scorer scorer = weight.scorer(reader);
if (scorer == null)
return;
scorer.score(collector);
}
先看Scorer scorer,通过weight的scorer方法获取到一个Scorer,在内部类org.apache.lucene.search.TermQuery.TermWeight中可以看到TermWeight的score方法如何实现的:
public Scorer scorer(IndexReader reader) throws IOException {
TermDocs termDocs = reader.termDocs(term);
if (termDocs == null)
return null;
return new TermScorer(this, termDocs, similarity,reader.norms(term.field()));
}
通过IndexReader reader获取到一个TermDocs,其中TermDocs是与检索相关的(即查询中使用Term构造了查询)包含Term的所有<document, frequency>对,document为Document的编号,frequency为Term在该Document中出现的频率(次数)。
执行reader.termDocs(term);时,调用了IndexReader类的termDocs方法,如下所示:
public TermDocs termDocs(Term term) throws IOException {
ensureOpen();
TermDocs termDocs = termDocs();
termDocs.seek(term);
return termDocs;
}
TermScorer是Scorer的子类,它有如下一些成员:
private Weight weight;
private TermDocs termDocs;
private byte[] norms;
private float weightValue;
private int doc;
private final int[] docs = new int[32]; // buffered doc numbers
private final int[] freqs = new int[32]; // buffered term freqs
private int pointer;
private int pointerMax;
private static final int SCORE_CACHE_SIZE = 32;
private float[] scoreCache = new float[SCORE_CACHE_SIZE];
通过这些成员,我们能够知道返回一个TermScorer对象都包含哪些内容,从而为使用Scorer scorer = weight.scorer(reader);便于理解。
weight.scorer(reader) 返回的是一个Scorer,它是用来迭代得分与查询匹配的Docuemnt的。
最后,执行scorer.score(collector);,最终的检索结果可以直接从一个HitCollector collector中提取出来,返回最终的检索结果。
Scorer类的score方法如下:
public void score(HitCollector hc) throws IOException {
while (next()) {
hc.collect(doc(), score());
}
}
再看TopDocCollector(extends HitCollector)类的collect方法的实现:
public void collect(int doc, float score) {
if (score > 0.0f) {
totalHits++;
if (hq.size() < numHits || score >= minScore) {
hq.insert(new ScoreDoc(doc, score));
minScore = ((ScoreDoc)hq.top()).score; // maintain minScore
}
}
}
根据Document的编号和得分,调整Hit的优先级队列,获得满足条件的Docuemt的集合,最终构造出ScoreDoc,实际上是得到了一个ScoreDoc[]数组,从而访问TopDocs的这个ScoreDoc[]成员,就返回了最终检索的结果集合。
关于Hits类。
这个Hits类可是非常的重要,因为Lucene使用了缓存机制,关于缓存的实现就是在这个Hits类中。Hits工作过程中,使用了LRU算法,即通过一个HitDoc结构来实现一个双向链表,使用LRU置换算法,记录用户最近访问过的Document。
开门见山,直接拿出Hits类的实现代码来说话。
package org.apache.lucene.search;
import java.io.IOException;
import java.util.Vector;
import java.util.Iterator;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
public final class Hits {
private Weight weight;
private Searcher searcher;
private Filter filter = null;
private Sort sort = null;
private int length; // Hits的长度,即满足查询的结果数量
private Vector hitDocs = new Vector(); // 用作缓存检索结果的(Hit)
private HitDoc first; // head of LRU cache
private HitDoc last; // tail of LRU cache
private int numDocs = 0; // number cached
private int maxDocs = 200; // max to cache
Hits(Searcher s, Query q, Filter f) throws IOException {
weight = q.weight(s);
searcher = s;
filter = f;
getMoreDocs(50); // retrieve 100 initially | 从缓存中取出检索结果,如果缓存为null,则需要查询,查询后将结果加入缓存中
}
Hits(Searcher s, Query q, Filter f, Sort o) throws IOException {
weight = q.weight(s);
searcher = s;
filter = f;
sort = o;
getMoreDocs(50); // retrieve 100 initially | 从缓存中取出检索结果,如果缓存为null,则需要查询,查询后将结果加入缓存中
}
/**
* 将满足检索结果的Document加入到缓存hitDocs中
*/
private final void getMoreDocs(int min) throws IOException {
/////////////////////////////////////////////////////////////////////////////////////////////
System.out.println("■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size="+hitDocs.size());
///////////////////////////////////////////////////////////////////////////////////////////
if (hitDocs.size() > min) {
min = hitDocs.size();
}
int n = min * 2; // 扩充缓存容量为默认的2倍(默认最小情况下,也要扩充缓存。即使检索结果为1条记录,缓存的长度也扩充为100)
TopDocs topDocs = (sort == null) ? searcher.search(weight, filter, n) : searcher.search(weight, filter, n, sort);
length = topDocs.totalHits;
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
float scoreNorm = 1.0f;
if (length > 0 && topDocs.getMaxScore() > 1.0f) {
scoreNorm = 1.0f / topDocs.getMaxScore();
}
int end = scoreDocs.length < length ? scoreDocs.length : length;
for (int i = hitDocs.size(); i < end; i++) {
hitDocs.addElement(new HitDoc(scoreDocs[i].score * scoreNorm,
scoreDocs[i].doc));
}
/////////////////////////////////////////////////////////////////////////////////////////////
System.out.println("■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size="+hitDocs.size());
///////////////////////////////////////////////////////////////////////////////////////////
}
// 返回Hits的长度,即满足查询的Document的数量,并非是缓存Vector hitDocs的长度
public final int length() {
return length;
}
// 根据Document的编号获取到Document
public final Document doc(int n) throws CorruptIndexException, IOException {
/////////////////////////////////////////////////////////////////////////////////////////////
System.out.println("hitDocs.size()="+hitDocs.size());
/////////////////////////////////////////////////////////////////////////////////////////////
HitDoc hitDoc = hitDoc(n);
// Update LRU cache of documents
remove(hitDoc); // remove from list, if there
addToFront(hitDoc); // add to front of list
if (numDocs > maxDocs) { // if cache is full
HitDoc oldLast = last;
remove(last); // flush last
oldLast.doc = null; // let doc get gc'd
}
if (hitDoc.doc == null) {
hitDoc.doc = searcher.doc(hitDoc.id); // cache miss: read document
}
return hitDoc.doc;
}
// 得到第n个Document的得分
public final float score(int n) throws IOException {
return hitDoc(n).score;
}
// 得到第n个Document的编号
public final int id(int n) throws IOException {
return hitDoc(n).id;
}
public Iterator iterator() {
return new HitIterator(this);
}
private final HitDoc hitDoc(int n) throws IOException {
if (n >= length) {
throw new IndexOutOfBoundsException("Not a valid hit number: " + n);
}
if (n >= hitDocs.size()) {
getMoreDocs(n);
}
return (HitDoc) hitDocs.elementAt(n);
}
private final void addToFront(HitDoc hitDoc) { // insert at front of cache
if (first == null) {
last = hitDoc;
} else {
first.prev = hitDoc;
}
hitDoc.next = first;
first = hitDoc;
hitDoc.prev = null;
numDocs++;
}
private final void remove(HitDoc hitDoc) { // remove from cache
if (hitDoc.doc == null) { // it's not in the list
return; // abort
}
if (hitDoc.next == null) {
last = hitDoc.prev;
} else {
hitDoc.next.prev = hitDoc.prev;
}
if (hitDoc.prev == null) {
first = hitDoc.next;
} else {
hitDoc.prev.next = hitDoc.next;
}
numDocs--;
}
}
final class HitDoc {
float score;
int id;
Document doc = null;
HitDoc next; // in doubly-linked cache
HitDoc prev; // in doubly-linked cache
HitDoc(float s, int i) {
score = s;
id = i;
}
}
上面代码中,红色标注的部分为后面测试之用。
一次查询时,需要构造一个Query实例。从Hits类的成员变量来看,在检索的过程中,一个Query实例并不是只使用一次,那么多次使用进行查询就需要记录这个Query实例的状态。
为了更加直观,写了一个测试类,来观察缓存长度的分配情况:
package org.shirdrn.lucene.learn.test;
import java.io.IOException;
import java.util.Date;
import java.util.Iterator;
import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.Hit;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.LockObtainFailedException;
public class MyHitsTest {
public void create() throws CorruptIndexException, LockObtainFailedException, IOException{
String indexPath = "H:\\index";
IndexWriter writer = new IndexWriter(indexPath,new CJKAnalyzer(),true);
for(int i=0;i<500;i++){
Document doc = new Document();
doc.add(new Field("title","搜索引擎收录新页面与文章原创性问题",Field.Store.YES,Field.Index.TOKENIZED));
doc.add(new Field("date",
DateTools.timeToString((new Date().getTime()), DateTools.Resolution.MINUTE),
Field.Store.YES, Field.Index.UN_TOKENIZED));
doc.add(new Field("author","Shirdrn",Field.Store.YES,Field.Index.UN_TOKENIZED));
String contents = "如果分词后的多个关键字,在关键文章分词中,某一关键片段中出现的关键频率最高,这个关键片段当然是Google检索结果呈现的那两行关键摘要。";
doc.add(new Field("contents",contents,Field.Store.NO,Field.Index.TOKENIZED));
writer.addDocument(doc);
}
writer.optimize();
writer.close();
}
public void search() throws CorruptIndexException, IOException{
Query query = new TermQuery(new Term("contents","关键"));
String indexPath = "H:\\index";
IndexSearcher searcher = new IndexSearcher(indexPath);
Hits hits = searcher.search(query);
System.out.println("★★★共检索出满足的结果 "+hits.length()+" 条。");
Iterator it = hits.iterator();
while(it.hasNext()){
System.out.print("★调用Hits的doc()方法,(Vector): ");
hits.doc(0); //执行这一行代码是为了观察:当获取一个Document的时候缓存的长度,因为第一次查看缓存的时候其长度是为0的,如果检索结果数量不为0则之后缓存长度是不为0的,至少为100
Hit hit = (Hit)it.next();
System.out.println("★检索到的Hit的ID为 : "+hit.getId());
}
}
}
class MyTest{
public static void main(String[] args) throws CorruptIndexException, LockObtainFailedException, IOException, ParseException {
MyHitsTest hitsTest = new MyHitsTest();
//hitsTest.create();
hitsTest.search();
}
}
首先要构造500个Document,建立索引,之后,执行检索的操作,结果如下所示:
■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=0
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=100
★★★共检索出满足的结果 500 条。
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 0
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 1
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 2
……
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 98
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 99
★调用Hits的doc()方法,(Vector): hitDocs.size()=100■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=100
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=200
★检索到的Hit的ID为 : 100
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 101
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 102
……
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 198
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 199
★调用Hits的doc()方法,(Vector): hitDocs.size()=200■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=200
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=400
★检索到的Hit的ID为 : 200
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 201
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 202
……
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 398
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 399
★调用Hits的doc()方法,(Vector): hitDocs.size()=400■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=400
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=500
★检索到的Hit的ID为 : 400
★调用Hits的doc()方法,(Vector): hitDocs.size()=500★检索到的Hit的ID为 : 401
★调用Hits的doc()方法,(Vector): hitDocs.size()=500★检索到的Hit的ID为 : 402
……
由结果可见看到,构造一个Hits的实例的时候,调用getMoreDocs()方法。
第一次进入getMoreDocs()方法时,hitDocs.size() = 0 > min = 50不成立,接着n = min*2 = 50*2 = 100,因此离开getMoreDocs()方法时hitDocs.size() = 100;
第二次进入getMoreDocs()方法时,hitDocs.size() = 100 > min = 50成立,从而设置min = hitDocs.size() = 100,接着n = min*2 = 100*2 = 200, 因此离开getMoreDocs()方法时hitDocs.size() = 200;
第三次进入getMoreDocs()方法时,hitDocs.size() = 200 > min = 100成立,从而设置min = hitDocs.size() = 200,接着n = min*2 = 200*2 = 400, 因此离开getMoreDocs()方法时hitDocs.size() = 400;
如果满足查询的检索结果的Document数量足够大的话,应该继续是:
第四次进入getMoreDocs()方法时,hitDocs.size() = 400,离开getMoreDocs()方法时hitDocs.size() = 800;
第五次进入getMoreDocs()方法时,hitDocs.size() = 800,离开getMoreDocs()方法时hitDocs.size() = 1600;
……
根据上面,最后一次(第四次)进入getMoreDocs()方法的时候,hitDocs.size() = 400 > min = 400不成立,接着n = min*2 = 400*2 = 800,此时虽然缓存扩充了,但是执行searcher.search(weight, filter, n) 的时候取到了100条满足条件的Document,故而缓存的实际大小为hitDocs.size() = 500, 因此离开getMoreDocs()方法时hitDocs.size() = 500,其实此次如果满足查询的Document数量足够,可以达到hitDocs.size() = 800。