构建索引
1.索引的基本元素
在索引操作期间,文本首先从原始文档提取出来,然后组装成Document,一个Document包含多个Field。然后分析程序把Field的值分析成多个词汇单元,并把这些词汇单元通过倒排的方式和Document对应起来,这就形成了索引文件。
索引选项
索引选项(Field.Index.*)通过倒排索引来控制域文本可用何种搜索方式。他们分别如下:- Index.ANALYZED
- Index.NOT_ANALYZED
- Index.ANALYZED_NO_NORMS
- Index.NOT_ANALYZED_NO_NORMS
- Index.NO
存储选项
存储选项(Field.Store.*)用来确定是否需要存储域的真实值。他们分别如下:- Store.YES
- Store.NO
- 项向量选项
它是介于索引和存储域的一个中间结构,它让你选择是否把搜索期间该文档所有的唯一项存储在索引当中。 排序选项
Lucene可以设置根据某个Field进行排序。多值域
一个Field可以有多个不相同的域值。
2.基本索引操作
向索引添加文档
向文档添加索引使用addDocument方法。
package com.lucene._2_1;
import junit.framework.TestCase;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import java.io.IOException;
/**
* Created by xun.zhang on 2017/10/27.
*/
public class IndexingTest extends TestCase {
protected String[] ids = {"1","2"};
protected String[] unindexed = {"Netherlands", "Italy"};
protected String[] unstored = {"Amsterdam has lots of bridges", "Venice has lots of canals"};
protected String[] text = {"Amsterdam","Venice"};
private Directory directory;
public void setUp() throws Exception {
directory = new RAMDirectory();
IndexWriter writer = getWriter();
for (int i = 0; i < ids.length; i++) {
Document doc = new Document();
doc.add(new Field("id", ids[i] , Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("country", unindexed[i], Field.Store.YES, Field.Index.NO));
doc.add(new Field("contents", unindexed[i], Field.Store.NO, Field.Index.ANALYZED));
doc.add(new Field("city",text[i], Field.Store.YES, Field.Index.ANALYZED));
writer.addDocument(doc);
}
writer.close();
}
private IndexWriter getWriter() throws IOException {
return new IndexWriter(directory, new WhitespaceAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
}
protected int getHitCount(String fieldName, String searchString) throws IOException {
IndexSearcher searcher = new IndexSearcher(directory);
Term t = new Term(fieldName, searchString);
Query query = new TermQuery(t);
TopDocs topDocs = searcher.search(query, 1);
int hitCount = topDocs.totalHits;
searcher.close();
return hitCount;
}
public void testIndexWriter() throws IOException {
IndexWriter writer = getWriter();
assertEquals(ids.length, writer.numDocs());
}
public void testIndexReader() throws IOException {
IndexReader reader = IndexReader.open(directory);
assertEquals(ids.length, reader.maxDoc());
assertEquals(ids.length, reader.numDocs());
}
}
删除索引中的文档
删除索引中的文档使用deleteDocument方法。
package com.lucene._2_2;
import junit.framework.TestCase;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import java.io.IOException;
/**
* Created by xun.zhang on 2017/10/27.
*/
public class IndexingTest extends TestCase {
protected String[] ids = {"1","2"};
protected String[] unindexed = {"Netherlands", "Italy"};
protected String[] unstored = {"Amsterdam has lots of bridges", "Venice has lots of canals"};
protected String[] text = {"Amsterdam","Venice"};
private Directory directory;
public void setUp() throws Exception {
directory = new RAMDirectory();
IndexWriter writer = getWriter();
for (int i = 0; i < ids.length; i++) {
Document doc = new Document();
doc.add(new Field("id", ids[i] , Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("country", unindexed[i], Field.Store.YES, Field.Index.NO));
doc.add(new Field("contents", unindexed[i], Field.Store.NO, Field.Index.ANALYZED));
doc.add(new Field("city",text[i], Field.Store.YES, Field.Index.ANALYZED));
writer.addDocument(doc);
}
writer.close();
}
private IndexWriter getWriter() throws IOException {
return new IndexWriter(directory, new WhitespaceAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
}
public void testDeleteBeforeOptimize() throws IOException {
IndexWriter writer = getWriter();
assertEquals(2, writer.numDocs());
writer.deleteDocuments(new Term("id", "1"));
writer.commit();
assertTrue(writer.hasDeletions());
assertEquals(2, writer.maxDoc());
assertEquals(1, writer.numDocs());
writer.close();
}
public void testDeleteAfterOptimize() throws IOException {
IndexWriter writer = getWriter();
assertEquals(2, writer.maxDoc());
writer.deleteDocuments(new Term("id", "1"));
writer.optimize();
writer.commit();
assertEquals(1, writer.maxDoc());
assertEquals(1, writer.numDocs());
writer.close();
}
}
更新索引中的文档
更新索引中的文档使用updateDocument方法,其实updateDocument方法只是多deleteDocument和addDocument的一个封装,也就是说Lucene的更新就是把之前的全部删除掉,然后再添加。
package com.lucene._2_3;
import junit.framework.TestCase;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import java.io.IOException;
/**
* Created by xun.zhang on 2017/10/27.
*/
public class IndexingTest extends TestCase {
protected String[] ids = {"1","2"};
protected String[] unindexed = {"Netherlands", "Italy"};
protected String[] unstored = {"Amsterdam has lots of bridges", "Venice has lots of canals"};
protected String[] text = {"Amsterdam","Venice"};
private Directory directory;
public void setUp() throws Exception {
directory = new RAMDirectory();
IndexWriter writer = getWriter();
for (int i = 0; i < ids.length; i++) {
Document doc = new Document();
doc.add(new Field("id", ids[i] , Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("country", unindexed[i], Field.Store.YES, Field.Index.NO));
doc.add(new Field("contents", unindexed[i], Field.Store.NO, Field.Index.ANALYZED));
doc.add(new Field("city",text[i], Field.Store.YES, Field.Index.ANALYZED));
writer.addDocument(doc);
}
writer.close();
}
private IndexWriter getWriter() throws IOException {
return new IndexWriter(directory, new WhitespaceAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
}
protected int getHitCount(String fieldName, String searchString) throws IOException {
IndexSearcher searcher = new IndexSearcher(directory);
Term t = new Term(fieldName, searchString);
Query query = new TermQuery(t);
TopDocs topDocs = searcher.search(query, 1);
int hitCount = topDocs.totalHits;
searcher.close();
return hitCount;
}
public void testUpdate() throws IOException {
assertEquals(1, getHitCount("city", "Amsterdam"));
IndexWriter writer = getWriter();
Document doc = new Document();
doc.add(new Field("id","1",Field.Store.YES,Field.Index.NOT_ANALYZED));
doc.add(new Field("country","Netherlands",Field.Store.YES,Field.Index.NO));
doc.add(new Field("contents","Den Haag has a lot of museums",Field.Store.NO,Field.Index.ANALYZED));
doc.add(new Field("city","DenHaag",Field.Store.YES,Field.Index.ANALYZED));
writer.updateDocument(new Term("id","1"),doc);
writer.optimize();
writer.close();
assertEquals(0,getHitCount("city","Amsterdam"));
assertEquals(1,getHitCount("city","DenHaag"));
}
}
3.高级索引选项
加权
Lucene支持既可以对Document进行加权,也可以对Document立面的Field加权,加权后会使加权过的文档和Field排在前面一些,但是加权只是影响排序的一个因子,也就是说就算加权了也不一定就排在前面,因为还有其他一些因素影响着排序。加权的方法只有一个:setBoost(float)。
域截取
有时候是无法预知将要被索引的文档到底有多大,如果说索引的时候出现了一个巨大无比的文档,那么将会导致内存耗尽。碰到有可能发生这种情况的时候,在索引时就必须设置对输入文档大小的限制,也即域截取。MaxFieldLength.UNLIMITED和MaxFieldLength.LIMITED就是来干这件事的。MaxFieldLength.LIMITED表示只截取Field中前1000个项。这里的1000个项,就是经过分析程序分析后的1000个词汇单元。
近实时搜索
Lucene从2.9版本开始新增了一项被称为实时搜索的重要功能,该功能解决了一个长期困扰搜索引擎的问题:文档的即时索引和搜索问题。
索引优化
随着索引的不断修改,索引会出现多个独立段,这会给搜索带来一点性能损耗,因为搜索程序必须打开每个独立段进行搜索,然后合并搜索结果。Lucene使用optimize方法优化索引。
多种Directory
并发、线程安全及锁机制
线程安全
Lucene的并发处理规则非常简单。- 任意数量的只读属性的IndexReader类都可以同时打开一个索引,IndexReader甚至可以在IndexWriter索引的时候打开索引。
- 对于一个索引,同一时刻只能被一个IndexWriter打开,并且在索引的根目录会创一个write.lock文件。这一机制,Lucene也提供可接口,可供使用者实现自己的逻辑,但是如果不是对Lucene这一机制非常熟悉,建议最好不要改写。
- 任意多个线程可以共享一个IndexReader类或IndexWriter类,因为它们是线程安全的。
- 远程文件系统
Lucene可以使用远程文件系统来做集群,比如:Samba、NFS、AFP等。当然每中类型的操作系统,他们对远程文件系统的支持由差异,使用时需要注意。 - 调试索引
如果你需要看到Lucene建立索引的详细过程,可以使用IndexWriter类的setInfoStream方法,一般这么使用:
writer.setInfoStream(System.out);
4.高级索引概念
用IndexReader删除文档
前面已提到过IndexWriter可以删除文档,但是IndexReader删除文档的功能和IndexWriter还是有些不一样的。
- IndexReader能够根据文档号删除文档。IndexWriter则不能,因为一定进行段合并后,文档号是会发生变化的。
- IndexReader能通过Term对象删除文档,IndexWriter也可以,但是IndexReader能返回删除的文档数量,但是IndexWriter不能,因为IndexWriter删除文档只是对文档做一个删除标记,然后由后台线程周期性的去删除,可以说是异步的,所以IndexWriter不能。
- IndexWriter可以通过Query对象执行删除操作,但是IndexReader不行。
- IndexReader提供了一个undeleteAll,该操作可以取消被挂起的删除操作,有点类似回滚还没有被真正执行的删除操作。
索引提交和刷新
使用IndexWriter不管是添加、修改或删除索引,Lucene不是立马执行的,而是先放在一个缓冲区,待达到写入条件时,才真正的写入到索引,这些条件是比如:内存达到预设值、手动调用优化方法(optimize()、expungeDeletes())等。
Lucene符合ACID事物模型
- Atomic(原子性),同一时刻,只能有一个writer打开索引。
- Consistency(一致性),索引必须是连续的。
- Isolation(隔离性),当使用IndexWriter进行索引变更时,只有进行后续提交时,新打开的IndexReader才能看到上一次提交的索引变化。
- Durability(持久性),索引修改commit后,一旦通过周期性策略写入文件,索引将永久保存至文件中。当然,如果commit后,还没有写入文件,这时突然断电,会损失最后一次没有写入文件的修改,对之前已写入的没有影响。
合并段
为什么要合并段?
索引在不断的修改的过程中会不停的新建独立段,这些段越来越多的时候会出现两个问题:
- 超过打开文件句柄限制,每种操作系统都限制了运行于其之中的线程能打开的最大文件句柄数量。
- 影响搜索的性能
段合并策略
Lucene提供了两个核心的合并策略,他们分别是LogMergePolicy的子类,LogByteSizeMergePolicy和LogDocMergePolicy。
- LogByteSizeMergePolicy,根据索引段的大小来考虑是否划分索引段和合并索引。
- LogDocMergePolicy,根据索引段里面包含的文档多少来考虑是否划分索引段和合并索引。
一旦索引的级别数达到或者超过mergeFactor所预设的尺寸,这些段将被被合并。mergeFactor不但要控制如何将段按照此春分配和给个级别以用于触发合并操作,还要控制一次性合并的段数量。
段的级别计算公式:
(int)log(max(minMergeMB,size))/log(mergeFactor)
这能按照答题相当的尺寸将段按照同一级别进行分组。尺寸小于minMergeMB的小段通常被分配到级别为0的组。级别0的段大小为mergeFactor,级别1的段大小为mergeFactor的平方,级别2的段大小为mergeFactor的3次方。
段的合并
段的合并策略对段进行分级后,这通过MergeSchduler的一个子类来完成这个工作,默认情况下,IndexWriter通过ConcurrentMergeScheduler进行合并,该类是通过多线程实现的。让然使用着也可以实现自己的MergerScheduler,如果足够自信。
参考:
Lucene实战(第2版)著:Michael McCandles、Erik Hatcher、Otis Gospodnetic 译:牛长流、肖宇