Hits类中缓存的实现、LRU算法

关于Hits类。
这个Hits类可是非常的重要,因为Lucene使用了缓存机制,关于缓存的实现就是在这个Hits类中。Hits工作过程中,使用了LRU算法,即通过一个HitDoc结构来实现一个双向链表,使用LRU置换算法,记录用户最近访问过的Document。
开门见山,直接拿出Hits类的实现代码来说话。
package org.apache.lucene.search;
import java.io.IOException;
import java.util.Vector;
import java.util.Iterator;

 

import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;

public final class Hits {
private Weight weight;   
private Searcher searcher;
private Filter filter = null;
private Sort sort = null;

private int length;                // Hits的长度,即满足查询的结果数量
private Vector hitDocs = new Vector();    // 用作缓存检索结果的(Hit)

private HitDoc first;         // head of LRU cache
private HitDoc last;          // tail of LRU cache
private int numDocs = 0;      // number cached
private int maxDocs = 200;    // max to cache

Hits(Searcher s, Query q, Filter f) throws IOException {
    weight = q.weight(s);
    searcher = s;
    filter = f;
    getMoreDocs(50); // retrieve 100 initially | 从缓存中取出检索结果,如果缓存为null,则需要查询,查询后将结果加入缓存中
}

Hits(Searcher s, Query q, Filter f, Sort o) throws IOException {
    weight = q.weight(s);
    searcher = s;
    filter = f;
    sort = o;
    getMoreDocs(50); // retrieve 100 initially | 从缓存中取出检索结果,如果缓存为null,则需要查询,查询后将结果加入缓存中
   
}

/**
   * 将满足检索结果的Document加入到缓存hitDocs中
   */

private final void getMoreDocs(int min) throws IOException {
    /////////////////////////////////////////////////////////////////////////////////////////////
    System.out.println("■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size="+hitDocs.size());
    ///////////////////////////////////////////////////////////////////////////////////////////

    if (hitDocs.size() > min) {
      min = hitDocs.size();
    }

    int n = min * 2;    // 扩充缓存容量为默认的2倍(默认最小情况下,也要扩充缓存。即使检索结果为1条记录,缓存的长度也扩充为100)
    TopDocs topDocs = (sort == null) ? searcher.search(weight, filter, n) : searcher.search(weight, filter, n, sort);
    length = topDocs.totalHits;
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;

    float scoreNorm = 1.0f;
   
    if (length > 0 && topDocs.getMaxScore() > 1.0f) {
      scoreNorm = 1.0f / topDocs.getMaxScore();
    }

    int end = scoreDocs.length < length ? scoreDocs.length : length;
    for (int i = hitDocs.size(); i < end; i++) {
      hitDocs.addElement(new HitDoc(scoreDocs[i].score * scoreNorm,
                                    scoreDocs[i].doc));
    }
    /////////////////////////////////////////////////////////////////////////////////////////////
    System.out.println("■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size="+hitDocs.size());
    ///////////////////////////////////////////////////////////////////////////////////////////

}

// 返回Hits的长度,即满足查询的Document的数量,并非是缓存Vector hitDocs的长度
public final int length() {
    return length;
}

// 根据Document的编号获取到Document
public final Document doc(int n) throws CorruptIndexException, IOException {
    /////////////////////////////////////////////////////////////////////////////////////////////
    System.out.println("hitDocs.size()="+hitDocs.size());
    /////////////////////////////////////////////////////////////////////////////////////////////    
    HitDoc hitDoc = hitDoc(n);

    // Update LRU cache of documents
    remove(hitDoc);               // remove from list, if there
    addToFront(hitDoc);           // add to front of list
    if (numDocs > maxDocs) {      // if cache is full
      HitDoc oldLast = last;
      remove(last);             // flush last
      oldLast.doc = null;       // let doc get gc'd
    }

    if (hitDoc.doc == null) {
      hitDoc.doc = searcher.doc(hitDoc.id); // cache miss: read document
    }

    return hitDoc.doc;
}

// 得到第n个Document的得分
public final float score(int n) throws IOException {
    return hitDoc(n).score;
}

// 得到第n个Document的编号
public final int id(int n) throws IOException {
    return hitDoc(n).id;
}


public Iterator iterator() {
    return new HitIterator(this);
}

private final HitDoc hitDoc(int n) throws IOException {
    if (n >= length) {
      throw new IndexOutOfBoundsException("Not a valid hit number: " + n);
    }

    if (n >= hitDocs.size()) {
      getMoreDocs(n);
    }

    return (HitDoc) hitDocs.elementAt(n);
}

private final void addToFront(HitDoc hitDoc) { // insert at front of cache
    if (first == null) {
      last = hitDoc;
    } else {
      first.prev = hitDoc;
    }

    hitDoc.next = first;
    first = hitDoc;
    hitDoc.prev = null;

    numDocs++;
}

private final void remove(HitDoc hitDoc) {    // remove from cache
    if (hitDoc.doc == null) {     // it's not in the list
      return;                    // abort
    }

    if (hitDoc.next == null) {
      last = hitDoc.prev;
    } else {
      hitDoc.next.prev = hitDoc.prev;
    }

    if (hitDoc.prev == null) {
      first = hitDoc.next;
    } else {
      hitDoc.prev.next = hitDoc.next;
    }

    numDocs--;
}
}

