深入浅出 jackrabbit 十 查询概览

/**
*author: ahuaxuan
*date: 2009-06-20
*/
查询进阶
在第一篇文章的例子中,我们看到jackrabbit中存放数据的流程还是比较清晰的,而且我们也基本确定了其中数据的存储方式,一颗m叉树。

正如db一样,insert都是[color=red]看上去[/color]相对简单的,select相对总是没有这么简单的,如同关系数据库一样,jackrabbit中查询也有类似的流程,一般来说分成三个部分:1解析查询语句,2查询,3返回结果。这个应该是一个比较通用的模型。Jackrabbit的search部分亦不过如此。下面我们来看一个search的小例子。
public static void search(String co) throws Exception {
Repository repository = new TransientRepository();

Session session = repository.login(new SimpleCredentials("username",
"password".toCharArray()));
try {

// path query
Workspace workSpace = session.getWorkspace();
QueryManager queryMgr = workSpace.getQueryManager();
//查询语句
String queryPath2 = "//world [jcr:contains(., 'Maven')] order by jcr:score() descending, @name ascending";

//编译查询语句
Query query = queryMgr.createQuery(queryPath2,
Query.XPATH);

//执行查询任务
QueryResult queryResult = query.execute();

//得到结果
RowIterator rows = queryResult.getRows();
String result = "";
while (rows.hasNext()) {
Row resultRow = rows.nextRow();

/* Value[] values = resultRow.getValues();
for (Value value : values) {
System.out.println(" Value ------ " + value.getString());
}
*/ //
// --------------------------------------------------------------------------
TimerMasker tm = new TimerMasker();
result = resultRow.getValue("rep:excerpt(jcr:data)").getString();
tm.print("----------------------------------"); //
}
} finally {
session.logout();
}
}

这个例子很简单,使用xpath语法来查询world节点中包含指定字符串的同层节点。然后按照指定的属性排序并返回。从上面的例子中我们大概就只能得到这么多信息了,但是我想爱好技术的你一定不会满足,也一定不会止步于这个小小的例子。如果同第一篇文章所讲,我们不能只看到事物的表面,而是要通过这个表面来挖掘其背后的本质。

所以接下来让我们来看看事物背后的本质。众所周知,只要是数据存储方案,那么必定离不开一个东西,那就是索引,索引可以帮助我们快速找到我们想要的数据。那么jackrabbit是怎么根据xpath语法就找到对应的数据的呢,无疑,它也是在运用搜索技术,而且通过它的package列表我们可以看到,它其实就是利用lucene来完成了它的查询模块。也就是说在jackrabbit中,利用了xpath和lucene实现了它的查询模块。

搞清楚了这两点,我们就可以继续往下深入了。导入源代码,让我们从createQuery方法开始,这个方法很重要,可以说是非常重要,而且这个方法中也包含了一门基础学科-编译原理(jackrabbit中是使用javacc来生成xpath的词法语法分析类的org.apache.jackrabbit.core.query.xpath.Xpath,并且它的语法文件xpath.jj长达9000行,[url="http://ahuaxuan.iteye.com/"]ahuaxuan[/url]暂时还没有这个闲心看下去)。createQuery方法的两个参数,一个是查询语句,还有一个是语句的语法,目前jackrabbit支持xpath语法和sql语法,他们在整个流程上属于一个可以相互替换的节点上,就是生成AST。再生产语法树之后,会生成查询节点树,这个颗树中包含了lucene需要的很多信息,比如需要搜索的key word,搜索时的排序字段等等。所以在第一大步骤createquery中我们可以细分为两个小的步骤:
1 编译query statement,生成AST
2 通过AST生成querynode

通过上面的两个主要步骤,我们顺利的得到了query对象,query对象就包含的查询所需要的基本条件了,很显然它和hibernate的query对象有异曲同工之妙。都是通过一个查询语句来生产query对象,然后query对象中包含了查询的所有信息,一旦执行execute方法,就真正的触发了的查询的流程。

说到这里就不得不看一下一个关键的方法(org.apache.jackrabbit.core.query.lucene.QueryImpl):
public QueryResult execute(long offset, long limit) throws RepositoryException {
if (log.isDebugEnabled()) {
log.debug("Executing query: \n" + root.dump());
}
TimerMasker tk = new TimerMasker();
// 这里创建lucene的query对象,同时,这是一个BooleanQuery
Query query = LuceneQueryBuilder.createQuery(root, session,
index.getContext().getItemStateManager(),
index.getNamespaceMappings(), index.getTextAnalyzer(),
propReg, index.getSynonymProvider(),
index.getIndexFormatVersion());
//得到排序信息
OrderQueryNode orderNode = root.getOrderNode();

OrderQueryNode.OrderSpec[] orderSpecs;
if (orderNode != null) {
orderSpecs = orderNode.getOrderSpecs();
} else {
orderSpecs = new OrderQueryNode.OrderSpec[0];
}
Name[] orderProperties = new Name[orderSpecs.length];
boolean[] ascSpecs = new boolean[orderSpecs.length];
for (int i = 0; i < orderSpecs.length; i++) {
orderProperties[i] = orderSpecs[i].getProperty();
ascSpecs[i] = orderSpecs[i].isAscending();
}

//创建查询结果,事实上,这个一部才是真正执行lucene query的//地方。这一点可以从QueryResultImpl的构造方法中看出来,构造方法中//的getresult其实就是真正执行lucene search的地方,比较顺理成章
QueryResultImpl qri = new QueryResultImpl(index, itemMgr,
session, session.getAccessManager(),
this, query, new SpellSuggestion(index.getSpellChecker(), root),
getSelectProperties(), orderProperties, ascSpecs,
getRespectDocumentOrder(), offset, limit);
tk.print("execute");
return qri;
}


