- 浏览: 640657 次
- 性别:
- 来自: 杭州
文章分类
最新评论
-
liuche20083736:
非常好
从问题看本质: 研究TCP close_wait的内幕 -
xiaopohai85707:
优化算法与原来需求不符
过滤字符的性能调优?挤一挤还是有的 -
kmy_白衣:
生成的area图有时候 标签的数值和图标上看上去的数值不一致。 ...
OpenFlashChart2之恶心文档 -
tom&jerry:
大神,请教一个问题,按名称排序为何无效,用的2.4.3 XPA ...
深入浅出jackrabbit之十三 查询之AST和QT -
jd2bs:
改成精确匹配可以了< filter-mapping &g ...
细谈Ehcache页面缓存的使用
用lucene作过索引的同学一定对文本提取不会陌生,但凡有binary需要作索引的地方,那就离不开文本的提取,而jackrabbit是可以存储二进制数据的,而这些二进制数据中包含的文本也是需要被搜索到的,所以jackrabbit里建索引的过程中自然离不开文本提取。
那么看到本文的标题,有经验的同学一定会问,前面都已经讲过索引的提交了,为什么到现在才讲文本提取呢。要回答这个问题,我们必须来回顾一下jackrabbit中建索引的流程(简单的划分为4个步骤,如下):
1创建document-----2加入到volatileindex的pending缓存中---------3生成index数据到内存-------4将内存中的二进制数据刷到文件系统。
那么哪里需要提取文本呢,很显然是在创建document的时候。那这个话题是不是应该放到创建document一文中阐述比较好呢,非也,这个问题之所以放到索引提交后面阐述是有原因的。
一般来说,文本提取的耗时长度和文本的大小有直接关系,当文本的大小超过一定的量之后,提取的速度会比较慢。那么当前的WorkThread就会一直耗在这块地方,也就是上述流程中的第一步,这样WorkThread一定要等到文本提取之后才能返回,不知道有多少用户有这个耐性等待下去呢。而且这里还有另外一个问题,那就是WorkThread如果因为这种操作而阻塞住,那么它回线程池的时间就会延后,新的请求过来因为拿到不到线程池中的WorkThread而不得不等待,一直到池中有可用线程为止,或者选择timeout返回,那么如果因为WorkThread都耗在文本提取这个地方,那么就太冤了,因为一般来说大文本做索引的实时性要求并不是很高,所以因为同步文本提取这个原因导致系统的并发量急剧下降,用户就会开始抱怨,你们这个破玩意怎么会这么慢?
为了解决这个问题,在jackrabbit中引入了异步处理文本提取任务的功能(它必须是异步的吗?当然不,jackrabbit也提供了同步的选择,但是本文主要阐述异步提取的功能,所以在下面的文章中我们假设我们配置异步提取的功能),说到这里,我们不得不再说说他是怎么个异步法的呢?它是异步提取,然后由处理提取的线程负责把这个document重新加入到index中呢,还是提取的线程只负责提取,提取完成之后再把提取完成的document交给专门的线程去add到document中呢。
后者,当然是后者,提取的线程只负责提取,有专门的线程来负责更新document到index中。如果说观察者模式是设计模式中的皇后,那么ahuaxuan可以说,生产消费模型是编程模型中的皇后,它无处不在(您要是天天写crud的话,ahuaxuan很难保证它无处不在)。
在前面的文章中,我们一直在提MultiIndex#indexingqueue,而且一直没有详细解释indexingqueue的功能。其实它就是在异步文本提取中充当着消费队列的职责,提取线程(生产者)再文本提取完成之后,会把document加入到indexingqueue中。那么消费者线程就负责定时的从indexingqueue中取出一件准备好的document,并执行multiindex#update方法。
我们先来看看提取文本是怎么做的。再次回到NodeIndexer类的addBinary方法,我相信大家对这个方法还有印象,他负责创建一个名字叫jcr:data的field。他的值就是来自于二进制文件,比如pdf,doc等等:
protected void addBinaryValue(Document doc, String fieldName, Object internalValue) { ……………………………………………….. Reader reader; if (extractor instanceof PooledTextExtractor) { //从类名上我们也可以看出点端倪,这里貌似有个线程池。 reader = ((PooledTextExtractor)extractor).extractText(stream, type, encoding, node.getNodeId().getUUID().toString()); } else { //忽视这个else,假设只进入if, reader = extractor.extractText(stream, type, encoding); } //这个createFulltextField方法非常之相当重要。后面会讲到 doc.add(createFulltextField(reader)); …………………………………………………………….. }
现在我们要做的就是跟到PooledTextExtractor# extractText方法中去,其他能做的也不多:
public Reader extractText(InputStream stream, String type, String encoding, String jobId) throws IOException { if (null == jobId || "".equals(jobId)) { //结合上面的方法,我们得知,jobid的值是一个node的id return extractText(stream, type, encoding); } else { /*在这里,创建了一个TextExtractorJob,它是Runnable的一个实现类*/ TextExtractorJob job = new TextExtractorJob(extractor, stream, type, encoding, jobId); return new TextExtractorReader(job, executor, timout); } }
从上面的方法中,我们可以看到一个简单的逻辑,创建一个thread,然后放到池中执行,并返回结果。
先看看创建线程的流程:
public TextExtractorJob(final TextExtractor extractor, final InputStream stream, final String type, final String encoding, final String jobId) { this.type = type; this.jobId = jobId; this.cmd = setter(new Callable() { public Object call() throws Exception { Reader r = extractor.extractText(stream, type, encoding); if (r != null) { if (discarded) { r.close(); r = null; } else if (timedOut) { // spool a temp file to save memory /*这里的逻辑非常重要,这里的逻辑表明,一旦方法执行到这里,表示上面的extractor.extractText 已经超过了我们预设的时间,所以为了避免占用过多的内存,需要把内存中的数据拷贝到磁盘上,以减少内存的消耗。*/ r = getSwappedOutReader(r); } } return r; } }); }
在上面的这个方法中,我们看到在TextExtractorJob这个线程类内部还有一个线程类,就是cmd,而且这个线程类有一个奇怪的地方,cmd这个线程的run方法中其实是执行一个Callable接口的实现类(这个逻辑写在了setter(Callable cc)中)。cmd的逻辑其实就是执行真正的文本提取操作。而TextExtractorJob的run方法其实就是执行cmd的run方法。也就是说cmd作为一个线程类实现压根没有被真正的作为一个线程启动过,所以cmd定义为runnable只是给人多一份疑惑,其他作用没有看出来。
创建完TextExtractorJob之后,就要用它来创建TextExtractorReader。我们看看TextExtractorReader的构造方法:
TextExtractorReader(TextExtractorJob job, Executor executor, long timeout) { this.job = job; this.executor = executor; this.timeout = timeout; }
很简单,什么都没有,也就是说在这个操作的过程中,并没有触发文本提取的操作,那么这个操作是什么时候做的呢?这就需要我们回到addBinary方法,看看它是怎么用这个TextExtractorReader类的:
doc.add(createFulltextField(reader));
原来是又调用了createFulltextField方法,那么我们得进去看一下,说不定触发文本提取的操作就在这里哦:
protected Fieldable createFulltextField(Reader value) { if (supportHighlighting) { return new LazyTextExtractorField(FieldNames.FULLTEXT, value, true, true); } else { return new LazyTextExtractorField(FieldNames.FULLTEXT, value, false, false); } }
额的神啊,啥都没有,就创建了一个LazyTextExtractorField类(还记得supportHighlighting参数吗,不记得的话再回头看看前面的文章),也就是说我们不得不再去看看LazyTextExtractorField这个类,但是我们有种感觉,快要看到我们想看到的东西了,这个继承实现了lucene中的AbstractField,结合上面的doc.add方法,我们可以想象得到,真正触发提取线程工作的应该是lucene,lucene一定会调用LazyTextExtractorField类中的某个方法,以得到值,而这个时候,如果有文本提取的任务,那么应该会触发它,然后改方法先返回空字符串,把残缺版的document先加入索引中,等待提取完成后,再由前面提到的消费者把残缺版的document数据从索引中删除,再把完整版的document加入到索引中。
上面说到的索引时触发文本提取操作只是一种触发方式,通过阅读源代码,我们还可以发现第二种触发方式,那就是在AbstractIndex的addDocument方法中,会判断,如果提取超过100ms,就把document拷贝一份,把copy的document加入到index中,然后把原始的document加入到indexingQueue队列中。这个时候,提取线程继续为这个原始的document执行提取操作。
那么我们先来看第一种触发,lucene触发,下面这个方法是LazyTextExtractorField中的方法,这个方法被lucene调用,用以分词, ahuaxuan已经在关键的代码中加入了注释:
public String stringValue() { if (extract == null) { StringBuffer textExtract = new StringBuffer(); char[] buffer = new char[1024]; int len; try { /*这里的读取操作有一定的迷惑性,其实它是调用了,考虑到这里的reader是TextExtractorReader,所以耗无疑问,这里的方法一定是调用了TextExtractorReader#read */ while ((len = reader.read(buffer)) > -1) { textExtract.append(buffer, 0, len); } } catch (IOException e) { log.warn("Exception reading value for field: " + e.getMessage()); log.debug("Dump:", e); } finally { try { reader.close(); } catch (IOException e) { // ignore } } extract = textExtract.toString(); } return extract; }
方法很简单,就是调用reader的读取,只是它调用的是Reader类的read()方法:
public int read(char cbuf[]) throws IOException { return read(cbuf, 0, cbuf.length); } 而其实目标的方法是子类的read(cbuf, 0, cbuf.length);也就是说应该是TextExtractorReader# read(cbuf, 0, cbuf.length),那么我们来看一下这个方法TextExtractorReader# read:这个read方法才是真正为LazyTextExtractorField#stringValue返回数据的地方 public int read(char cbuf[], int off, int len) throws IOException { /*事实上,这里的extractedText应该是不会等于空的,因为如果是异步提取,那么在lucene调用read方法之前,extract线程早就把extractedText提取出来,或者将其置为new StringReader(“”)了,除非一个document在isExtractorFinished返回false的时候,也被加入了 indexwriter。在正常流程的debug过程中,ahuaxuan并没有发现程序执行到这个if块里面*/ if (extractedText == null) { // no reader present // check if job is started already if (jobStarted) { // wait until available /*如果任务已经开始,那么等待读取文本,那么如果任务开始,这个开始是谁开始的呢?其实就是后面要讲到的第二种触发方式,这里是无限时等待,太恐怖了,所以这个代码段是存在危险的,实际上,如果我们如果在每个单独的Extractor类中设置专门的超时时间,那么这里就没有问题了。*/ extractedText = job.getReader(Long.MAX_VALUE); } else { // execute with current thread /*如果提取任务还没有被触发,则把该线程实例放到DIRECT_EXECUTOR这个线程池中运行,注意,其实这个类很诡异,它并不是一个线程池,它拿到线程对象之后会调用run方法,而不是start,这也意味着这个操作是非异步的,这里不会有多线程的问题 */ try { DIRECT_EXECUTOR.execute(job); } catch (InterruptedException e) { // current thread is in interrupted state // -> ignore (job will not return a reader, which is fine) } /*同步执行,立即取结果,取不到就返回null*/ extractedText = job.getReader(0); } if (extractedText == null) { // exception occurred extractedText = new StringReader(""); } } return extractedText.read(cbuf, off, len); }
需要注意的是getSwappedOutReader这个方法,该方法用临时文件的方式来节约内存。但不是说什么地方都可以使用这种方法的,因为提取之后的问题可能在后面才能被用到,所以暂时存放在临时文件中是可行的,这种方式在下载工具中非常常见,一般下载工具下载文件时会创建临时文件也是同样的道理。
接着,我们再来看看第二中方式触发(事实上,最开始触发这个方法的地方才是真正的第二种触发,它在什么地方呢,请参考AbstractIndex#getFinishedDocument(xx),再文本提取的第二篇文章中也有比较详细的说明):
public boolean isExtractorFinished() { if (!jobStarted) { try { /*如果任务还没有开始,那么则把job放到线程池中执行,并且将jobStarted设置为true, */ executor.execute(job); jobStarted = true; } catch (InterruptedException e) { // this thread is in interrupted state return false; } /*然后开始提取的操作,同时,提取的时候有一个超时时间,代表超过多长时间就直接返回,默认时间是100毫秒,在repository.xml中的 SearchIndex节点中可以配置,一旦超时之后,reader就会被写入到临时文件中,下次读取就从临时文件中读取*/ extractedText = job.getReader(timeout); } else { // job is already running, check for immediate result /*如果任务已经在上次检查中被触发过了,那么就直接获取提取之后的结果,如果还没有提取完,0表示没有提取完则抛出TimeoutException,但是getReader会捕获这个异常,并返回一个null*/ extractedText = job.getReader(0); } /*如果extractedText并且提取的时候出现异常,超时等,那么就置其为空*/ if (extractedText == null && job.getException() != null) { // exception occurred extractedText = new StringReader(""); } return extractedText != null; }
由此可见,这里的逻辑是当地一次提取的时候,等待100毫秒之后返回,如果100毫秒之内搞定问题,那事情很顺利,如果没有搞定呢,那就等下次,下次再进入这个方法,如果还没有搞定,那么reader就是new StringReader("")了,这样二进制就没有能被成功提取出来。
这个方法是谁来调用的呢,还是那个indexingqueue的消费者。在下文中,让我们来分析一下它。
好了说到这里,大家应该对jackrabbit中异步文本提取的功能算是比较了解了,之前ahuaxuan说过,jackrabbit也是支持同步文本提取的,这个就比较简单了,相信大家都可以想象得出来,在一个hashmap中存在了一组extractor,比mswordextractor,pdfextractor,那么当需要提取文本时,只要把inputstream和filetype交给处理器,那么,处理器就会为该filetype选择合适的extractor并执行extractText方法,这些逻辑都在CompositeTextExtractor.java类中,根据filetype委派给对应的extractor,其代码也只有90行,大家可以自行查看。
总结,在本文中,ahuaxuan主要描述了jackrabbit文本提取的两种方式,同步和异步,而且在异步的方式中,触发提取操作的有两个点,一个是消费者检查哪些document已经提取完成的时候,还有一个是lucene调用field的stringValue时候,即从document生成二进制index数据的时候。这样通过阅读源代码,我们详细的知道了jackrabbit中提取文件的逻辑。不过这个过程并不是无可挑剔的,比如存在二进制文件过大,那么提取文本过多,占用过多内存,而且占用很长的cpu时间,如何修改才能使之满足我们的需求呢。
从本文中我们得到如下信息,文本提取的主要逻辑是什么,文本提取的触发点是什么。当然我们也得到了一个疑问,在文件提取的异步模型中,谁是生产者,谁是消费者,具体的流程是怎么样的。
在下文中,ahuaxuan将把本文提出的细节和整个流程串起来,形成一幅完整的流程。
To be continue
发表评论
-
深入浅出jcr之16 该死的RMI,我们需要HTTP+简单RPC协议
2009-12-12 13:22 6713从这篇文 ... -
深入浅出jackrabbit之十五 文档提取优化2.docx
2009-10-22 18:38 3880/** *author:ahuaxuan *2009- ... -
深入浅出jackrabbit之十四 分布式文档提取
2009-09-24 12:20 4737/** *author:ahuaxuan *200 ... -
深入浅出jackrabbit之十三 查询之AST和QT
2009-09-10 10:12 3438简介:在前面的文章中 ... -
深入浅出jcr之十二 key-value存储系统
2009-08-26 09:31 3764作者:ahuaxuan 在写文章方面,惰性心理 ... -
深入浅出jcr之十一 jackrabbit改进要点
2009-08-18 18:22 3619作者,ahuaxuan 在看过前 ... -
深入浅出jcr之十 redolog 和 recovery.docx
2009-08-18 18:14 2158作者:ahuaxuan 在前面的 ... -
深入浅出 jackrabbit 九 索引合并(下)
2009-07-22 14:16 2106在上文中,ahuaxuan讲到了索引创建的主体流程,但是索引合 ... -
深入浅出 jackrabbit 八 索引合并(上)
2009-07-21 17:32 2557我们从文本提取的逻辑中走出来,回到主体流程。 在前面的文 ... -
深入浅出 jackrabbit 七 文本提取(下)
2009-07-21 17:29 2524接上文,说到文本提取,在上一篇文章中,我们是管中窥豹,并没有把 ... -
深入浅出 jackrabbit 之五 索引提交(下)
2009-07-14 17:53 2259接上文,在上面一篇文章中,我们谈到了update中的Delet ... -
深入浅出 jackrabbit 之四 索引提交(上)
2009-07-14 09:10 2957在上上篇文章中,我们了解了创建索引的一般流程,在上篇文章中,我 ... -
深入浅出 jackrabbit 3 创建 document
2009-07-01 13:03 4307/** *作者:ahuaxuan 张荣华 *日期:2009-0 ... -
深入浅出 jackrabbit 2 索引概览
2009-06-30 08:51 5375任何一个数据库都离不 ... -
深入浅出 jackrabbit 十 查询概览
2009-06-20 10:29 6294/** *author: ahuaxuan *date: ... -
深入浅出 jackrabbit 1
2009-05-19 18:31 12477/** * author:ahuaxuan( ...
相关推荐
《深入浅出 Jackrabbit 1》 Jackrabbit 是一个开源的、实现了 Java Content Repository (JCR) API 的内容管理系统,它允许程序通过统一的方式访问、存储和管理各种数据,包括文本、图像、视频等多媒体信息。这篇...
JackRabbit学习参考资料总汇涉及了深入浅出的JackRabbit内容仓库API的学习,内容涉及多个专题,整个学习资料是PDF文档格式。从标签来看,这份资料主要涉及JackRabbit以及JCR(Java Content Repository)的内容仓库...
这个“jackrabbit最全入门教程”将会带你深入理解这个强大的内容管理解决方案。 首先,我们需要了解什么是JCR。JCR提供了一种统一的方式来访问和管理数字内容,无论这些内容是文档、图像、视频还是其他形式的数据。...
jackrabbit, 在amqplib上,简单的amqp/rabbitmq作业队列基于 node Jackrabbitnode.js 在不讨厌生命的情况下。producer.js:var jackrabbit = require('jackrabbit');var rabbit = jackrabbit(process
在本文中,我们将深入探讨Apache Jackrabbit的基础知识,以及如何开始使用它。 一、JCR和Apache Jackrabbit的概念 1. JCR:JCR为存储和检索非结构化信息提供了一个模型和API。它允许开发者创建可以跨各种存储后端...
### Jackrabbit 在项目实施中的常见问题与解决方案 #### 一、Jackrabbit简介 Jackrabbit 是一个完全用 Java 编写的 JCR(Java Content Repository)实现,它可以作为一个独立的服务运行,也可以嵌入到更大的应用...
jackrabbit 1.5.6 jar
Apache Jackrabbit API 是一个强大的内容管理系统(CMS)的核心组件,它是Apache Software Foundation 开发的Java Content Repository (JCR) 的实现。JCR 是一个标准,它定义了一个用于存储、管理和检索结构化内容的...
Apache Jackrabbit 是一个...对于开发人员来说,理解这些功能以及如何将 Jackrabbit 集成到现有应用中是深入学习的关键部分。通过实践示例代码和探索 Jackrabbit API 文档,你可以逐步掌握这个强大的内容管理系统框架。
jackrabbit-standalone-1.6.5.jar是webDav的支持jar包。
Apache Jackrabbit是一个开源的、实现了Java Content ...总之,这个"Jackrabbit入门实例"是学习和探索JCR和Jackrabbit的好起点,它涵盖了基本的操作和概念,帮助你快速上手并深入了解这个强大的内容管理系统。
标题中的“查看jackrabbit仓库的小工具”指的是一个用于观察和管理Apache Jackrabbit仓库的实用程序。Jackrabbit是Java Content Repository (JCR) API的一个开源实现,它提供了一个内容管理系统(CMS)的基础框架,...
通过深入理解和使用"jackrabbit-webdav-2.3.2.src.zip"中的源代码,开发者不仅可以学习WebDAV协议的工作原理,还能了解如何在Android环境中实现高效稳定的WebDAV客户端功能。此外,对于想要对Jackrabbit进行定制化...
综上所述,Apache Jackrabbit 2.6 是一个强大而灵活的内容管理解决方案,它为开发者提供了丰富的功能和API,以构建高效、可扩展的CMS应用。其对JCR规范的支持以及与多种系统的兼容性,使其成为构建企业级内容管理...
jackrabbit-webdav-2.1.0.jar 具体用法可以网上查找
这两个项目将帮助我们深入理解和快速入门Jackrabbit的使用。 1. Jackrabbit核心概念: - JCR:JSR 170定义了内容存储的标准接口,使得应用程序可以透明地访问和操作不同类型的存储系统。 - Node:在JCR中,内容被...
标题中的"jackrabbit-webdav-2.7.1.zip"指的是Apache Jackrabbit的一个特定版本——2.7.1的WebDAV模块的压缩包。Apache Jackrabbit是Java内容存储库(Content Repository)的一个实现,它遵循JCR(Java Content ...
jackrabbit-api-1.5.0.jar
杰克兔(Jackrabbit)是Apache软件基金会的一个开源项目,主要致力于实现Java Content Repository (JCR) 规范。这个规范定义了一种用于存储、管理和检索结构化内容的标准API。在给定的压缩包"jackrabbit-jcr-commons...