final class HitDoc {
float score;
int id;
Document doc = null;

HitDoc next; // in doubly-linked cache
HitDoc prev; // in doubly-linked cache

HitDoc(float s, int i) {
    score = s;
    id = i;
}
}
上面代码中,红色标注的部分为后面测试之用。
一次查询时,需要构造一个Query实例。从Hits类的成员变量来看,在检索的过程中,一个Query实例并不是只使用一次,那么多次使用进行查询就需要记录这个Query实例的状态。

为了更加直观,写了一个测试类,来观察缓存长度的分配情况:
package org.shirdrn.lucene.learn.test;

import java.io.IOException;
import java.util.Date;
import java.util.Iterator;

import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.Hit;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.LockObtainFailedException;

public class MyHitsTest {
   
    public void create() throws CorruptIndexException, LockObtainFailedException, IOException{
        String indexPath = "H:\\index";
        IndexWriter writer = new IndexWriter(indexPath,new CJKAnalyzer(),true);
        for(int i=0;i<500;i++){
            Document doc = new Document();
            doc.add(new Field("title","搜索引擎收录新页面与文章原创性问题",Field.Store.YES,Field.Index.TOKENIZED));
            doc.add(new Field("date",
                    DateTools.timeToString((new Date().getTime()), DateTools.Resolution.MINUTE),
                    Field.Store.YES, Field.Index.UN_TOKENIZED));
            doc.add(new Field("author","Shirdrn",Field.Store.YES,Field.Index.UN_TOKENIZED));
            String contents = "如果分词后的多个关键字,在关键文章分词中,某一关键片段中出现的关键频率最高,这个关键片段当然是Google检索结果呈现的那两行关键摘要。";
            doc.add(new Field("contents",contents,Field.Store.NO,Field.Index.TOKENIZED));
            writer.addDocument(doc);
        }
        writer.optimize();
        writer.close();
    }
   
    public void search() throws CorruptIndexException, IOException{
        Query query = new TermQuery(new Term("contents","关键"));
        String indexPath = "H:\\index";
        IndexSearcher searcher = new IndexSearcher(indexPath);
        Hits hits = searcher.search(query);
        System.out.println("★★★共检索出满足的结果 "+hits.length()+" 条。");
        Iterator it = hits.iterator();
        while(it.hasNext()){
            System.out.print("★调用Hits的doc()方法,(Vector): ");
            hits.doc(0); //执行这一行代码是为了观察:当获取一个Document的时候缓存的长度,因为第一次查看缓存的时候其长度是为0的,如果检索结果数量不为0则之后缓存长度是不为0的,至少为100
            Hit hit = (Hit)it.next();
            System.out.println("★检索到的Hit的ID为 : "+hit.getId());
        }
    }
}

