1 Lucene 的认识
提到 Lucene 很多人都知道这个开源的搜索工具,其魅力也是很大的。它让我们对搜索引擎的认识不在那么神秘,也不会在觉得百度和 google 的技术多么的高深没测,其实其原理都是一样的,只是他们要做的更好,走的更远罢了。
Lucene 可以对任何的数据做索引和搜索,说这样的话其实不过分,真的就是这样,只要你能处理好这些数据,交给 Lucene 去建立索引它都可以帮你把这些数据给检索出来,是不是很好玩了。真正好玩的地方还在后面呢。
2 Lucene 的学习
前面已经对 Lucene 有了一些了解,现在我们想象它怎么去搜索这些数据呢,如果知道倒排索引,你就知道了,其实 lucene 检索的是它自己建立的索引,从索引中的到数据的指针,从而得到数据。其实就这么简单。
提到索引,现在的索引技术中有:倒排索引、后缀数组和签名文件这三种,其中后缀数组这种技术虽然检索速度也很快,但是它的数据结构构造和维护都是相当麻烦的所以不可取了。我也懒得去看了。至于签名文件嘛,那是 80 年代的玩意了,现在已经过时了。现在可是倒排索引的天下啊!相信百度和 google 都是这种技术。
3 索引的建立
我们从索引的建立入手:
我们建立一个 lucene 的索引时必须先建立该索引文件存放的位置,看一下代码:
IndexWriter writer = null;
writer = new IndexWriter("c:\\index", new CJKAnalyzer(), true);
这段代码就时建立一个索引前所必须的操作,先声明这个 IndexWriter ,实例化它你必须传入三个参数。他们分别代表:你要建立索引文件的存放位置、你要使用索引建立的分词方法、是否重新建立索引。这样你就告诉 lucene 我要在 c 盘的 index 目录下建立索引文件,我要使用车东老师的二分词算法做分析器、我要在这个目录下删除以前的索引或任何文件创立我的索引文件。
索引的建立有三种方式,让我一一道来:
1 、 new IndexWriter(new RAMDirectory(), new StandardAnalyzer(), true);
在内存中建立索引,速度最快但是耗资源,而且重启就没了。
2 、 new IndexWriter(FSDirectory.getDirectory(path, true), new StandardAnalyzer(), true);
在文件系统中建立索引,这里有两个参数,分别是:建立索引的路径、是否要删除当前目录下的文件重新建立索引。
3 、 new IndexWriter("c:\\index", new CJKAnalyzer(), true);
最常见的一种,在制定目录下建立索引,看了源码你就知道这种方法也是用的第二种方式。 Lucene 的源码:
public IndexWriter(String path, Analyzer a, boolean create)
throws IOException {
this(FSDirectory.getDirectory(path, create), a, create, true);
}
我想的没错。
Indexwriter 性能调整参数:
第一个优化的参数 : mergeFactor 这个参数用于控制 lucene 在把索引从内存写入到磁盘上的文件系统时内存最大的 Document 对象的数量。这个数要根据你的计算机设置,默认情况下是 10 。
第二个优化的参数 : maxMergeFactor 这个参数用来设置当有多少个 Segment 时进行合并操作。当然我们知道当索引文件太多的话其检索的速度就会很慢,所以我们要当文件数量一定时让它进行索引的合并。这样就可以加快索引速度,但是这个值要根据你的情况而定。当文档数量较多时我们将值设大些,当文档数量较少时我们将值设小些。
第三个优化的参数 : minMergeDocs 这个参数用于控制内存中文档的数量。
这样我们建立索引已经完成,接下来我们要建立 Document 对象,因为你必须告诉我要搜索什么吧!好了,看看源码:
File file = new File("1.txt");
Document doc = new Document();
doc.add(Field.UnIndexed("filename", file.getName()));
FileInputStream fis = new FileInputStream(file);
byte[] b = new byte[fis.available()];
fis.read(b);
String content = new String(b);
doc.add(Field.Text("content", content));
fis.close();
以上我们就完成了将 1.txt 文件放到我们的 Document 对象了。这里我们用了 Field.Text(); 这样的操作和 doc.add(); 这样的方法建立的。这也是建立索引的必须。
稍微介绍一下 Field ,它就是你要建立索引的字段。它分别有
类型 / 方法 |
是否分词 |
是否索引 |
是否存储 |
常用实例 |
Keyword(String,String)
Keyword(String,Date) |
否 |
是 |
是 |
电话号码,身份证,人名,地名,日期 |
Unindexed(String,String) |
否 |
否 |
是 |
文档类型,文档名称 |
UnStored(String,String) |
是 |
是 |
否 |
文档的标题和内容 |
Text(String,String) |
是 |
是 |
是 |
文档的标题和内容 |
Text(String,Reader) |
是 |
是 |
否 |
文档的标题和内容 |
这样我们要建什么样的索引就对号入座吧,只要最后我们使用 doc.add(Field.Text("content", content)); 把它添加到 Document 中就可以了。
这时我们的文档已经建立好了,现在就开始向索引中添加文档吧!这里我们使用
writer.addDocument(doc); 来向 Indexwriter 索引中添加构造好的文档。
这样我们是不是就可以说我们已经建立完了索引呢,其实不然,我们还要优化优化,这样才快嘛!对不对?
writer.optimize(); 这样一句话就可以实现索引优化了,具体的优化过程我就不说了,是不是很简单。但是一定不要忘了哦。调用这个方法时最好建立一个合适的周期。定期进行优化。
好了,这样我们就完成了索引的建立了。
下面我们看看缩影的合并吧!
当我们在很多地方建立了很多的索引后,想要合并这些索引我们怎么办呢?
使用 IndexWriter.assIndexs(New Directory[]{path});
就可以对 path 路径下的索引合并到当前的索引中了。
下面再看看索引的删除吧!
有一些过时的索引我们需要删除,怎么办呢?
IndexReader reader = IndexReader.open("c:\\index");
reader.delete(0);
这样我们就可以按照文档的顺序删除对应的文档了,但是这样不太现实,不对吗?我们怎么会知道文档的顺序呢?
下面我们看看第二中方法:
IndexReader reader = IndexReader.open("c:\\index");
reader.delete(new Term("name","word1"));
reader.close();
按照字段来删除对应的文档,这样合理多了。以后要删除时就按照词条的方式去删除吧 !
索引锁: write.lock , commit.lock.
write.lock 是为了避免几个线程同时修改一个索引文档而设置。当实例一个 indexwrite 时建立和使用 indexReader 删除文档时建立。
Commit.lock 该锁主要在 segment 在建立,合并或读取时生成。
4 Lucene 的搜索
以上完成了索引的建立和一些关于索引的知识,但是光有索引是不行的,我们真正要做的检索,这才是我们的关键。现在我们看看 lucene 的检索吧。
认识检索从检索的工具开始吧! IndexSearcher 类是 lucene 用于检索的工具类,我们在检索之前要得到这个类的实例。
第一步我们看以下代码:
IndexSearcher searcher = new IndexSearcher("c:\\index");
创建 IndexSearcher 实例需要告诉 lucene 索引的位置,就是你 IndexWrite 的文件路径。
Query query = null;
Hits hits = null;
query = QueryParser.parse(key1, "name", new StandardAnalyzer());
hits = searcher.search(query);
if (hits != null) {
if (hits.length() == 0) {
System.out.println(" 没有找到任何结果 ");
} else {
System.out.print(" 找到 ");
for (int i = 0; i < hits.length(); i++) {
Document d = hits.doc(i);
String dname = d.get("title");
System.out.print(dname + " " );
}
}
}
}
以上就是一个完整的检索过程,这里我们看见了个 Query 和 Hits ,这两个类就是比较关键的了,我们先从检索结果的 Hits 类说起。
我们使用 Hits 经常使用的几个方法有:
length() : 返回搜索结果的总数量。
Doc(int n) : 放回第 n 个文档。
Id(int n) : 返回第 n 个文档的内部编号。
Sorce(int n) : 返回第 n 个文档的得分。
看见这个 Sorce(int n) 这个方法,是不是就可以联想到搜索引擎的排序问题呢,像百度的推广是怎么做出来的呢 , 可想而知吧,那就说明必定存在一中方法可以动态的改变某片文档的得分。对了, lucene 中可以使用 Document 的 setBoost 方法可以改变当前文档的 boost 因子。
下面我们看看:
Document doc1 = new Document();
doc1.add(Field.Text("contents", "word1 word"));
doc1.add(Field.Keyword("path", "path\\document1.txt"));
doc1.setBoost(1.0f);
这样我们就在改变了篇文档的评分了,当 boost 的值越大它的分值就越高,其出现的位置就越靠前。
让我们再来看看 lucene 为我们提供的各种 Query 吧。
第一、 按词条搜索 - TermQuery
query = new TermQuery(new Term("name","word1"));
hits = searcher.search(query);
这样就可以把 field 为 name 的所有包含 word1 的文档检索出来了。
第二、 “与或”搜索 - BooleanQuery
它实际是一个组合 query 看看下面的代码:
query1 = new TermQuery(new Term("name","word1"));
query2 = new TermQuery(new Term("name","word2"));
query = new BooleanQuery();
query.add(query1, false, false);
query.add(query2, false, false);
hits = searcher.search(query);
看看 booleanQuery 的用法吧:
true & true : 表明当前加入的字句是必须要满足的。相当于逻辑与。
false & true : 表明当前加入的字句是不可一被满足的, 相当于逻辑非。
false & false : 表明当前加入的字句是可选的,相当于逻辑或。
true & true : 错误的情况。
Lucene 可以最多支持连续 1024 的 query 的组合。
第三、 在某一范围内搜索 - RangeQuery
IndexSearcher searcher = new IndexSearcher("c:\\index");
Term beginTime = new Term("time","200001");
Term endTime = new Term("time","200005");
Hits hits = null;
RangeQuery query = null;
query = new RangeQuery(beginTime, endTime, false);
hits = searcher.search(query);
RangeQuery 的构造函数的参数分别代表起始、结束、是否包括边界。这样我们就可以按照要求检索了。
第四、 使用前缀检索 - PrefixQuery
这个检索的机制有点类似于 indexOf() 从前缀查找。这个常在英文中使用,中文中就很少使用了。代码如下:
IndexSearcher searcher = new IndexSearcher("c:\\index");
Term pre1 = new Term("name", "Da");
query = new PrefixQuery(pre1);
hits = searcher.search(query);
第五、 多关键字的搜索 - PhraseQuery
可以多个关键字同时查询。使用如下:
query = new PhraseQuery();
query.add(word1);
query.add(word2);
query.setSlop(0);
hits = searcher.search(query);
printResult(hits, "'david' 与 'mary' 紧紧相隔的 Document");
query.setSlop(2);
hits = searcher.search(query);
printResult(hits, "'david' 与 'mary' 中相隔两个词的短语 ");
这里我们要注意 query.setSlop(); 这个方法的含义。
query.setSlop(0); 紧紧相连 (这个的条件比较苛刻)
query.setSlop(2); 相隔
第六、 使用短语缀搜索 - PharsePrefixQuery
使用 PharsePrefixQuery 可以很容易的实现相关短语的检索功能。
实例:
query = new PhrasePrefixQuery();
// 加入可能的所有不确定的词
Term word1 = new Term("content", "david");
Term word2 = new Term("content", "mary");
Term word3 = new Term("content", "smith");
Term word4 = new Term("content", "robert");
query.add(new Term[]{word1, word2});
// 加入确定的词
query.add(word4);
query.setSlop(2);
hits = searcher.search(query);
printResult(hits, " 存在短语 'david robert' 或 'mary robert' 的文档 ");
第七、 相近词语的搜索 - fuzzyQuery
可以通俗的说它是一种模糊查询。
实例:
Term word1 = new Term("content", "david");
Hits hits = null;
FuzzyQuery query = null;
query = new FuzzyQuery(word1);
hits = searcher.search(query);
printResult(hits," 与 'david' 相似的词 ");
第八、 使用通配符搜索 - WildcardQuery
实例:
IndexSearcher searcher = new IndexSearcher("c:\\index");
Term word1 = new Term("content", "*ever");
Term word2 = new Term("content", "wh?ever");
Term word3 = new Term("content", "h??ever");
Term word4 = new Term("content", "ever*");
WildcardQuery query = null;
Hits hits = null;
query = new WildcardQuery(word1);
hits = searcher.search(query);
printResult(hits, "*ever");
query = new WildcardQuery(word2);
hits = searcher.search(query);
printResult(hits, "wh?ever");
query = new WildcardQuery(word3);
hits = searcher.search(query);
printResult(hits, "h??ever");
query = new WildcardQuery(word4);
hits = searcher.search(query);
printResult(hits, "ever*");
由上可以看出通配符?代便 1 个字符, * 代表 0 到多个字符。
Lucene 现在支持以上八中的搜索方式,我们可以根据需要选择适合自己的搜索方式。当然上面提供的一些可能对英文还是比较有效,中文就不可取了,所以我们开始想想百度,我们只在一个输入框中搜索结果。有了这个疑问我们揭开下一章的讨论吧!
查询字符串的解析:这个就是我们经常在一个输入框中输入我们要检索的文字,交给搜索引擎去帮我们分词。
QueryParser 类就是对查询字符串的解析类。
看看它的用法:
query = QueryParser.parse(key1, "name", new StandardAnalyzer());
hits = searcher.search(query);
它直接返回一个 Query 对象。需要传入的参数分别是:
用户需要查询的字符串、需要检索的对应字段名称、采用的分词类。
Analyzer analyzer = new CJKAnalyzer();
String[] fields = {"filename", "content"};
Query query = MultiFieldQueryParser.parse(searchword, fields, analyzer);
Hits hits = searcher.search(query);
QueryParser 的“与” 和 “或”:
QueryParser 之间默认是或,我们想改变为与的话加入以下代码:
QueryParser.setOperator(QueryParser.DEFAULT_OPERATOR_AND);
就可以了。
5 高级搜索技巧
前面我们已经介绍了一般情况下 lucene 的使用技巧,现在我们探讨一下高级搜索的技巧吧!
1、 对搜索结果进行排序:
1) 使用 sort 类排序:
Sort sort = new Sort();
hits = searcher.search(query,sort);
这种方式是使用默认的 sort 排序方式进行排序。默认的 sort 排序是按照相关度进行排序。即通过 luence 的评分机制进行排序。
2) 对某一字段进行排序
Sort sort = new Sort( “ content ” );
hits = searcher.search(query,sort);
3) 对多个字段进行排序
Sort sort = new Sort(new SortField[]{new SortField("title"),new SortField("contents")});
hits = searcher.search(query,sort);
2、 多域搜索和多索引搜索:
在使用 luecene 时,如果查询的只是某些 terms ,而不关心这些词条到时来自那个字段中时。这时可以使用 MultiFieldQueryParser 类。这个用于用户搜索含有某个关键字是否存在在字段中,他们之间的关系使用 OR 连接。即不管存在在哪一个字段都会显示显示出来。
使用 MultiSearcher 可以满足同时多索引的搜索需求。
Searcher[] searchers = new Searcher[2];
searchers[0] = new IndexSearcher(indexStoreB);
searchers[1] = new IndexSearcher(indexStoreA);
// 创建一个多索引检索器
Searcher mSearcher = new MultiSearcher(searchers);
3、 对搜索结果进行过滤:
1) 对时间进行过滤
通常情况下我们对搜索结果要进行过滤显示,即只显示过滤后的结果。
doc.add(Field.Keyword("datefield", DateField.timeToString(now - 1000)));
DateFilter df1 = DateFilter.Before("datefield", now);
2) 查询过滤器
通过查询过滤器可以过滤一部分的信息。
Filter filter = new Filter()
{
public BitSet bits (IndexReader reader) throws IOException
{
BitSet bitset = new BitSet(5);
bitset.set (1);
bitset.set (3);
return bitset;
}
};
// 生成带有过滤器的查询对象
Query filteredquery = new FilteredQuery (query, filter);
// 返回检索结果
Hits hits = searcher.search (filteredquery);
这样我们就可以使用自己定义的过滤方式去过滤信息了。
3) 带缓存的过滤器:
使用待缓存的过滤器我们可以重用过滤功能,如下:
MockFilter filter = new MockFilter();
CachingWrapperFilter cacher = new CachingWrapperFilter(filter);
cacher.bits(reader);
以上介绍完了现在学习 luence ,没有太详细的介绍它的实现,因为它对于我们来说是一个工具,既然是工具我们就要会用就可以了。
Boosting特性
luncene对Documen
t和Field提供了一个可以设
置的Boosting参数, 这个参数的用处是告诉lucen
e, 某些记录更重要,在搜索的时候优
先考虑他们 比如在搜索的时候你可能觉得几个
门户的网页要比垃圾小站更优先考
虑
lucene默认的boosting参数是1.0, 如果你觉得这个field重要,你可以把boosting设置为1.5, 1.2….等, 对Document设置boosting相当设定了它的每个Field的基准boosting,到时候实际Field的boosting就是(Document-boosting*Field-boosting)设置了一遍相同的boosting.
似乎在lucene的记分公式里面有boosting参数,不过我估计一般人是不会去研究他的公式的(复杂),而且公式也无法给出最佳值,所以我们所能做的只能是一点一点的改变boosting, 然后在实际检测中观察它对搜索结果起到多大的作用来调整
一般的情况下是没有必要使用boosting的, 因为搞不好你就把搜索给搞乱了, 另外如果是单独对Field来做Bossting, 也可以通过将这个Field提前来起到近似的效果
Indexing Date
日期是lucene需要特殊考虑
的地方之一, 因为我们可能需要对日期进行范围
搜索, Field.keyword(s
tring,Date)提供了这
样的方法,lucene会把这个
日期转换为string, 值得注意的是这里的日期是精确到
毫秒的,可能会有不必要的性能损
失, 所以我们也可以把日期自行转化为
YYYYMMDD这样的形势,就
不用精确到具体时间了,通过Fi
le.keyword(Stir
ng,String) 来index, 使用PrefixQuery 的YYYY一样能起到简化版的日
期范围搜索(小技巧), lucene提到他不能处理19
70年以前的时间,似乎是上一代
电脑系统遗留下来的毛病
Indexing 数字
- 如果数字只是简单的数据, 比如中国有56个民族. 那么可以简单的把它当字符处理
- 如果数字还包含数值的意义,比如价格, 我们会有范围搜索的需要(20元到30元之间的商品),那么我们必须做点小技巧, 比如把3,34,100 这三个数字转化为003,034,100 ,因为这样处理以后, 按照字符排序和按照数值排序是一样的,而lucene内部按照字符排序,003->034->100 NOT(100->3->34)
排序
Lucene默认按照相关度(s
core)排序,为了能支持其他
的排序方式,比如日期,我们在a
dd Field的时候,必须保证fi
eld被Index且不能被to
kenized(分词),并且排
序的只能是数字,日期,字符三种
类型之一
Lucene的IndexWriter调整
IndexWriter提供了一
些参数可供设置,列表如下
|
属性 |
默认值 |
说明 |
mergeFactor |
org.apache.lucene.mergeFactor |
10 |
控制index的大小和频率,两个作用 |
maxMergeDocs |
org.apache.lucene.maxMergeDocs |
Integer.MAX_VALUE |
限制一个段中的document数目 |
minMergeDocs |
org.apache.lucene.minMergeDocs |
10 |
缓存在内存中的document数目,超过他以后会写入到磁盘 |
maxFieldLength |
|
1000 |
一个Field中最大Term数目,超过部分忽略,不会index到field中,所以自然也就搜索不到 |
这些参数的的详细说明比较复杂:mergeFactor有双重作用
- 设置每mergeFactor个document写入一个段,比如每10个document写入一个段
- 设置每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
RAMDirectory 和 FSDirectory 转化
RAMDirectory(RA
MD)在效率上比FSDirec
tyr(FSD)高不少, 所以我们可以手动的把RAMD当
作FSD的buffer,这样就
不用去很费劲的调优FSD那么多
参数了,完全可以先用RAM跑好
了index, 周期性(或者是别的什么算法)来
回写道FSD中。 RAMD完全可以做FSD的bu
ffer。
为查询优化索引(index)
Indexwriter.opt
imize()方法可以为查询优
化索引(index),之前提到
的参数调优是为indexing
过程本身优化,而这里是为查询优
化,优化主要是减少index文
件数,这样让查询的时候少打开文
件,优化过程中,lucene会
拷贝旧的index再合并,合并
完成以后删除旧的index,所
以在此期间,磁盘占用增加, IO符合也会增加,在优化完成瞬
间,磁盘占用会是优化前的2倍,
在optimize过程中可以同
时作search。
并发操作Lucene和locking机制
- 所有只读操作都可以并发
- 在index被修改期间,所有只读操作都可以并发
- 对index修改操作不能并发,一个index只能被一个线程占用
- index的优化,合并,添加都是修改操作
IndexWriter和IndexReader的实例可以被多线程共享,他们内部是实现了同步,所以外面使用不需要同步
Locing
lucence内部使用文件来l
ocking, 默认的locking文件放在j
ava.io.tmpdir,可
以通过-Dorg.apache
.lucene.lockDir
=xxx指定新的dir,有wr
ite.lock commit.lock两个文件
,lock文件用来防止并行操作
index,如果并行操作, lucene会抛出异常,可以通
过设置-DdisableLuc
eneLocks=true来禁
止locking,这样做一般来
说很危险,除非你有操作系统或者
物理级别的只读保证,比如把in
dex文件刻盘到CDROM上。
调试IndexWriter
IndexWriter 有一个infoStream的变
量,调试信息从这里输出。可以把
System.out设置给它