1. 程式人生 > >lucene原始碼分析---10

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函式)。