lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口
org.apache.Lucene.search/ |
搜索入口 |
org.apache.Lucene.index/ |
索引入口 |
org.apache.Lucene.analysis/ |
语言分析器 |
org.apache.Lucene.queryParser/ |
查询分析器 |
org.apache.Lucene.document/ |
存储结构 |
org.apache.Lucene.store/ |
底层IO/存储结构 |
org.apache.Lucene.util/ |
一些公用的数据结构 |
简单的例子演示一下Lucene的使用方法:
索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字段规则,列表如下:
方法 |
切词 |
索引 |
存储 |
用途 |
Field.Text(String name, String value) |
Yes |
Yes |
Yes |
切分词索引并存储,比如:标题,内容字段 |
Field.Text(String name, Reader value) |
Yes |
Yes |
No |
切分词索引不存储,比如:META信息, 不用于返回显示,但需要进行检索内容 |
Field.Keyword(String name, String value) |
No |
Yes |
Yes |
不切分索引并存储,比如:日期字段 |
Field.UnIndexed(String name, String value) |
No |
No |
Yes |
不索引,只存储,比如:文件路径 |
Field.UnStored(String name, String value) |
Yes |
Yes |
No |
只全文索引,不存储 |
public class IndexFiles {
//使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);
//构造包含2个字段Field的Document对象
//一个是路径path字段,不索引,只存储
//一个是内容body字段,进行全文索引,并存储
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//将文档写入索引
writer.addDocument(doc);
is.close();
};
//关闭写索引器
writer.close();
}
}
具体的子类只需要提供一个返回getName()方法的实现,当在Spring上下文中定义MessageProcessor接口时,该方法返回接口特定实现的bean名字。子类也可以提供自己的MessageConverter,使用不同的策略填充MessageData。以下是MessageProcessor的一个简单实现:public class SimpleMessageProcessor implements MessageProcessor { private Log log = LogFactory.getLog(getClass()); public Object process( MessageData messageData) { log.info(messageData); return null; }}
最后,具体的MDB看起来如下所示。注意,我们使用Xdoclet注释来声明部署描述符的元数据:
/** * SimpleMdb * * .bean * name="org.javatx.mdb.SimpleMdb" * type="MDB" * destination-type="javax.jms.Queue" * transaction-type="Bean" * * .pool * initial-beans-in-free-pool= * "" * max-beans-in-free-pool= * "" * * .message-driven * connection-factory-jndi-name= * "-e" * destination-jndi-name= * "-e" * jms-polling-interval-seconds= * "" * * .env-entry * name="BeanFactoryPath" * value="applicationContext.xml" */public class SimpleMdb extends MessageDataDrivenBean { protected String getName() { return "simpleProcessor"; } }
在上述代码中,BeanFactoryPath的env-entry被Spring的EJB类用来定位应用程序上下文。应用程序上下文中应该有simpleProcessor bean的声明,这个bean会处理所有的处理逻辑,以及非功能性的需求,比如:事务、防止消息的重复处理以及可选的跟踪和性能监控。
显然,将所有非功能方面移到通知中,并利用包装了MessageProcessor实际实现的ProxyFactoryBean来定义拦截器链是很有意义的。定义可能如下所示:
图1中的顺序图说明了消息处理过程以及支持该服务质量模型所需的advisor堆栈:
图 1.处理传入消息的advisor堆栈(单击图像查看大图)
实现消息拦截器
现在我们来仔细看一下mdbTransactionInterceptor和mdbDuplicateHandlingAdvisor,它们使用上述方法提供了保证服务质量所需的功能。
mdbTransactionAdvisor是利用标准的Spring TransactionInterceptor以及process()方法的PROPAGATION_REQUIRES_NEW事务属性定义的。
PROPAGATION_REQUIRES_NEW,timeout_300
在WebLogic Server中,可以将Spring包装器用作服务器JNDI中javax.transaction.UserTransaction所公开的平台事务管理器,并定义应用程序上下文如下:
链中的下一个通知是mdbDuplicateHandlingAdvisor。因为它还要将一些独有键保存到数据库表中,所以需要一个数据源:
更通用的输入输出接口
虽然lucene没有定义一个确定的输入文档格式,但越来越多的人想到使用一个标准的中间格式作为Lucene的数据导入接口,然后其他数据,比如PDF只需要通过解析器转换成标准的中间格式就可以进行数据索引了。这个中间格式主要以XML为主,类似实现已经不下4,5个:
数据源: WORD PDF HTML DB other
\ | | | /
XML中间格式
|
Lucene INDEX
目前还没有针对MSWord文档的解析器,因为Word文档和基于ASCII的RTF文档不同,需要使用COM对象机制解析。这个是我在Google上查的相关资料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一个办法就是把Word文档转换成text:http://www.winfield.demon.nl/index.html
索引过程优化
索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。
Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
搜索过程优化
lucene支持内存索引:这样的搜索比基于文件的I/O有数量级的速度提升。
http://www.onjava.com/lpt/a/3273
而尽可能减少IndexSearcher的创建和对搜索结果的前台的缓存也是必要的。
Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。
如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。
Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。
我的一些尝试:
- 支持中文的Tokenizer:这里有2个版本,一个是通过JavaCC生成的,对CJK部分按一个字符一个TOKEN索引,另外一个是从SimpleTokenizer改写的,对英文支持数字和字母TOKEN,对中文按迭代索引。
- 基于XML数据源的索引器:XMLIndexer,因此所有数据源只要能够按照DTD转换成指定的XML,就可以用XMLIndxer进行索引了。
- 根据某个字段排序:按记录索引顺序排序结果的搜索器:IndexOrderSearcher,因此如果需要让搜索结果根据某个字段排序,可以让数据源先按某个字段排好序(比如:PriceField),这样索引后,然后在利用这个按记录的ID顺序检索的搜索器,结果就是相当于是那个字段排序的结果了。
从Lucene学到更多
Luene的确是一个面对对象设计的典范
- 所有的问题都通过一个额外抽象层来方便以后的扩展和重用:你可以通过重新实现来达到自己的目的,而对其他模块而不需要;
- 简单的应用入口Searcher, Indexer,并调用底层一系列组件协同的完成搜索任务;
- 所有的对象的任务都非常专一:比如搜索过程:QueryParser分析将查询语句转换成一系列的精确查询的组合(Query),通过底层的索引读取结构IndexReader进行索引的读取,并用相应的打分器给搜索结果进行打分/排序等。所有的功能模块原子化程度非常高,因此可以通过重新实现而不需要修改其他模块。
- 除了灵活的应用接口设计,Lucene还提供了一些适合大多数应用的语言分析器实现(SimpleAnalyser,StandardAnalyser),这也是新用户能够很快上手的重要原因之一。
这些优点都是非常值得在以后的开发中学习借鉴的。作为一个通用工具包,Lunece的确给予了需要将全文检索功能嵌入到应用中的开发者很多的便利。
此外,通过对Lucene的学习和使用,我也更深刻地理解了为什么很多数据库优化设计中要求,比如:
- 尽可能对字段进行索引来提高查询速度,但过多的索引会对数据库表的更新操作变慢,而对结果过多的排序条件,实际上往往也是性能的杀手之一。
- 很多商业数据库对大批量的数据插入操作会提供一些优化参数,这个作用和索引器的merge_factor的作用是类似的,
- 20%/80%原则:查的结果多并不等于质量好,尤其对于返回结果集很大,如何优化这头几十条结果的质量往往才是最重要的。
- 尽可能让应用从数据库中获得比较小的结果集,因为即使对于大型数据库,对结果集的随机访问也是一个非常消耗资源的