上一节 下一节
任何用户, 包括系统开发者, 使用搜索引擎的共同方式只有一个: 查询(query). 整个搜索过程的目的是为了满足查询要求, 搜索过程是由查询贯穿的. 若没有指定查询, 而是从索引(index)的内容出发, "搜索"将是漫无目的且毫无意义的过程. 搜索过程的起点只能是索引.
以下引用自Lucene in Action 的入门章节, 在其中能看到Query是如何用来启动查询的.
public class BasicSearchingTest extends LiaTestCase {
public void testTerm() throws Exception {
IndexSearcher searcher = new IndexSearcher(directory);
Term t = new Term(“subject”, “ant”);
Query query = new TermQuery(t);
Hits hits = searcher.search(query);
assertEquals(“JDwA”, 1, hits.length());
t = new Term(“subject”, “junit”);
hits = searcher.search(new TermQuery(t));
assertEquals(2, hits.length());
searcher.close();
}
}
public class IndexSearcher extends Searcher {
public TopDocs search(Query query, Filter filter, final int nDocs)
throws IOException {
Scorer scorer = query.weight(this).scorer(reader);
if (scorer == null)
return new TopDocs(0, new ScoreDoc[0]);
final BitSet bits = filter!=null?filter.bits(reader):null;
final HitQueue hq = new HitQueue(nDocs);
final int[] totalHits = new int[1];
scorer.score(new HitCollector() {
public final void collect(int doc, float score) {
if (score > 0.0f &&
(bits==null || bits.get(doc))) {
totalHits[0]++;
hq.insert(new ScoreDoc(doc, score));
}
}
});
ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()];
for (int i = hq.size()-1; i >= 0; i--)
scoreDocs[i] = (ScoreDoc)hq.pop();
return new TopDocs(totalHits[0], scoreDocs);
}
...
}
Scorer scorer = termQuery.weight.scorer(indexReader)
scorer.score(new HitCollector() {
...
});
public scorer weight#scorer(IndexReader reader){
TermDocs termDocs = reader.termDocs(term);
if (termDocs == null)
return null;
return new TermScorer(this, termDocs, getSimilarity(searcher),
reader.norms(term.field()));
}
class Scorer{
...
public void score(HitCollector hc) throws IOException {
while (next()) {
hc.collect(doc(), score());
}
}
...
public boolean next() throws IOException {
pointer++;
if (pointer >= pointerMax) {
pointerMax = termDocs.read(docs, freqs); // refill buffer
if (pointerMax != 0) {
pointer = 0;
} else {
termDocs.close(); // close stream
doc = Integer.MAX_VALUE; // set to sentinel value
return false;
}
}
doc = docs[pointer];
return true;
}
...
}
可以看出, 上面的问题唯一可能的答案是: weight在构造Scorer时已经为Scorer决定了查询内容就在那个termDocs里. Scorer的代码也表明, 它在遍历所有合法文档时,背后的查询动作是在穷举一个数组:doc[], 而这个数组的来源就是TermDocs. 剩下的问题是,TermDoc在整个查询中扮演何种角色--它是怎么读数据的
看看TermDoc是个虾米东东
reader 创建好TermDoc后调用TermDoc.seek(term). 这个方法在硬盘索引文件中找到term所对应的所有文档记录, 每条记录包含文档id和term在文档中出现的次数tf. 这些文档信息是编制索引时就建好的, 索引文件中每个term对应的文档记录按顺序紧密排列在一起, seek方法能找到这些记录在索引中的开始位置及满足term的文档总数. 以后, TermDoc在scorer中的作用就是读入每个符合term的文档及term在该文档中的tf, 由于建好了索引只需在索引文件中遍历即可, termDoc包含的df将用于此过程的遍历计数.就是说scorer接收到的那个termDoc是调用过seek的, 已经定位到了term对应的数据位置,这便让scorer能遍历termQuery中所有包含那个term的Doc.
scorer怎样遍历全部doc
读数据又是另一门学问, 感兴趣的人也许研究过读一个100M文件,与读1000个1K byte文件有何区别,结果当然是震撼的.只要有可能,尽量读取整块数据而不是零碎地读取小数总不会让人失望,然而同样没有人会在构造一个对象时就去对一个未知大小(可能真的包含100万个文档)的数据.只要没收到必要请求,任何人都会尽力避免这种冗长的操作. Lucene的设计者同样只是让在scorer在需要读入数据即第一次调用next()方法时调用termDoc的read方法读数据(代码中黑体部分).为了避免零碎读取降低硬盘效率,termDoc.read()会一次性读入所有合法文档(当然仅包括文档id和tf, 建立索引的过程中,这两个数据一组组的放在专门的文件中,每个term对应的全部文档在这个文件里连续排列以避免零碎读取),scorer调用next ()语句,遍历read()返回的文档id数组,整个遍历过程只需把读出来的doc[i]里的i进行++便万事大吉.
遍历过程中发生什么事情大家心里应该很清楚,无非是把这些doc(这就是搜索结果)一个个添加到Collector中. 查询结束后, 我们将得到一个int数组, 里面保存着每个结果文档的id. 要使用这些查询结果,用户还需要从按照每篇文档的id文档库中取出结果, 这些只需调用searcher.doc(id)即可完成的事务性过程不在本"技术"文章讨论范围中.
所谓搜索原来如此简单...