通过上面的代码注释,那么我们可以得到一个有效的信息,那就是在ahuaxuan提供的示例代码中执行query.execute()的时候,这个时候,我们需要的结果其实就已经加载到内存中了。有的人会感到疑惑,为什么上面的QueryResultImpl中带有这么多构造参数,其实那些对象是用来组装lucene出来的结果的。因为放到索引中的数据并不是node的所有信息,比如一个node,因为放到索引中只是nodeid,其对应的node对象还是需要通过itemMgr这样的对象来得到。

这样我们又确定了第二大步骤包含了二个步骤:
1 根据querynode生成lucene的query对象
2 执行lucene的query对象并得到结果集


现在数据,也有了,那么接下来做什么呢?当然是遍历数据,拿出结果来,显然这个和jdbc的工作流程有得一拼啊(看来这里的时候,大家对查询的使用是不是比较清楚了,它和很多持久化框架在流程上是相当一致的,熟悉了一个之后,再来看其他的,学习的速度明显得到加强,当前前提是你已经充分理解了某个框架的内部原理)。那么接下来的事情只有这些:
//得到结果
RowIterator rows = queryResult.getRows();
while (rows.hasNext()) {
Row resultRow = rows.nextRow();
resultRow.getValue("rep:excerpt(jcr:data)").getString();
}


看上去很简单。实际上也比较简单,但是其中确有一个ahuaxuan认为比较愚蠢的设计。拿这个例子来说,接下来的流程就是得到得到数据,并作一些客户端需要的操作,比如高亮。
通过代码的阅读:在这个大步骤中,又细分为以下步骤:
1 根据getvalue中参数判断查询的内容,比如jcr:score,jcr:path等,这些属性很容易就能得到,可以说是现成的,稍微看一下代码就知道他们是怎么获得的,而在上面的例子中,我们是要找jcr:data这个属性,并高亮之。而且这个属性也是最复杂的,其他属性比较简单,下面我们把注意力集中到这个属性。Jcr:data是jackrabbit的内在属性,目前一个node只能有一个这样的属性,这个属性中存放着二进制文档提取之后的fulltext的信息(当然你可以手动指定这个属性存放什么text)。现在我们就是要在结果集中高亮这个我们的keyword->’Maven’,之前ahuaxuan将maven的一篇文档放到jackrabbit中了,所以现在要高亮这个结果集。

要高亮之,必须得到Maven在这个text中的offset,所以就出现了下面这段代码:
Reader r = new StringReader(text);
TokenStream ts = index.getTextAnalyzer().tokenStream("", r);
Token t;
try {
while ((t = ts.next()) != null) {
TermVectorOffsetInfo[] info =
(TermVectorOffsetInfo[]) termMap.get(t.termText());
if (info == null) {
info = new TermVectorOffsetInfo[1];
} else {
TermVectorOffsetInfo[] tmp = info;
info = new TermVectorOffsetInfo[tmp.length + 1];
System.arraycopy(tmp, 0, info, 0, tmp.length);
}
info[info.length - 1] = new TermVectorOffsetInfo(
t.startOffset(), t.endOffset());
termMap.put(t.termText(), info);
}
} catch (IOException e) {
// should never happen, we are reading from a string
}


就是这段代码,可能会给我们带来一些些小麻烦。得到offset之后,那么下一步肯定就是高亮了,高亮的逻辑简要说来就是遍历这个offset,并根据每个offset指定的start和end来插入高亮的标识。

看上去,一切顺理成章,我们的例子是使用1m的maven文档,测试的结果是:

createTermPositionVector >>>-------- total nano seconds : 373531154

highlight:2761 >>>-------- total nano seconds : 146860414


也就是说计算每个term和对应的offset集合用了370ms,是整个搜索过程耗时的75%以上,如果把这一步省掉,可以节约75%的时间,那么如何省掉呢,两个方案,一,修改analyser,只找keyword和对应的offset,二,搜索的时候将term和对应offset保存起来。这样此处的370ms应该可以大大变小。经过ahuaxuan的修改之后,将offset保存在index之中,然后高亮的时候直接拿出来,避免了实时分词。测试的结果如下
new createTermPositionVector  >>>-------- total nano seconds : 4003012

也就是说这个方法从370ms变成了4ms,速度大大加快(虽然索引文件会变大一些)。

除此之外,在高亮的时候我们可以不用高亮全文,也就是取前10个offset,然后高亮之,再次运行相同的测试:

highlight:10 >>>-------- total nano seconds : 1264721

高亮从146ms变成了1ms


这样虽然只会高亮前10个关键字,但是速度从146ms变成了1ms.对于用户来说,高亮一个摘要并非不能接受。带来的好处是:
1 用户可以更加快速的得到结果集
2 大家知道,文字处理是非常消耗cpu的,通过这种方式也一定程度上节约了服务器上的cpu
对服务器,对客户都好,何乐而不为

通过以上两处的修改,原来高亮1m的文本需要530ms,而现在只需要5ms,提高了100倍。ahuaxuan以为,为了速度,这样的修改是值得的。

说到这里,查询这块可以告一段落了:
下面总结一下查询过程中的主要步骤:
1 编译query statement,生成AST
2 通过AST生成querynode
3 根据querynode生成lucene的query对象
4 执行lucene的query对象并得到结果集
5 遍历中:通过itemmanager和查询结果中的nodeid得到需要的node
6 生成offset
7 高亮之

由于ahuaxuan水平有限,见解未必全面,如有错误,请大家拍砖。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值