Lucene
全文检索系统的结构包括:文本处理引擎,索引引擎,磁盘索引文件,查询引擎,二次应用开发接口。
Lucene是一个开放源代码的全文检索引擎工具包,不是一个完整的全文检索引擎,而是一个全文检索引擎的架构。
成功案例:Eclipse,Jive
Lucene包结构(七大块)
Lucene的倒排索引算法:
设有两篇文章1和2
文章1的内容为:Tom lives in
Guangzhou
,I live in
Guangzhou
too.
文章2的内容为:He once lived in
Shanghai
.
传统算法:
文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有关键词为:[he] [live] [shanghai]
倒排索引算法:
关键词 文章号[出现频率] 出现位置
guangzhou 1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1
一、环境
需要导入lucene.jar包(在lucene.apache.org下载)
二、基本概念
1.Lucene的工作流程:
(1) 建立索引:主要使用IndexWriter,在指定的目录建立索引的文件.
IndexWrite几种构造函数
IndexWrite(String str , Analyzer a , Boolean create)
IndexWrite(File f , Analyzer a, Boolean create)
IndexWrite(Directory d , Analyzer a, Boolean create)
a.提取文本. Lucene只能对文本信息建立索引,只要你能将要索引的文件转化成文本格式,Lucene 就能为你的文档建立索引。比如,如果你想为 HTML 文档或者 PDF 文档建立索引,那么首先你就需要从这些文档中提取出文本信息,然后把文本信息交给 Lucene 建立索引。
b.构建Document
c.分析并建立索引,在提取了需要Lucene建立索引的数据并且创建了Document之后,接下来就可以调用IndexWrite类的addDocument()方法来使Lucene建立索引了。
(2) 创建搜索的Query
(3 ) 利用IndexSearcher进行搜索
2.Lucene的字段Field类型
对于每个Field , Lucene提供了3种方式以供用户选择进行处理,这3种方式分别是“是否切词”,“是否索引”,“是否存储”。
Lucene有四种不同的字段类型:Keyword,UnIndexed,UnStored和Text,用于指定建立最佳索引。
字段类型 | 是否切词 | 是否索引 | 是否存储 | 主要用途 |
Field.Keyword(String name,String value) Field.Keyword(String Date) | 否 | 是 | 是 | 电话号码,居民身份证,人名,地名,日期等 |
Field.UnIndexed(String,String) | 否 | 否 | 是 | 文档的类型,例如:Word,PDF,HTML |
Field.UnStored(String,String) | 是 | 是 | 否 | 文档的标题和内容 |
Field.Text(String,String) | 是 | 是 | 是 | 文档的标题和内容 |
Field.Text(String,Reader) | 是 | 是 | 否 | 文档的标题和内容 |
3.基本概念(与传统表的对比):
Lucene | 传统表 | 说明 |
IndexWriter | table |
|
Document | 一条记录 |
|
Field | 每个字段 | 分为可被索引的,可切分的,不可被切分的,不可被索引的几种组合类型 |
Hits | RecoreSet | 结果集 |
调整性能参数,提高建立索引效率
IndexWriter提供了一些参数可供设置,列表如下
| 属性 | 默认值 | 说明 |
mergeFactor | org.apache.lucene.mergeFactor | 10 | 控制index的大小和频率,两个作用 1.一个段有多少document 2.多少个段合成一个大段 |
maxMergeDocs | org.apache.lucene.maxMergeDocs | Integer.MAX_VALUE | 限制一个段中的document数目 |
minMergeDocs | org.apache.lucene.minMergeDocs | 10 | 缓存在内存中的document数目,超过他以后会写入到磁盘 |
maxFieldLength |
| 1000 | 一个Field中最大Term数目,超过部分忽略,不会index到field中,所以自然也就搜索不到 |
这些参数的的详细说明比较复杂:mergeFactor有双重作用
(1)设置每mergeFactor个document写入一个段,比如每10个document写入一个段
(2)设置每mergeFacotr个小段合并到一个大段,比如10个document的时候合并为1小段,以后有10个小段以后合并到一个大段,有10个大段以后再合并,实际的document数目会是mergeFactor的指数
简单的来说mergeFactor 越大,系统会用更多的内存,更少磁盘处理,如果要打批量的作index,那么把mergeFactor设置大没错, mergeFactor 小了以后, index数目也会增多,searhing的效率会降低,但是mergeFactor增大一点一点,内存消耗会增大很多(指数关系),所以要留意不要”out of memory”
把maxMergeDocs设置小,可以强制让达到一定数量的document写为一个段,这样可以抵消部分mergeFactor的作用.
minMergeDocs相当于设置一个小的cache,第一个这个数目的document会留在内存里面,不写入磁盘。这些参数同样是没有最佳值的,必须根据实际情况一点点调整。
maxFieldLength可以在任何时刻设置,设置后,接下来的index的Field会按照新的length截取,之前已经index的部分不会改变。可以设置为Integer.MAX_VALUE
索引的存放位置(FSDirectory与RAMDirectory)
FSDirectory : IndexWriter writer = new IndexWriter(FSDirectory.getDirectory(path,true) , new StandardAnalyzer() , true);
RAMDirectory : 虚拟机退出以后,内存中的索引将不存在
IndexWriter writer = new IndexWriter(new RAMDirectory() , new StandardAnalyzer() , true);
索引的合并(利用IndexWriter.addIndexes())
FSDirectory fsDir = FSDirectory.getDirectory(“c://index”,true);
RAMDirectory ramDir = new RAMDirectory();
IndexWriter fsWriter = new IndexWriter(fsDIr , new StandardAnalyzer() , true);
IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer() , true);
Document doc1 = new Document();
doc1.add(Field.Text(“name”,”word1 word2 word 3” ));
Document doc2 = new Document();
doc2.add(Field.Text(“name” , “word1 word2 word3));
ramWriter.addDocument(doc1);
fsWrite.addDocument(doc2);
ramWriter.close();
fsWriter.addIndexes(new Directory[](fsDir));
fsWriter.close();
从索引里删除某个文档 (利用IndexReader.open()打开一个索引),并不是真正的删除某个文档,而只是标识这些文档为删除状态,可以通过IndexReader.undeleteAll()方法来恢复被删除的文档
根据文档id进行删除
IndexReader reader = IndexReader.open(“c://index”);
reader.delete(0);
reader.close();
按字段Filed来删除
IndexReader reader = IndexReader.open(“c://index”);
reader.delete(new Term(“name”,”word 1” ));
reader.close();
如果想从物理硬盘上真正的删除某个文档,还需要这样做:
File indexDir = new File("C://luceneIndex");
Analyzer luceneAnalyzer = new StandardAnalyzer();
IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,false);
indexWriter.optimize();
indexWriter.close();
创建了类 IndexWriter 的一个实例,并且打开了一个已经存在的索引。indexWriter.optimize();对索引进行清理,清理过程中将把所有标记为删除的文档物理删除。
Lucene 没有直接提供方法对文档进行更新,如果你需要更新一个文档,那么你首先需要把这个文档从索引中删除,然后把新版本的文档加入到索引中去。
索引的优化
Indexwriter.optimize()方法可以为查询优化索引(index),之前提到的参数调优是为indexing过程本身优化,而这里是为查询优化,优化主要是减少index文件数,这样让查询的时候少打开文件,优化过程中,lucene会拷贝旧的index再合并,合并完成以后删除旧的index,所以在此期间,磁盘占用增加, IO符合也会增加,在优化完成瞬间,磁盘占用会是优化前的2倍,在optimize过程中可以同时作search。
4.几种查询方式
查询方式 | 说明 |
TermQuery | 条件查询 例如:TermQuery tquery=new TermQuery(new Term("name","jerry")); name:字段名 jerry:要搜索的字符串 |
MultiTermQuery | 多个字段进行同一关键字的查询 Query query= null; Query =MultiFieldQueryParser.parse("我",new String[] {"title","content"},analyzer); Searcher searcher=new IndexSearcher(indexFilePath); |
BooleanQuery | 例如:BooleanQuery bquery=new BooleanQuery(); Query query = new TermQuery(newTerm(“name”,”aa”)); Query mquery = new TermQuery(new Term(“content”,”bbb”)); |
WildcardQuery | 语义查询(通配符查询) 例:Query query= new WildcardQuery(new Term("sender","*davy*")); |
PhraseQuery | 短语查询,多关键字查询 |
PrefixQuery | 前缀查询 Term term = new Term(“name”,”da”);Query query=new PrefixQuery(term); |
PhrasePrefixQuery | 短语前缀查询 |
FuzzyQuery | 模糊查询Term term = new Term(“name”,”da”);Query query=new FuzzyQuery (term); |
RangeQuery | 范围查询 Query query = new RangeQuery(beginTIme,endTime,true); |
SpanQuery | 范围查询 |
在全文检索时建议大家先采用语义时的搜索,先搜索出有意义的内容,之后再进行模糊之类的搜索
2个关键性应用搜索
1)多索引搜索:MultiSearcher
IndexSearcher[] searchers = new IndexSearcher[2];
searchers[0] = new IndexSearcher(m_indexpath);
searchers[1] = new IndexSearcher(m_outindexpath);
MultiSearcher multiSearcher = new MultiSearcher(searchers);
(2)多域搜索,MultiFieldQueryParser
Query query = MultiFieldQueryParse.parse(“Lucene”,new String[]{“title”,”content”},analyzer);
或者
BooleanQuery m_BooleanQuery = new BooleanQuery();
Query query = QueryParser.Parse(m_SearchText, "INSTRUMENT_NAME", analyzer);
Query query2 = QueryParser.Parse(m_SearchText2, "INSTRUMENT_NAME2", analyzer);
m_BooleanQuery.Add(query, true, false);
m_BooleanQuery.Add(query2, true, false);
5.Lucene 的检索结果排序
Lucene的排序主要是对org.apache.lucene.search.Sort的使用。Sort可以直接根据字段Field生成,也可以根据标准的SortField生成,但是作为Sort的字段,必须符合以下的条件:唯一值以及Indexed。可以对Integers, Floats, Strings三种类型排序。
对整数型的ID检索结果排序只要进行以下的简单操作:
Sort sort = new Sort("id");
对多个字段排序:Sort sort = new Sort(new SortField[]{new SortField(“title”),new SortField(“content”)});
Hits hits = searcher.search(query, sort);
用户还可以根据自己定义更加复杂的排序,只要实现SortComparatorSource接口就可以了
6.分析器(Analyzer) 包括 分词器(Tokenizer) 和 过滤器(TokenFilter)
Lucene使用分析器来处理被索引的文本。在将其存入索引之前,分析器用于将文本标记化、摘录有关的单词、丢弃共有的单词、处理派生词(把派生词还原到词根形式,意思是把bowling、bowler和bowls还原为bowl)和完成其它要做的处理。Lucene提供的通用分析器是:
SimpleAnalyzer:用字符串标记一组单词并且转化为小写字母。
StandardAnalyzer:用字符串标记一组单词,可识别缩写词、email地址、主机名称等等。并丢弃基于英语的stop words (a, an, the, to)等、处理派生词。
ChineseAnalyzer.class,它是一个单字分析法,它把句子中的词全部分成一个一个的字符,以单个字为单位存储。
CJKAnalyzer.class,它是双字分析法,它把中文以双字为单位拆分得到结果,从而建立词条。当然这些得到的双字词中会有很多不符合中文语义单位的双字被送进索引。
源码分析过滤器:
TokenStream result = new StandardTokenizer(reader);
result = new StandardFilter(result);
result = new LowerCaseFilter(result);
result = new StopFilter(result,stopSet);
中文分词方法:
1. 单字切分
2. 二分法
3. 词库分词
十、需要注意的问题:
1 .IndexWriter在添加新的document后,需要重新建立Index,则需要调用writer.optimize();方法
2. Lucene没有update索引的方法,需要删除后重新建立,参考remove方法
3 .用IndexReader删除Document后,需要重新用IndexWriter进行整理,否则无法在进行搜索(不知道是不是我设置问题)
4.Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
5.并发操作Lucene
(1)所有只读操作都可以并发
(2)在index被修改期间,所有只读操作都可以并发
(3)对index修改操作不能并发,一个index只能被一个线程占用
(4)ndex的优化,合并,添加都是修改操作
(5)但需要注意的是,在创建搜索的时候用:
searcher = new IndexSearcher(IndexReader.open("E://lucene//test4//index"));
searcher.close();
这时候是不能关闭searcher的.
如果想让searcher能关闭,就不要用IndexReader了:
searcher = new IndexSearcher("E://lucene//test4//index");
6.Locking机制
lucence内部使用文件来locking,默认的locking文件放在java.io.tmpdir,可以通过-Dorg.apache.lucene.lockDir=xxx指定新的dir,有write.lock commit.lock两个文件,lock文件用来防止并行操作index,如果并行操作, lucene会抛出异常,可以通过设置-DdisableLuceneLocks=true来禁止locking,这样做一般来说很危险,除非你有操作系统或者物理级别的只读保证,比如把index文件刻盘到CDROM上。
实例:
1.判断索引文件是否存在:
/**
* 检查索引是否存在.
* @param indexDir
* @return
*/
public static boolean indexExist(String indexDir)
{
return IndexReader.indexExists(indexDir);
}
private IndexWriter getWriter(String indexFilePath) throws Exception
{
boolean append=true;
File file=new File(indexFilePath+File.separator+"segments");
if(file.exists())
append=false;
return new IndexWriter(indexFilePath,analyzer,append);
}
2.删除索引
/**
* 删除索引.
* @param aTerm 索引删除条件
* @param indexDir 索引目录
*/
public static void deleteIndex(Term aTerm, String indexDir)
{
List aList = new ArrayList();
aList.add(aTerm);
deleteIndex(aList, indexDir);
}
/**
* 删除索引.
* @param aTerm 索引删除条件.
* @param indexDir 索引目录 *
*/
public static void deleteIndex(List terms, String indexDir)
{
if (null == terms) {
return;
}
if(!indexExist(indexDir)) { return; }
IndexReader reader = null;
try {
reader = IndexReader.open(indexDir);
for (int i = 0; i < terms.size(); i++){
Term aTerm = (Term) terms.get(i);
if (null != aTerm){
reader.delete(aTerm);
}
}
} catch (IOException e){
LogMan.warn("Error in Delete Index", e);
} finally {
try{
if (null != reader){
reader.close();
}
}catch (IOException e){
LogMan.warn("Close reader Error");
}
}
}
删除索引需要一个条件,类似数据库中的字段条件,例如删除一条新闻的代码如下:
public static void deleteNewsInfoIndex(int nid)
{
Term aTerm = new Term("nid", String.valueOf(nid));
deleteIndex(aTerm,indexDir);
}
3. 检查某个分析器的分词效果
public class ShowResult
{
public static void show(Analyzer a , String s) throws Exception
{
StringReader reader = new StringReader(s);
TokenStream ts = a.tokenStream(reader);
Token t = ts.next();
while(t != null)
{
System.out.println(t.termText());
t = ts.next();
}
}
public static void main(String[] args) throws Exception
{
Analyzer a = new SimpleAnalyzer();
// Analyzer a = new WhitespaceAnalyzer();
// Analyzer a = new CJKAnalyzer();
String key = "中华人 民共和国";
ShowResult.show(a,key);
}
}
4. 高亮设置
Hits hits = searcher.search(query);
//高亮显示设置
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<red>","</red>");
highlighter = new Highlighter(simpleHTMLFormatter,new QueryScorer(query));
highlighter.setTextFragmenter(new SimpleFragmenter(10));
5.索引监视器
Web形式下的LIMO
桌面应用下的Luke
命令行下的LUCLI