lucene原始碼分析---10
lucene原始碼分析—倒排索引的讀過程
上一章中分析了lucene倒排索引的寫過程,本章開始分析其讀過程,重點分析SegmentTermsEnum的seekExact函式。
首先看幾個建構函式,先看SegmentCoreReaders的建構函式,在Lucene50PostingFormat的fieldsProducer函式中建立。
BlockTreeTermsReader::BlockTreeTermsReader
public BlockTreeTermsReader(PostingsReaderBase postingsReader, SegmentReadState state) throws IOException {
boolean success = false;
IndexInput indexIn = null;
this.postingsReader = postingsReader;
this.segment = state.segmentInfo.name;
String termsName = IndexFileNames.segmentFileName(segment, state.segmentSuffix, TERMS_EXTENSION);
try {
termsIn = state.directory.openInput(termsName, state.context);
version = CodecUtil.checkIndexHeader (termsIn, TERMS_CODEC_NAME, VERSION_START, VERSION_CURRENT, state.segmentInfo.getId(), state.segmentSuffix);
...
String indexName = IndexFileNames.segmentFileName(segment, state.segmentSuffix, TERMS_INDEX_EXTENSION);
indexIn = state.directory.openInput(indexName, state.context);
CodecUtil.checkIndexHeader (indexIn, TERMS_INDEX_CODEC_NAME, version, version, state.segmentInfo.getId(), state.segmentSuffix);
CodecUtil.checksumEntireFile(indexIn);
postingsReader.init(termsIn, state);
CodecUtil.retrieveChecksum(termsIn);
seekDir(termsIn, dirOffset);
seekDir(indexIn, indexDirOffset);
final int numFields = termsIn.readVInt();
for (int i = 0; i < numFields; ++i) {
final int field = termsIn.readVInt();
final long numTerms = termsIn.readVLong();
final int numBytes = termsIn.readVInt();
final BytesRef rootCode = new BytesRef(new byte[numBytes]);
termsIn.readBytes(rootCode.bytes, 0, numBytes);
rootCode.length = numBytes;
final FieldInfo fieldInfo = state.fieldInfos.fieldInfo(field);
final long sumTotalTermFreq = fieldInfo.getIndexOptions() == IndexOptions.DOCS ? -1 : termsIn.readVLong();
final long sumDocFreq = termsIn.readVLong();
final int docCount = termsIn.readVInt();
final int longsSize = termsIn.readVInt();
BytesRef minTerm = readBytesRef(termsIn);
BytesRef maxTerm = readBytesRef(termsIn);
final long indexStartFP = indexIn.readVLong();
FieldReader previous = fields.put(fieldInfo.name, new FieldReader(this, fieldInfo, numTerms, rootCode, sumTotalTermFreq, sumDocFreq, docCount, indexStartFP, longsSize, indexIn, minTerm, maxTerm));
}
indexIn.close();
success = true;
} finally {
}
}
BlockTreeTermsReader的核心功能是開啟.tim和.tip檔案並建立輸出流,然後建立FiledReader用於讀取資料。
函式中的segment為段名,例如”_0”,state.segmentSuffix假設返回Lucene50_0,TERMS_EXTENSION預設為tim,因此segmentFileName構造檔名_0_Lucene50_0.tim。
directory對於cfs檔案,返回Lucene50CompoundReader。
openInput函式返回SingleBufferImpl或者MultiBufferImpl,下面假設為SingleBufferImpl,termsIn封裝了_0_Lucene50_0.tim檔案的輸出流。
checkIndexHeader檢查頭資訊,和寫過程的writeIndexHeader函式對應。
和.tim檔案的開啟過程類似,BlockTreeTermsReader的建構函式接下來開啟_0_Lucene50_0.tip檔案,檢查頭資訊,同樣呼叫openInput返回的indexIn封裝了_0_Lucene50_0.tip檔案的輸出流。
seekDir最終呼叫SingleBufferImpl的父類ByteBufferIndexInput的seek函式,改變DirectByteBufferR的position指標的位置,用於略過一些頭資訊。然後從tim檔案中讀取並設定域的相應資訊。最後建立FieldReader並返回。
BlockTreeTermsReader::BlockTreeTermsReader->FieldReader::FieldReader
FieldReader(BlockTreeTermsReader parent, FieldInfo fieldInfo, long numTerms, BytesRef rootCode, long sumTotalTermFreq, long sumDocFreq, int docCount, long indexStartFP, int longsSize, IndexInput indexIn, BytesRef minTerm, BytesRef maxTerm) throws IOException {
this.fieldInfo = fieldInfo;
this.parent = parent;
this.numTerms = numTerms;
this.sumTotalTermFreq = sumTotalTermFreq;
this.sumDocFreq = sumDocFreq;
this.docCount = docCount;
this.indexStartFP = indexStartFP;
this.rootCode = rootCode;
this.longsSize = longsSize;
this.minTerm = minTerm;
this.maxTerm = maxTerm;
rootBlockFP = (new ByteArrayDataInput(rootCode.bytes, rootCode.offset, rootCode.length)).readVLong() >>> BlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS;
if (indexIn != null) {
final IndexInput clone = indexIn.clone();
clone.seek(indexStartFP);
index = new FST<>(clone, ByteSequenceOutputs.getSingleton());
} else {
index = null;
}
}
FieldReader函式的核心部分是建立一個FST,FST,全稱Finite State Transducer,用有限狀態機實現對詞典中單詞字首和字尾的重複利用,壓縮儲存空間,在上一章已經介紹瞭如何將FST中的資訊寫入.tip檔案,這一章後面介紹的過程相反,要將.tip檔案中的資料讀取出來。
rootBlockFP被建立為ByteArrayDataInput,ByteArrayDataInput對應的每個儲存結構的最高位bit用來表示是否後面的位置資訊有用。例如10000001(高位1表示後面的資料和前面的資料組成一個數據)+00000001最終其實為10000001。
seek函式調整ByteBufferIndexInput中當前ByteBuffer中的position位置為indexStartFP。
最後建立FST賦值給成員變數index。
BlockTreeTermsReader::BlockTreeTermsReader->FieldReader::FieldReader->FST::FST
public FST(DataInput in, Outputs<T> outputs) throws IOException {
this(in, outputs, DEFAULT_MAX_BLOCK_BITS);
}
public FST(DataInput in, Outputs<T> outputs, int maxBlockBits) throws IOException {
this.outputs = outputs;
version = CodecUtil.checkHeader(in, FILE_FORMAT_NAME, VERSION_PACKED, VERSION_NO_NODE_ARC_COUNTS);
packed = in.readByte() == 1;
if (in.readByte() == 1) {
BytesStore emptyBytes = new BytesStore(10);
int numBytes = in.readVInt();
emptyBytes.copyBytes(in, numBytes);
BytesReader reader;
if (packed) {
reader = emptyBytes.getForwardReader();
} else {
reader = emptyBytes.getReverseReader();
if (numBytes > 0) {
reader.setPosition(numBytes-1);
}
}
emptyOutput = outputs.readFinalOutput(reader);
} else {
emptyOutput = null;
}
final byte t = in.readByte();
switch(t) {
case 0:
inputType = INPUT_TYPE.BYTE1;
break;
case 1:
inputType = INPUT_TYPE.BYTE2;
break;
case 2:
inputType = INPUT_TYPE.BYTE4;
break;
default:
throw new IllegalStateException("invalid input type " + t);
}
if (packed) {
nodeRefToAddress = PackedInts.getReader(in);
} else {
nodeRefToAddress = null;
}
startNode = in.readVLong();
if (version < VERSION_NO_NODE_ARC_COUNTS) {
in.readVLong();
in.readVLong();
in.readVLong();
}
long numBytes = in.readVLong();
if (numBytes > 1 << maxBlockBits) {
bytes = new BytesStore(in, numBytes, 1<<maxBlockBits);
bytesArray = null;
} else {
bytes = null;
bytesArray = new byte[(int) numBytes];
in.readBytes(bytesArray, 0, bytesArray.length);
}
cacheRootArcs();
}
FST的建構函式簡而言之就是從.tip檔案中讀取寫入的各個索引,並進行初始化。
傳入的引數DEFAULT_MAX_BLOCK_BITS表示讀取檔案時每個塊的大小,預設為30個bit。
checkHeader檢查.tip檔案的合法性。getForwardReader和getReverseReader返回FST.BytesReader。getForwardReader返回的BytesReader從快取中向前讀取資料,getReverseReader向後讀取資料。
讀取資料型別至inputType,即一個Term中的每個元素佔多少位元組。
最後讀取了.tip檔案最核心的內容並存儲至bytesArray中,即倒排索引寫過程中寫入樹的每個節點的資訊。
cacheRootArcs函式對bytesArray中的資料進行解析並快取根節點。
BlockTreeTermsReader::BlockTreeTermsReader->FieldReader::FieldReader->FST::FST->cacheRootArcs
private void cacheRootArcs() throws IOException {
final Arc<T> arc = new Arc<>();
getFirstArc(arc);
if (targetHasArcs(arc)) {
final BytesReader in = getBytesReader();
Arc<T>[] arcs = (Arc<T>[]) new Arc[0x80];
readFirstRealTargetArc(arc.target, arc, in);
int count = 0;
while(true) {
if (arc.label < arcs.length) {
arcs[arc.label] = new Arc<T>().copyFrom(arc);
} else {
break;
}
if (arc.isLast()) {
break;
}
readNextRealArc(arc, in);
count++;
}
int cacheRAM = (int) ramBytesUsed(arcs);
if (count >= FIXED_ARRAY_NUM_ARCS_SHALLOW && cacheRAM < ramBytesUsed()/5) {
cachedRootArcs = arcs;
cachedArcsBytesUsed = cacheRAM;
}
}
}
cacheRootArcs函式首先建立Arc,並呼叫getFirstArc對第一個節點進行初始化。targetHasArcs函式判斷是否有可讀資訊,即在.tip檔案中,一個節點是否有下一個節點。接著呼叫readFirstRealTargetArc讀取第一個節點也即根節點的資訊,這裡就不往下看了,其中最重要的是讀取該節點的內容label和下一個節點在bytesArray快取中的位置。
再往下看cacheRootArcs函式,接下來通過一個while迴圈讀取其他的根節點,如果讀取的內容label大於128或者已經讀取到最後的一個葉子節點,就退出迴圈,否則將讀取到的節點資訊存入arcs中,最後根據條件快取到cachedRootArcs和cachedArcsBytesUsed成員變數裡。
BlockTreeTermsReader::BlockTreeTermsReader->FieldReader::FieldReader->FST::FST->cacheRootArcs->getFirstArc
public Arc<T> getFirstArc(Arc<T> arc) {
T NO_OUTPUT = outputs.getNoOutput();
if (emptyOutput != null) {
arc.flags = BIT_FINAL_ARC | BIT_LAST_ARC;
arc.nextFinalOutput = emptyOutput;
if (emptyOutput != NO_OUTPUT) {
arc.flags |= BIT_ARC_HAS_FINAL_OUTPUT;
}
} else {
arc.flags = BIT_LAST_ARC;
arc.nextFinalOutput = NO_OUTPUT;
}
arc.output = NO_OUTPUT;
arc.target = startNode;
return arc;
}
getFirstArc函式用來初始化第一個節點,最重要的是設定了最後的arc.target,標識了一會從.tip核心內容的快取bytesArray的哪個位置開始讀。
下面開始分析SegmentTermsEnum的seekExact函式,先看一下SegmentTermsEnum的建構函式。
SegmentTermsEnum::SegmentTermsEnum
public SegmentTermsEnum(FieldReader fr) throws IOException {
this.fr = fr;
stack = new SegmentTermsEnumFrame[0];
staticFrame = new SegmentTermsEnumFrame(this, -1);
if (fr.index == null) {
fstReader = null;
} else {
fstReader = fr.index.getBytesReader();
}
for(int arcIdx=0;arcIdx<arcs.length;arcIdx++) {
arcs[arcIdx] = new FST.Arc<>();
}
currentFrame = staticFrame;
validIndexPrefix = 0;
}
根據前面的分析,FieldReader的成員變數index是前面構造的FST,其建構函式讀取了.tip檔案,快取了其核心內容到bytesArray中,並標記了起始位置為startNode。如果該index不為null,接下來的getBytesReader返回的就是bytesArray。
SegmentTermsEnum::seekExact
第一部分
public boolean seekExact(BytesRef target) throws IOException {
term.grow(1 + target.length);
FST.Arc<BytesRef> arc;
int targetUpto;
BytesRef output;
targetBeforeCurrentLength = currentFrame.ord;
if (currentFrame != staticFrame) {
...
} else {
targetBeforeCurrentLength = -1;
arc = fr.index.getFirstArc(arcs[0]);
output = arc.output;
currentFrame = staticFrame;
targetUpto = 0;
currentFrame = pushFrame(arc, BlockTreeTermsReader.FST_OUTPUTS.add(output, arc.nextFinalOutput), 0);
}
...
}
這裡的fr是前面建立的FieldReader,index是FST,內部分裝了從.tip檔案讀取的資訊,FST_OUTPUTS是ByteSequenceOutputs。ByteSequenceOutputs的add函式合併arc.output和arc.nextFinalOutput兩個BytesRef。
currentFrame和staticFrame不相等的情況不是第一次呼叫seekExact,if裡省略的程式碼會利用之前的查詢結果,本章不分析這種情況。
如果currentFrame和staticFrame相等,就呼叫getFirstArc初始化第一個Arc,最後pushFrame獲得對應位置上(這裡是第一個)的SegmentTermsEnumFrame並進行相應的設定。一個SegmentTermsEnumFrame代表的是一層節點,並不是一個節點,一層節點表示樹中大於1個以上葉子節點到下一個該種節點間的所有節點。
SegmentTermsEnum::seekExact->SegmentTermsEnumFrame::pushFrame
SegmentTermsEnumFrame pushFrame(FST.Arc<BytesRef> arc, BytesRef frameData, int length) throws IOException {
scratchReader.reset(frameData.bytes, frameData.offset, frameData.length);
final long code = scratchReader.readVLong();
final long fpSeek = code >>> BlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS;
final SegmentTermsEnumFrame f = getFrame(1+currentFrame.ord);
f.hasTerms = (code & BlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS) != 0;
f.hasTermsOrig = f.hasTerms;
f.isFloor = (code & BlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR) != 0;
if (f.isFloor) {
f.setFloorData(scratchReader, frameData);
}
pushFrame(arc, fpSeek, length);
return f;
}
frameData儲存了從.tip檔案中讀取的該節點對應的下一層節點的所有資訊,即Arc結構中的nextFinalOutput。getFrame函式從SegmentTermsEnumFrame陣列stack中獲取對應位置上的SegmentTermsEnumFrame結構,然後呼叫pushFrame對其設定記錄資訊。
繼續看seekExact函式的後一部分。
SegmentTermsEnum::seekExact
第二部分
public boolean seekExact(BytesRef target) throws IOException {
...
while (targetUpto < target.length) {
final int targetLabel = target.bytes[target.offset + targetUpto] & 0xFF;
final FST.Arc<BytesRef> nextArc = fr.index.findTargetArc(targetLabel, arc, getArc(1+targetUpto), fstReader);
if (nextArc == null) {
validIndexPrefix = currentFrame.prefix;
currentFrame.scanToFloorFrame(target);
if (!currentFrame.hasTerms) {
termExists = false;
term.setByteAt(targetUpto, (byte) targetLabel);
term.setLength(1+targetUpto);
return false;
}
currentFrame.loadBlock();
final SeekStatus result = currentFrame.scanToTerm(target, true);
if (result == SeekStatus.FOUND) {
return true;
} else {
return false;
}
} else {
arc = nextArc;
term.setByteAt(targetUpto, (byte) targetLabel);
if (arc.output != BlockTreeTermsReader.NO_OUTPUT) {
output = BlockTreeTermsReader.FST_OUTPUTS.add(output, arc.output);
}
targetUpto++;
if (arc.isFinal()) {
currentFrame = pushFrame(arc, BlockTreeTermsReader.FST_OUTPUTS.add(output, arc.nextFinalOutput), targetUpto);
}
}
}
validIndexPrefix = currentFrame.prefix;
currentFrame.scanToFloorFrame(target);
if (!currentFrame.hasTerms) {
termExists = false;
term.setLength(targetUpto);
return false;
}
currentFrame.loadBlock();
final SeekStatus result = currentFrame.scanToTerm(target, true);
if (result == SeekStatus.FOUND) {
return true;
} else {
return false;
}
}
getArc函式每次在SegmentTermsEnum的成員變數Arc陣列arcs中分配一個Arc結構,用於存放下一個節點資訊,例如查詢“abc”,如果當前查詢“a”,有可能下一個節點即為“b”。
findTargetArc查詢byte對應節點。
if部分表示找到了最後一層節點,或者沒找到節點,scanToFloorFrame函式首先從.tip檔案讀取的結果中獲取.tim檔案中的指標。如果currentFrame.hasTerms為false,則表示沒有找到Term,此時就直接返回了。如果找到了,則首先通過loadBlock函式從.tim檔案中讀取餘下的資訊,再呼叫scanToTerm進行比較,返回最終的結果。
這個舉個例子,假設lucene索引中儲存了“aab”、“aac”兩個Term,在呼叫loadBlock前,已經找到了“aa”在.tip檔案中資訊,loadBlock函式就是根據“aa”在.tip檔案中提供的指標位置,在.tim檔案中獲取到了b、c。
else部分表示找到了節點,則將查詢到的label快取到term中,如果到達了該層的最後一個節點,就呼叫pushFrame函式建立一個SegmentTermsEnumFrame記錄下一層節點的資訊。
SegmentTermsEnum::seekExact->getArc->findTargetArc
public Arc<T> findTargetArc(int labelToMatch, Arc<T> follow, Arc<T> arc, BytesReader in) throws IOException {
return findTargetArc(labelToMatch, follow, arc, in, true);
}
private Arc<T> findTargetArc(int labelToMatch, Arc<T> follow, Arc<T> arc, BytesReader in, boolean useRootArcCache) throws IOException {
...
in.setPosition(getNodeAddress(follow.target));
arc.node = follow.target;
...
readFirstRealTargetArc(follow.target, arc, in);
while(true) {
if (arc.label == labelToMatch) {
return arc;
} else if (arc.label > labelToMatch) {
return null;
} else if (arc.isLast()) {
return null;
} else {
readNextRealArc(arc, in);
}
}
}
省略的部分程式碼處理兩種情況,一種情況是要查詢的byte是個結束字元-1,另一種是直接從快取cachedRootArcs查詢。
第二部分省略的程式碼是當節點數量相同時採用二分法查詢。
剩下的程式碼就是線性搜尋了,傳入的引數in就是對應.tip檔案核心內容的快取,即前面讀取到的bytesArray,follow的target變數儲存了第一個節點在.tip檔案快取中的指標位置,呼叫setPosition調整指標位置。
如果是線性搜尋,則首先呼叫readFirstRealTargetArc讀取根節點資訊到arc,讀取的資訊最重要的一是根節點的label,二是根節點的下一個節點。如果匹配到要查詢的labelToMatch就直接返回該節點,否則繼續讀取下一個節點直到匹配到或返回。
進入seekExact函式的if部分,scanToFloorFrame根據.tip檔案中的資訊獲取最後的葉子節點在.tim檔案中的指標,loadBlock則從.tim檔案中讀取最後的資訊。
SegmentTermsEnum::seekExact->SegmentTermsEnumFrame::loadBlock
void loadBlock() throws IOException {
ste.initIndexInput();
ste.in.seek(fp);
int code = ste.in.readVInt();
entCount = code >>> 1;
isLastInFloor = (code & 1) != 0;
code = ste.in.readVInt();
isLeafBlock = (code & 1) != 0;
int numBytes = code >>> 1;
if (suffixBytes.length < numBytes) {
suffixBytes = new byte[ArrayUtil.oversize(numBytes, 1)];
}
ste.in.readBytes(suffixBytes, 0, numBytes);
suffixesReader.reset(suffixBytes, 0, numBytes);
numBytes = ste.in.readVInt();
if (statBytes.length < numBytes) {
statBytes = new byte[ArrayUtil.oversize(numBytes, 1)];
}
ste.in.readBytes(statBytes, 0, numBytes);
statsReader.reset(statBytes, 0, numBytes);
metaDataUpto = 0;
state.termBlockOrd = 0;
nextEnt = 0;
lastSubFP = -1;
numBytes = ste.in.readVInt();
if (bytes.length < numBytes) {
bytes = new byte[ArrayUtil.oversize(numBytes, 1)];
}
ste.in.readBytes(bytes, 0, numBytes);
bytesReader.reset(bytes, 0, numBytes);
fpEnd = ste.in.getFilePointer();
}
ste為SegmentTermsEnum。initIndexInput函式設定SegmentTermsEnum的成員變數in為BlockTreeTermsReader中的termsIn,對應.tim檔案的輸出流。fp為檔案中的指標位置,在.tip檔案中讀取出來的。seek調整termsIn的讀取位置。然後從tim檔案讀取資料到suffixBytes中,在封裝到suffixBytes中。讀取資料到statBytes中,封裝到statsReader中。讀取資料到bytes中,封裝到bytesReader中。其中suffixBytes中封裝的是待比較的資料。
SegmentTermsEnum::seekExact->SegmentTermsEnumFrame::scanToTerm
public SeekStatus scanToTerm(BytesRef target, boolean exactOnly) throws IOException {
return isLeafBlock ? scanToTermLeaf(target, exactOnly) : scanToTermNonLeaf(target, exactOnly);
}
public SeekStatus scanToTermLeaf(BytesRef target, boolean exactOnly) throws IOException {
ste.termExists = true;
subCode = 0;
if (nextEnt == entCount) {
if (exactOnly) {
fillTerm();
}
return SeekStatus.END;
}
nextTerm: while (true) {
nextEnt++;
suffix = suffixesReader.readVInt();
final int termLen = prefix + suffix;
startBytePos = suffixesReader.getPosition();
suffixesReader.skipBytes(suffix);
final int targetLimit = target.offset + (target.length < termLen ? target.length : termLen);
int targetPos = target.offset + prefix;
int bytePos = startBytePos;
while(true) {
final int cmp;
final boolean stop;
if (targetPos < targetLimit) {
cmp = (suffixBytes[bytePos++]&0xFF) - (target.bytes[targetPos++]&0xFF);
stop = false;
} else {
cmp = termLen - target.length;
stop = true;
}
if (cmp < 0) {
if (nextEnt == entCount) {
break nextTerm;
} else {
continue nextTerm;
}
} else if (cmp > 0) {
fillTerm();
return SeekStatus.NOT_FOUND;
} else if (stop) {
fillTerm();
return SeekStatus.FOUND;
}
}
}
if (exactOnly) {
fillTerm();
}
return SeekStatus.END;
}
suffixesReader中有多個可以匹配的term,外層的while迴圈依次取出每個term,其中prefix是已經匹配的長度,不需要再匹配的,因為該長度已經對應到一個block中了(block下面包含多個suffix)。suffix儲存了term的長度,startBytePos儲存了該term在suffixesReader也即在suffixBytes中的偏移。內層的while迴圈依次比對每個位元組,直到每個位元組都相等,targetPos會等於targetLimit,stop被設為true。其他情況下,例如遍歷了所有suffix都沒找到,或者cmp大於0(suffix中的位元組按順序排序),意味著該block中找不到匹配的term,則也返回。
最後如果找到了,就返回SeekStatus.FOUND。
總結
下面通過一個例子總結一下lucene倒排索引的讀過程。
假設索引檔案中儲存了“abc”“abd”兩個字串,待查詢的字串為“abc”,首先從.tip檔案中按層次按節點查詢“a”節點、再查詢“b”節點(findTargetArc函式),獲得“b”節點後繼續從.tip檔案中讀取剩下的部分在.tim檔案中的指標(scanToFloorFrame函式),然後從.tim檔案中讀取了“c”和“d”(loadBlock函式),最後比較獲得最終結果(scanToTerm函式)。