在应用中加入全文检索功能
t<O5_}R%d ——基于Java的全文索引引擎Lucene简介
q #f
U* 作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com
tt=?*n H'myd=*h~8 写于:2002/08 最后更新: 11/29/2006 17:23:30
GS |sx Feed Back >> (Read this before you ask question)
Ti/t\'6 r3o_mO?X 版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
L&1VPli http://www.chedong.com/tech/lucene.html (~/VP3.S uLYz!E+E 关键词:Lucene java full-text search engine Chinese word segment
e{edI{g !1f8~"Z 内容摘要:
$'3`$
+zxj-diM Lucene是一个基于Java的全文索引工具包。
LOyL:~$ xq:.|{HUk 基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史
<dx
xXzLT 全文检索的实现:Luene全文索引和数据库索引的比较
so|5HR| 中文切分词机制简介:基于词库和自动切分词算法的比较
F_ ~L&jHP 具体的安装和使用简介:系统结构介绍和演示
=z'w-ARy Hacking Lucene:简化的查询分析器,删除的实现,定制的排序,应用接口的扩展
MnvFmYgxA 从Lucene我们还可以学到什么
ZF
:e6em 基于Java的全文索引/检索引擎——Lucene
!j8h$+:K 37)Dx Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/
*F+t`<2 QRnkj]b 检索功能。
:.!]+#Me )rv5QH`i Lucene的作者:Lucene的贡献者Doug Cutting是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的
:8}QKp *loPwV8 主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用
G#/}_P -ea>}S 程序加入全文检索功能。
8P r H"pI @NGK2J Lucene的发展历程:早先发布在作者自己的
www.lucene.com,后来发布在SourceForge,2001年年底成为APACHE基金会jakarta的一个子项目:
>W"gr]R< (#* 7LdZ http://jakarta.apache.org/lucene/ q10gKVJum W=M`Bkw{ 已经有很多Java项目都使用了Lucene作为其后台的全文索引引擎,比较著名的有:
THirh6 b:.aZ7+4 Jive:WEB论坛系统;
&eV& +j Eyebrows:邮件列表HTML归档/浏览/查询系统,本文的主要参考文档“TheLucene search engine: Powerful, flexible, and free”作者就是
HlqvXt\ Ktg{-Xl EyeBrows系统的主要开发者之一,而EyeBrows已经成为目前APACHE项目的主要邮件列表归档系统。
I0 a,mO;m Cocoon:基于XML的web发布框架,全文检索部分使用了Lucene
v8"plx=3 Eclipse:基于Java的开放开发平台,帮助部分的全文索引使用了Lucene
\P]w^ >ir'v5 对于中文用户来说,最关心的问题是其是否支持中文的全文检索。但通过后面对于Lucene的结构的介绍,你会了解到由于Lucene良好架构设计
M:|Z3p K FR9<$ ,对中文的支持只需对其语言词法分析接口进行扩展就能实现对中文检索的支持。
X l#P@60 @'U9*:}U 全文检索的实现机制
*)k}@tY ZSq7>} Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的
t>|Y-i3cb Go3EWM`Cd8 映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。
{hB7F"S ghm5g/ 比较一下Lucene和数据库:
y0qrl4S)v brJ_q0@ Lucene 数据库
O(;K]8 索引数据源:doc(field1,field2...) doc(field1,field2...) \ indexer / _____________
hK9Trr wau Dt)\q^bH) | Lucene Index| -------------- / searcher \ 结果输出:Hits(doc(field1,field2) doc
knX0b$$ 6>v`6 (field1...))
J&}/Xw) 索引数据源:record(field1,field2...) record(field1..) \ SQL: insert/ _____________
Pl<r*d)h 6\ /x | DB Index | ------------- / SQL: select \结果输出:results(record(field1,field2..) record
@cdd~9w yiGq?WA7 (field1...))
naCPSsei ^,')1r, Document:一个需要进行索引的“单元”
24"Trg\WK[ 一个Document由多个字段组成 Record:记录,包含多个字段
O[f* ! Field:字段 Field:字段
Q=J"#EFs Hits:查询结果集,由匹配的Document组成 RecordSet:查询结果集,由多个Record组成
f7 V3 6Q8 8;;!2>N 全文检索 ≠ like "%keyword%"
uZ( I|N$ H];|<G 通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页,上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。
R*IO%9O Qj~m;F! 而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所
d_=@1JM> 8R Wfv}:X 以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题。
%)T>Wn%b]v ')t
:!#
由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类
+[*VU2f t }\}pSqW 似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹
`E>HpRcxD L<!}!v5ja 配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
ZB GLwe Xn-GSW3{ 所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外
)ALPMmlRs M>dP
1 一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至
IRNL(9H |WH'aGG 包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大
QlJ
cj+_h "P HkbU 提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。
{8UYu2t &Yi)|TU3'R 由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特
qLBXyQ;U Y~Y-L<`I 征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。
#pP4\n-~hU F<q'ivj:w 可以通过一下表格对比一下数据库的模糊查询:
*JE%bQ2Q Twyx(~'&R Lucene全文索引引擎 数据库
;vvO#3DWM 索引 将数据源中的数据都通过全文索引一一建立反向索引 对于LIKE查询来说,数据传统的索引是根本用不上的。数据需要逐个便利记录进行
pC
l[DE ,80qwN, GREP式的模糊匹配,比有索引的搜索速度要有多个数量级的下降。
q65KxOf` 匹配效果 通过词元(term)进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。 使用:like "%net%" 会把netherlands也
K:
g_M Nq1la8oQ3 匹配出来,
zWpJ\/k~ 多个关键词的模糊匹配:使用like "%com%net%":就不能匹配词序颠倒的xxx.net..xxx.com
zbK=yOIOd 匹配度 有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。 没有匹配程度的控制:比如有记录中net出现5词和出现1次的,结果是
/^^t>L o&P}GcEIw 一样的。
OQMkpX-dH 结果输出 通过特别的算法,将最匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的。 返回所有的结果集,在匹配条目非常多的
I&~kwOP `<!Nk^2ap 时候(比如上万条)需要大量的内存存放这些临时结果集。
~>&7~N8 可定制性 通过不同的语言分析接口实现,可以方便的定制出符合应用需要的索引规则(包括对中文的支持) 没有接口或接口复杂,无法定制
=r"8J5[f 结论 高负载的模糊查询应用,需要负责的模糊查询的规则,索引的资料量比较大 使用率低,模糊匹配规则简单或者需要模糊查询的资料量少
_O)xE9t#ru [2?|BUtD[ 全文检索和数据库应用最大的不同在于:让最相关的头100条结果满足98%以上用户的需求
XlUM ~(7+v [
qt
hn[3 Lucene的创新之处:
_#@n^c k`JP 大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一
Y$hYW ~$n4Yuu2[ 个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略
=!T@'P? !E!i`yF ,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。
DhY.5 .?R~!K{` Lucene和其他一些全文检索系统/应用的比较:
iSu7K&X9q $Llv6<B Lucene 其他开源全文检索系统
-SZXUN 增量索引和批量索引 可以进行增量的索引(Append),可以对于大量数据进行批量索引,并且接口设计用于优化批量索引和小批量的增量索引。
yG\^PD
wqB{cr}! 很多系统只支持批量的索引,有时数据源有一点增加也需要重建索引。
f =@'F= 数据源 Lucene没有定义具体的数据源,而是一个文档的结构,因此可以非常灵活的适应各种应用(只要前端有合适的转换器把数据源转换成相
51j5AbFQ" )QYg[<e6 应结构), 很多系统只针对网页,缺乏其他格式文档的灵活性。
4k/B=%l 索引内容抓取 Lucene的文档是由多个字段组成的,甚至可以控制那些字段需要进行索引,那些字段不需要索引,近一步索引的字段也分为需要
[xzgk[>5 \J[m4tw^ 分词和不需要分词的类型:
!.1oW( 需要进行分词的索引,比如:标题,文章内容字段
^Pl(V@ 不需要进行分词的索引,比如:作者/日期字段 缺乏通用性,往往将文档整个索引了
T<(1)N1H` 语言分析 通过语言分析器的不同扩展实现:
#\s*>Z 可以过滤掉不需要的词:an the of 等,
't:$Lx 西文语法分析:将jumps jumped jumper都归结成jump进行索引/检索
K
;\~otR^ 非英文支持:对亚洲语言,阿拉伯语言的索引支持 缺乏通用接口实现
2Ya)I k{ 查询分析 通过查询分析接口的实现,可以定制自己的查询语法规则:
lM1~K 比如: 多个关键词之间的 + - and or关系等
cb!mV5M-g 并发访问 能够支持多用户的使用
FJ0Ity4u6 gU\pP,a gY\X? -&4>>h9_ 关于亚洲语言的的切分词问题(Word Segment)
<knf^D<" $/;D8P5/&= 对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文
nZZNx
a (AKVk\ 语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。
,Y *unk<S f%vJmpg 首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。
G165grGFd ~hK7(K 但一句话:“北京天安门”,计算机如何按照中文的语言习惯进行切分呢?
Aq'yr,
“北京 天安门” 还是“北 京 天安门”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别
0VsQ$4'V^ ?>c*[>LpZ 出语句中的单词。
x`T )_bc:6Q 另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如:
MMlryn||1 "北京天安门" ==> "北京 京天 天安 安门"。
kQ~2mU D![42H+-Qd 这样,在查询的时候,无论是查询"北京" 还是查询"天安门",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与
!5,>[^y3 |^fubQs;2 "and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。
ql"&E{u? gc(Gc vdB\ 基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于
]0v;;PfVl6 ^b|Z<oF 2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同,
3m3ljy U\aP <Sds5 d 自动切分 词表切分
+B(x:hzY9 实现 实现非常简单 实现复杂
,fWQSc\} 查询 增加了查询分析的复杂程度, 适于实现比较复杂的查询语法规则
;W%nBdE6| 存储效率 索引冗余大,索引几乎和原文一样大 索引效率高,为原文大小的30%左右
(NfP2E|B 维护成本 无词表维护成本 词表维护成本非常高:中日韩等语言需要分别维护。
aAM!;3j]B` 还需要包括词频统计等内容
F6>K FU8 适用领域 嵌入式系统:运行环境资源有限
.*XELP=BT 分布式系统:无词表同步问题
EUBJnf:q 多语言环境:无词表维护成本 对查询和存储效率要求高的专业搜索引擎
CTawXHM Q{%2Npvq eu=G[> 目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment
:"m~tU3& |OW/-&) search"能找到更多相关的资料。
}/tT=G]91 337y,; 安装和使用
eC%uu =5:L#` . 下载:
http://jakarta.apache.org/lucene/ B
~u9"SR. $t*>A+J 注意:Lucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,纯Java的词法分析生成器),所以如果从源
|-Rg]. kk|7{83O 代码编译或需要修改其中的QueryParser、定制自己的词法分析器,还需要从
https://javacc.dev.java.net/下载javacc。
GJZGHUB=> h>Z NPP8N lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口
<Q57}[$*) M)v\7a org.apache.Lucene.search/ 搜索入口
n(X {|? org.apache.Lucene.index/ 索引入口
"FuOWI{in org.apache.Lucene.analysis/ 语言分析器
2P\k;T( org.apache.Lucene.queryParser/ 查询分析器
hxG=g6:G org.apache.Lucene.document/ 存储结构
V|6PKED org.apache.Lucene.store/ 底层IO/存储结构
+'fy%/ org.apache.Lucene.util/ 一些公用的数据结构
wVegr 0|6]ps4Z7 简单的例子演示一下Lucene的使用方法:
~K'e}<-G feJzX*u 索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位
9Z?P/
o M:t!g% 是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字
l^`& Tnzv .II*wKk 段规则,列表如下: 方法 切词 索引 存储 用途
{
'A`ram Field.Text(String name, String value) Yes Yes Yes 切分词索引并存储,比如:标题,内容字段
qY,z,oAF Field.Text(String name, Reader value) Yes Yes No 切分词索引不存储,比如:META信息,
v[$-)vs*ag 不用于返回显示,但需要进行检索内容
C]@v60I
Field.Keyword(String name, String value) No Yes Yes 不切分索引并存储,比如:日期字段
:r4]8X- Field.UnIndexed(String name, String value) No No Yes 不索引,只存储,比如:文件路径
}"}
z7Xb0 Field.UnStored(String name, String value) Yes Yes No 只全文索引,不存储
RYhaQ&1i $~>3bik@ public class IndexFiles { //使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ... public static void main(String[]
hz:^3F`>/& JA]TO(x args) throws Exception { String indexPath = args[0]; IndexWriter writer; //用指定的语言分析器构造一个新的写索引器(
0!4;."S G.j R 第3个参数表示是否为追加索引) writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false); for (int i=1;
'5^$v{ g/*x;d= i<args.length; i++) { System.out.println("Indexing file " + args
); InputStream is = new FileInputStream(args m(2(Caz{
"n<rP 3y
); //构造包含2个字段Field的Document对象 //一个是路径path字段,不索引,只存储 //一个是内容body字段,进行全文 7JC^+rk
l>(w]
索引,并存储 Document doc = new Document(); doc.add(Field.UnIndexed("path", args)); doc.add(Field.Text )q.Z}_,)@
^O>G?a
("body", (Reader) new InputStreamReader(is))); //将文档写入索引 writer.addDocument(doc); is.close(); }; ZD$W>'m{F
K&L9Ue
//关闭写索引器 writer.close(); }} NxOiT#YH
索引过程中可以看到: euxkw]`h6
J#k3iE}
语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器SimpleAnalyser和 '(ZJsw
]V*ku%L0
StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析器。 Oup5LH!sW
Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD p#14
8PN/*Sa
文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。 0P MF)';R
对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。 "zN2+X"&
检索过程和结果显示: 1yFVF
L#
搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。 P<.
TiF?@
T/[8w
假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来, xXa* d
eA4dDKX+
public class Search { public static void main(String[] args) throws Exception { String indexPath = args[0], queryString JA=9EnTU
#sHA!@ |
= args[1]; //指向索引目录的搜索器 Searcher searcher = new IndexSearcher(indexPath); //查询解析器:使用和索引同样的语 m7~<z>5$
0LX"<~3j
言分析器 Query query = QueryParser.parse(queryString, "body", new SimpleAnalyzer()); //搜 |)%]MK$;
/6?A#%hc
索结果使用Hits存储 Hits hits = searcher.search(query); //通过hits可以访问到相应字段的数据和查询的匹配度 for (int ,s=jtK
/ 7i>0J]
i=0; i<hits.length(); i++) { System.out.println(hits.doc(i).get("path") + "; Score: " + JPo.&5k
n jfh4}g:
hits.score(i)); }; }} y#Cp Vm#!>
在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。 #F>7@N:5
Hacking Lucene ^*6So3
}JP0q
简化的查询分析器 ] ^f7s36
8|-j]
个人感觉lucene成为JAKARTA项目后,画在了太多的时间用于调试日趋复杂QueryParser,而其中大部分是大多数用户并不很熟悉的,目前 oK-T@ &-
MU
}<-1
LUCENE支持的语法: jruXl>T!U
6[b?ckvi
Query ::= ( Clause )* Y 6NoNc]h
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")") UU7E+4O&
"-y2En
中间的逻辑包括:and or + - &&||等符号,而且还有"短语查询"和针对西文的前缀/模糊查询等,个人感觉对于一般应用来说,这些功能有一 96V@+I
ym\AVRO{
些华而不实,其实能够实现目前类似于Google的查询语句分析功能其实对于大多数用户来说已经够了。所以,Lucene早期版本的QueryParser仍 E1|> O
5g x9W\a ?
是比较好的选择。 98c##NV(7|
P M
x`PB
添加修改删除指定记录(Document) d65fkz==A)
S_Tv Ix/7&
Lucene提供了索引的扩展机制,因此索引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。 PZT]H?
rP5&&Hso
如何删除指定的记录呢?删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另建索引,然后利用IndexReader.delete
<>|&%gmz
nZ4JI+Q)~
(Termterm)方法通过这个记录ID删除相应的Document。 $ 8"we
a\K__NCrX
根据某个字段值的排序功能 Z.4 vKO[<
a&sVcsX
lucene缺省是按照自己的相关度算法(score)进行结果排序的,但能够根据其他字段进行结果排序是一个在LUCENE的开发邮件列表中经常提到 V6c>1nZ
a{4Wg:
的问题,很多原先基于数据库应用都需要除了基于匹配度(score)以外的排序功能。而从全文检索的原理我们可以了解到,任何不基于索引的 9s#Q[\B!
^#6"d+lp
搜索过程效率都会导致效率非常的低,如果基于其他字段的排序需要在搜索过程中访问存储字段,速度回大大降低,因此非常是不可取的。 &Zxo\[lP
d9j+==S
<
但这里也有一个折中的解决方法:在搜索过程中能够影响排序结果的只有索引中已经存储的docID和score这2个参数,所以,基于score以外的 J|O=w(
-\6";_Y
排序,其实可以通过将数据源预先排好序,然后根据docID进行排序来实现。这样就避免了在LUCENE搜索结果外对结果再次进行排序和在搜索过 |UudP?E
O#}d!}SIp
程中访问不在索引中的某个字段值。 [N35.O6P6u
5s5GBJ?
这里需要修改的是IndexSearcher中的HitCollector过程: 5l(8{,NDt
X0QY:?
... scorer.score(new HitCollector() { private float minScore = 0.0f; public final void collect(int doc, float score) { "8.to=Lx
_f"HUKGN
if (score > 0.0f && // ignore zeroed buckets (bits==null || bits.get(doc))) { // skip /~8<;N>,+
%^`b)
docs not in bits totalHits[0]++; if (score >= minScore) { /* 原先:Lucene将docID和相应的匹配 ^~p^N <
{6y@;Fd
度score例入结果命中列表中: * hq.put(new ScoreDoc(doc, score)); // update hit queue * 如果用 @;6I94Bp
#5Q?Q~E@
doc 或 1/doc 代替 score,就实现了根据docID顺排或逆排 * 假设数据源索引时已经按照某个字段排好了序,而结果根据 "M-zBBY ]
Hm>7|!
docID排序也就实现了 * 针对某个字段的排序,甚至可以实现更复杂的score和docID的拟合。 */ mJ'Q9x"
&J]|pf3m
hq.put(new ScoreDoc(doc, (float) 1/doc )); if (hq.size() > nDocs) { // if hit queue overfull 46yq F
[Iwb7a0p
hq.pop(); // remove lowest in hit queue minScore = ((ScoreDoc)hq.top()).score; // reset m
L#%H(
xr;:gz!h
minScore } } } } }, reader.maxDoc()); ""Ub^:ucD
更通用的输入输出接口 8C[W;&Y=
&N