class MyTest{
    public static void main(String[] args) throws CorruptIndexException, LockObtainFailedException, IOException, ParseException {
        MyHitsTest hitsTest = new MyHitsTest();
        //hitsTest.create();
        hitsTest.search();
    }
}

首先要构造500个Document,建立索引,之后,执行检索的操作,结果如下所示:

■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=0
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=100
★★★共检索出满足的结果 500 条。
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 0
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 1
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 2
……

★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 98
★调用Hits的doc()方法,(Vector): hitDocs.size()=100★检索到的Hit的ID为 : 99
★调用Hits的doc()方法,(Vector): hitDocs.size()=100■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=100
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=200
★检索到的Hit的ID为 : 100
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 101
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 102
……

★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 198
★调用Hits的doc()方法,(Vector): hitDocs.size()=200★检索到的Hit的ID为 : 199
★调用Hits的doc()方法,(Vector): hitDocs.size()=200■■■■■■■■■■■■■■■■■■■■■■■■进入getMoreDocs()方法中时,hitDocs.size=200
■■■■■■■■■■■■■■■■■■■■■■■■离开getMoreDocs()方法中时,hitDocs.size=400
★检索到的Hit的ID为 : 200
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 201
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 202
……

★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 398
★调用Hits的doc()方法,(Vector): hitDocs.size()=400★检索到的Hit的ID为 : 399
★调用Hits的doc()方法,(Vector): hitDocs.size()=400进入getMoreDocs()方法中时,hitDocs.size=400
离开getMoreDocs()方法中时,hitDocs.size=500
★检索到的Hit的ID为 : 400
★调用Hits的doc()方法,(Vector): hitDocs.size()=500★检索到的Hit的ID为 : 401
★调用Hits的doc()方法,(Vector): hitDocs.size()=500★检索到的Hit的ID为 : 402
……

由结果可见看到,构造一个Hits的实例的时候,调用getMoreDocs()方法。
第一次进入getMoreDocs()方法时,hitDocs.size() = 0 > min = 50不成立,接着n = min*2 = 50*2 = 100,因此离开getMoreDocs()方法时hitDocs.size() = 100;
第二次进入getMoreDocs()方法时,hitDocs.size() = 100 > min = 50成立,从而设置min = hitDocs.size() = 100,接着n = min*2 = 100*2 = 200, 因此离开getMoreDocs()方法时hitDocs.size() = 200;
第三次进入getMoreDocs()方法时,hitDocs.size() = 200 > min = 100成立,从而设置min = hitDocs.size() = 200,接着n = min*2 = 200*2 = 400, 因此离开getMoreDocs()方法时hitDocs.size() = 400;

如果满足查询的检索结果的Document数量足够大的话,应该继续是:

第四次进入getMoreDocs()方法时,hitDocs.size() = 400,离开getMoreDocs()方法时hitDocs.size() = 800;
第五次进入getMoreDocs()方法时,hitDocs.size() = 800,离开getMoreDocs()方法时hitDocs.size() = 1600;
……

根据上面,最后一次(第四次)进入getMoreDocs()方法的时候,hitDocs.size() = 400 > min = 400不成立,接着n = min*2 = 400*2 = 800,此时虽然缓存扩充了,但是执行searcher.search(weight, filter, n) 的时候取到了100条满足条件的Document,故而缓存的实际大小为hitDocs.size() = 500, 因此离开getMoreDocs()方法时hitDocs.size() = 500,其实此次如果满足查询的Document数量足够,可以达到hitDocs.size() = 800。

posted @ 2010-05-10 09:31  abstractwind  阅读(560)  评论(0编辑  收藏  举报