<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    深度解析 Lucene 輕量級(jí)全文索引實(shí)現(xiàn)原理

    共 14474字,需瀏覽 29分鐘

     ·

    2021-07-17 10:30


    作者:vivo互聯(lián)網(wǎng)服務(wù)器團(tuán)隊(duì)-Qian Yulun


    一、Lucene簡(jiǎn)介


    1.1 Lucene是什么?


    • Lucene是Apache基金會(huì)jakarta項(xiàng)目組的一個(gè)子項(xiàng)目;

    • Lucene是一個(gè)開(kāi)放源碼的全文檢索引擎工具包,提供了完整的查詢引擎和索引引擎,部分語(yǔ)種文本分析引擎;

    • Lucene并不是一個(gè)完整的全文檢索引擎,僅提供了全文檢索引擎架構(gòu),但仍可以作為一個(gè)工具包結(jié)合各類插件為項(xiàng)目提供部分高性能的全文檢索功能;

    • 現(xiàn)在常用的ElasticSearch、Solr等全文搜索引擎均是基于Lucene實(shí)現(xiàn)的。


    1.2 Lucene的使用場(chǎng)景


    適用于需要數(shù)據(jù)索引量不大的場(chǎng)景,當(dāng)索引量過(guò)大時(shí)需要使用ES、Solr等全文搜索服務(wù)器實(shí)現(xiàn)搜索功能。


    1.3 通過(guò)本文你能了解到哪些內(nèi)容?


    • Lucene如此繁雜的索引如何生成并寫入,索引中的各個(gè)文件又在起著什么樣的作用?

    • Lucene全文索引如何進(jìn)行高效搜索?

    • Lucene如何優(yōu)化搜索結(jié)果,使用戶根據(jù)關(guān)鍵詞搜索到想要的內(nèi)容?


    本文旨在分享Lucene搜索引擎的源碼閱讀和功能開(kāi)發(fā)中的經(jīng)驗(yàn),Lucene采用7.3.1版本。


    二、Lucene基礎(chǔ)工作流程


    索引的生成分為兩個(gè)部分:


    1. 創(chuàng)建階段:

    • 添加文檔階段,通過(guò)IndexWriter調(diào)用addDocument方法生成正向索引文件;

    • 文檔添加后,通過(guò)flush或merge操作生成倒排索引文件。


    2. 搜索階段:

    • 用戶通過(guò)查詢語(yǔ)句向Lucene發(fā)送查詢請(qǐng)求;

    • 通過(guò)IndexSearch下的IndexReader讀取索引庫(kù)內(nèi)容,獲取文檔索引;

    • 得到搜索結(jié)果后,基于搜索算法對(duì)結(jié)果進(jìn)行排序后返回。


    索引創(chuàng)建及搜索流程如下圖所示:



    三、Lucene索引構(gòu)成


    3.1 正向索引


    Lucene的基礎(chǔ)層次結(jié)構(gòu)由索引、段、文檔、域、詞五個(gè)部分組成。正向索引的生成即為基于Lucene的基礎(chǔ)層次結(jié)構(gòu)一級(jí)一級(jí)處理文檔并分解域存儲(chǔ)詞的過(guò)程。



    索引文件層級(jí)關(guān)系如圖1所示:


    • 索引:Lucene索引庫(kù)包含了搜索文本的所有內(nèi)容,可以通過(guò)文件或文件流的方式存儲(chǔ)在不同的數(shù)據(jù)庫(kù)或文件目錄下。

    • :一個(gè)索引中包含多個(gè)段,段與段之間相互獨(dú)立。由于Lucene進(jìn)行關(guān)鍵詞檢索時(shí)需要加載索引段進(jìn)行下一步搜索,如果索引段較多會(huì)增加較大的I/O開(kāi)銷,減慢檢索速度,因此寫入時(shí)會(huì)通過(guò)段合并策略對(duì)不同的段進(jìn)行合并。

    • 文檔:Lucene會(huì)將文檔寫入段中,一個(gè)段中包含多個(gè)文檔。

    • :一篇文檔會(huì)包含多種不同的字段,不同的字段保存在不同的域中。

    • :Lucene會(huì)通過(guò)分詞器將域中的字符串通過(guò)詞法分析和語(yǔ)言處理后拆分成詞,Lucene通過(guò)這些關(guān)鍵詞進(jìn)行全文檢索。


    3.2 倒排索引


    Lucene全文索引的核心是基于倒排索引實(shí)現(xiàn)的快速索引機(jī)制。


    倒排索引原理如圖2所示,倒排索引簡(jiǎn)單來(lái)說(shuō)就是基于分析器將文本內(nèi)容進(jìn)行分詞后,記錄每個(gè)詞出現(xiàn)在哪篇文章中,從而通過(guò)用戶輸入的搜索詞查詢出包含該詞的文章。



    問(wèn)題:上述倒排索引使用時(shí)每次都需要將索引詞加載到內(nèi)存中,當(dāng)文章數(shù)量較多,篇幅較長(zhǎng)時(shí),索引詞可能會(huì)占用大量的存儲(chǔ)空間,加載到內(nèi)存后內(nèi)存損耗較大。


    解決方案:從Lucene4開(kāi)始,Lucene采用了FST來(lái)減少索引詞帶來(lái)的空間消耗。


    FST(Finite StateTransducers),中文名有限狀態(tài)機(jī)轉(zhuǎn)換器。其主要特點(diǎn)在于以下四點(diǎn):

    • 查找詞的時(shí)間復(fù)雜度為O(len(str));

    • 通過(guò)將前綴和后綴分開(kāi)存儲(chǔ)的方式,減少了存放詞所需的空間;

    • 加載時(shí)僅將前綴放入內(nèi)存索引,后綴詞在磁盤中進(jìn)行存放,減少了內(nèi)存索引使用空間的損耗;

    • FST結(jié)構(gòu)在對(duì)PrefixQuery、FuzzyQuery、RegexpQuery等查詢條件查詢時(shí),查詢效率高。


    具體存儲(chǔ)方式如圖3所示:



    倒排索引相關(guān)文件包含.tip、.tim和.doc這三個(gè)文件,其中:

    • tip:用于保存倒排索引Term的前綴,來(lái)快速定位.tim文件中屬于這個(gè)Field的Term的位置,即上圖中的aab、abd、bdc。

    • tim:保存了不同前綴對(duì)應(yīng)的相應(yīng)的Term及相應(yīng)的倒排表信息,倒排表通過(guò)跳表實(shí)現(xiàn)快速查找,通過(guò)跳表能夠跳過(guò)一些元素的方式對(duì)多條件查詢交集、并集、差集之類的集合運(yùn)算也提高了性能。

    • doc:包含了文檔號(hào)及詞頻信息,根據(jù)倒排表中的內(nèi)容返回該文件中保存的文本信息。


    3.3 索引查詢及文檔搜索過(guò)程


    Lucene利用倒排索引定位需要查詢的文檔號(hào),通過(guò)文檔號(hào)搜索出文件后,再利用詞權(quán)重等信息對(duì)文檔排序后返回。

    • 內(nèi)存加載tip文件,根據(jù)FST匹配到后綴詞塊在tim文件中的位置;

    • 根據(jù)查詢到的后綴詞塊位置查詢到后綴及倒排表的相關(guān)信息;

    • 根據(jù)tim中查詢到的倒排表信息從doc文件中定位出文檔號(hào)及詞頻信息,完成搜索;

    • 文件定位完成后Lucene將去.fdx文件目錄索引及.fdt中根據(jù)正向索引查找出目標(biāo)文件。


    文件格式如圖4所示:



    上文主要講解Lucene的工作原理,下文將闡述Java中Lucene執(zhí)行索引、查詢等操作的相關(guān)代碼。


    四、Lucene的增刪改操作


    Lucene項(xiàng)目中文本的解析,存儲(chǔ)等操作均由IndexWriter類實(shí)現(xiàn),IndexWriter文件主要由Directory和IndexWriterConfig兩個(gè)類構(gòu)成,其中:

    Directory:用于指定存放索引文件的目錄類型。既然要對(duì)文本內(nèi)容進(jìn)行搜索,自然需要先將這些文本內(nèi)容及索引信息寫入到目錄里。Directory是一個(gè)抽象類,針對(duì)索引的存儲(chǔ)允許有多種不同的實(shí)現(xiàn)。常見(jiàn)的存儲(chǔ)方式一般包括存儲(chǔ)有本地(FSDirectory),內(nèi)存(RAMDirectory)等。
    IndexWriterConfig:用于指定IndexWriter在文件內(nèi)容寫入時(shí)的相關(guān)配置,包括OpenMode索引構(gòu)建模式、Similarity相關(guān)性算法等。


    IndexWriter具體是如何操作索引的呢?讓我們來(lái)簡(jiǎn)單分析一下IndexWriter索引操作的相關(guān)源碼。


    4.1. 文檔的新增


    a. Lucene會(huì)為每個(gè)文檔創(chuàng)建ThreadState對(duì)象,對(duì)象持有DocumentWriterPerThread來(lái)執(zhí)行文件的增刪改操作;


    ThreadState getAndLock(Thread requestingThread, DocumentsWriter documentsWriter) {  ThreadState threadState = null;  synchronized (this) {    if (freeList.isEmpty()) {      // 如果不存在已創(chuàng)建的空閑ThreadState,則新創(chuàng)建一個(gè)      return newThreadState();    } else {      // freeList后進(jìn)先出,僅使用有限的ThreadState操作索引      threadState = freeList.remove(freeList.size()-1);
    // 優(yōu)先使用已經(jīng)初始化過(guò)DocumentWriterPerThread的ThreadState,并將其與當(dāng)前 // ThreadState換位,將其移到隊(duì)尾優(yōu)先使用 if (threadState.dwpt == null) { for(int i=0;i<freeList.size();i++) { ThreadState ts = freeList.get(i); if (ts.dwpt != null) { freeList.set(i, threadState); threadState = ts; break; } } } } } threadState.lock(); return threadState;}


    b. 索引文件的插入:DocumentWriterPerThread調(diào)用DefaultIndexChain下的processField來(lái)處理文檔中的每個(gè)域,processField方法是索引鏈的核心執(zhí)行邏輯。通過(guò)用戶對(duì)每個(gè)域設(shè)置的不同的FieldType進(jìn)行相應(yīng)的索引、分詞、存儲(chǔ)等操作。FieldType中比較重要的是indexOptions:


    • NONE:域信息不會(huì)寫入倒排表,索引階段無(wú)法通過(guò)該域名進(jìn)行搜索;

    • DOCS:文檔寫入倒排表,但由于不記錄詞頻信息,因此出現(xiàn)多次也僅當(dāng)一次處理;

    • DOCS_AND_FREQS:文檔和詞頻寫入倒排表;

    • DOCS_AND_FREQS_AND_POSITIONS:文檔、詞頻及位置寫入倒排表;

    • DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS:文檔、詞頻、位置及偏移寫入倒排表。


    // 構(gòu)建倒排表
    if (fieldType.indexOptions() != IndexOptions.NONE) { fp = getOrAddField(fieldName, fieldType, true); boolean first = fp.fieldGen != fieldGen; // field具體的索引、分詞操作 fp.invert(field, first);
    if (first) { fields[fieldCount++] = fp; fp.fieldGen = fieldGen; }} else { verifyUnIndexedFieldType(fieldName, fieldType);}
    // 存儲(chǔ)該field的storeFieldif (fieldType.stored()) { if (fp == null) { fp = getOrAddField(fieldName, fieldType, false); } if (fieldType.stored()) { String value = field.stringValue(); if (value != null && value.length() > IndexWriter.MAX_STORED_STRING_LENGTH) { throw new IllegalArgumentException("stored field \"" + field.name() + "\" is too large (" + value.length() + " characters) to store"); } try { storedFieldsConsumer.writeField(fp.fieldInfo, field); } catch (Throwable th) { throw AbortingException.wrap(th); } }}
    // 建立DocValue(通過(guò)文檔查詢文檔下包含了哪些詞)DocValuesType dvType = fieldType.docValuesType();if (dvType == null) { throw new NullPointerException("docValuesType must not be null (field: \"" + fieldName + "\")");}if (dvType != DocValuesType.NONE) { if (fp == null) { fp = getOrAddField(fieldName, fieldType, false); } indexDocValue(fp, dvType, field);}if (fieldType.pointDimensionCount() != 0) { if (fp == null) { fp = getOrAddField(fieldName, fieldType, false); } indexPoint(fp, field);}


    c. 解析Field首先需要構(gòu)造TokenStream類,用于產(chǎn)生和轉(zhuǎn)換token流,TokenStream有兩個(gè)重要的派生類Tokenizer和TokenFilter,其中Tokenizer用于通過(guò)java.io.Reader類讀取字符,產(chǎn)生Token流,然后通過(guò)任意數(shù)量的TokenFilter來(lái)處理這些輸入的Token流,具體源碼如下:


    // invert:對(duì)Field進(jìn)行分詞處理首先需要將Field轉(zhuǎn)化為TokenStreamtry (TokenStream stream = tokenStream = field.tokenStream(docState.analyzer, tokenStream))// TokenStream在不同分詞器下實(shí)現(xiàn)不同,根據(jù)不同分詞器返回相應(yīng)的TokenStreamif (tokenStream != null) {  return tokenStream;} else if (readerValue() != null) {  return analyzer.tokenStream(name(), readerValue());} else if (stringValue() != null) {  return analyzer.tokenStream(name(), stringValue());}
    public final TokenStream tokenStream(final String fieldName, final Reader reader) { // 通過(guò)復(fù)用策略,如果TokenStreamComponents中已經(jīng)存在Component則復(fù)用。 TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName); final Reader r = initReader(fieldName, reader); // 如果Component不存在,則根據(jù)分詞器創(chuàng)建對(duì)應(yīng)的Components。 if (components == null) { components = createComponents(fieldName); reuseStrategy.setReusableComponents(this, fieldName, components); } // 將java.io.Reader輸入流傳入Component中。 components.setReader(r); return components.getTokenStream();}


    d. 根據(jù)IndexWriterConfig中配置的分詞器,通過(guò)策略模式返回分詞器對(duì)應(yīng)的分詞組件,針對(duì)不同的語(yǔ)言及不同的分詞需求,分詞組件存在很多不同的實(shí)現(xiàn)。


    • StopAnalyzer:停用詞分詞器,用于過(guò)濾詞匯中特定字符串或單詞。

    • StandardAnalyzer:標(biāo)準(zhǔn)分詞器,能夠根據(jù)數(shù)字、字母等進(jìn)行分詞,支持詞表過(guò)濾替代StopAnalyzer功能,支持中文簡(jiǎn)單分詞。

    • CJKAnalyzer:能夠根據(jù)中文語(yǔ)言習(xí)慣對(duì)中文分詞提供了比較好的支持。


     以StandardAnalyzer(標(biāo)準(zhǔn)分詞器)為例:

    // 標(biāo)準(zhǔn)分詞器創(chuàng)建Component過(guò)程,涵蓋了標(biāo)準(zhǔn)分詞處理器、Term轉(zhuǎn)化小寫、常用詞過(guò)濾三個(gè)功能protected TokenStreamComponents createComponents(final String fieldName) {  final StandardTokenizer src = new StandardTokenizer();  src.setMaxTokenLength(maxTokenLength);  TokenStream tok = new StandardFilter(src);  tok = new LowerCaseFilter(tok);  tok = new StopFilter(tok, stopwords);  return new TokenStreamComponents(src, tok) {    @Override    protected void setReader(final Reader reader) {      src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);      super.setReader(reader);    }  };}


    e. 在獲取TokenStream之后通過(guò)TokenStream中的incrementToken方法分析并獲取屬性,再通過(guò)TermsHashPerField下的add方法構(gòu)建倒排表,最終將Field的相關(guān)數(shù)據(jù)存儲(chǔ)到類型為FreqProxPostingsArray的freqProxPostingsArray中,以及TermVectorsPostingsArray的termVectorsPostingsArray中,構(gòu)成倒排表;


    // 以LowerCaseFilter為例,通過(guò)其下的increamentToken將Token中的字符轉(zhuǎn)化為小寫public final boolean incrementToken() throws IOException {  if (input.incrementToken()) {    CharacterUtils.toLowerCase(termAtt.buffer(), 0, termAtt.length());    return true;  } else    return false;}
      try (TokenStream stream = tokenStream = field.tokenStream(docState.analyzer, tokenStream)) {    // reset TokenStream    stream.reset();    invertState.setAttributeSource(stream);    termsHashPerField.start(field, first);    // 分析并獲取Token屬性    while (stream.incrementToken()) {      ……      try {        // 構(gòu)建倒排表        termsHashPerField.add();      } catch (MaxBytesLengthExceededException e) {        ……      } catch (Throwable th) {        throw AbortingException.wrap(th);      }    }    ……}


    4.2 文檔的刪除


    a. Lucene下文檔的刪除,首先將要?jiǎng)h除的Term或Query添加到刪除隊(duì)列中;

    synchronized long deleteTerms(final Term... terms) throws IOException {  // TODO why is this synchronized?  final DocumentsWriterDeleteQueue deleteQueue = this.deleteQueue;  // 文檔刪除操作是將刪除的詞信息添加到刪除隊(duì)列中,根據(jù)flush策略進(jìn)行刪除  long seqNo = deleteQueue.addDelete(terms);  flushControl.doOnDelete();  lastSeqNo = Math.max(lastSeqNo, seqNo);  if (applyAllDeletes(deleteQueue)) {    seqNo = -seqNo;  }  return seqNo;}


    b. 根據(jù)Flush策略觸發(fā)刪除操作;

    private boolean applyAllDeletes(DocumentsWriterDeleteQueue deleteQueue) throws IOException {  // 判斷是否滿足刪除條件 --> onDelete  if (flushControl.getAndResetApplyAllDeletes()) {    if (deleteQueue != null) {      ticketQueue.addDeletes(deleteQueue);    }    // 指定執(zhí)行刪除操作的event    putEvent(ApplyDeletesEvent.INSTANCE); // apply deletes event forces a purge    return true;  }  return false;}
    public void onDelete(DocumentsWriterFlushControl control, ThreadState state) {  // 判斷并設(shè)置是否滿足刪除條件  if ((flushOnRAM() && control.getDeleteBytesUsed() > 1024*1024*indexWriterConfig.getRAMBufferSizeMB())) {    control.setApplyAllDeletes();    if (infoStream.isEnabled("FP")) {      infoStream.message("FP", "force apply deletes bytesUsed=" + control.getDeleteBytesUsed() + " vs ramBufferMB=" + indexWriterConfig.getRAMBufferSizeMB());    }  }}


    4.3 文檔的更新


    文檔的更新就是一個(gè)先刪除后插入的過(guò)程,本文就不再做更多贅述。


    4.4 索引Flush


    文檔寫入到一定數(shù)量后,會(huì)由某一線程觸發(fā)IndexWriter的Flush操作,生成段并將內(nèi)存中的Document信息寫到硬盤上。Flush操作目前僅有一種策略:FlushByRamOrCountsPolicy。FlushByRamOrCountsPolicy主要基于兩種策略自動(dòng)執(zhí)行Flush操作:


    • maxBufferedDocs:文檔收集到一定數(shù)量時(shí)觸發(fā)Flush操作。

    • ramBufferSizeMB:文檔內(nèi)容達(dá)到限定值時(shí)觸發(fā)Flush操作。


    其中 activeBytes() 為dwpt收集的索引所占的內(nèi)存量,deleteByteUsed為刪除的索引量。

    @Overridepublic void onInsert(DocumentsWriterFlushControl control, ThreadState state) {  // 根據(jù)文檔數(shù)進(jìn)行Flush  if (flushOnDocCount()      && state.dwpt.getNumDocsInRAM() >= indexWriterConfig          .getMaxBufferedDocs()) {    // Flush this state by num docs    control.setFlushPending(state);  // 根據(jù)內(nèi)存使用量進(jìn)行Flush  } else if (flushOnRAM()) {// flush by RAM    final long limit = (long) (indexWriterConfig.getRAMBufferSizeMB() * 1024.d * 1024.d);    final long totalRam = control.activeBytes() + control.getDeleteBytesUsed();    if (totalRam >= limit) {      if (infoStream.isEnabled("FP")) {        infoStream.message("FP", "trigger flush: activeBytes=" + control.activeBytes() + " deleteBytes=" + control.getDeleteBytesUsed() + " vs limit=" + limit);      }      markLargestWriterPending(control, state, totalRam);    }  }}


    將內(nèi)存信息寫入索引庫(kù)。



    索引的Flush分為主動(dòng)Flush和自動(dòng)Flush,根據(jù)策略觸發(fā)的Flush操作為自動(dòng)Flush,主動(dòng)Flush的執(zhí)行與自動(dòng)Flush有較大區(qū)別,關(guān)于主動(dòng)Flush本文暫不多做贅述。需要了解的話可以跳轉(zhuǎn)鏈接


    4.5 索引段Merge


    索引Flush時(shí)每個(gè)dwpt會(huì)單獨(dú)生成一個(gè)segment,當(dāng)segment過(guò)多時(shí)進(jìn)行全文檢索可能會(huì)跨多個(gè)segment,產(chǎn)生多次加載的情況,因此需要對(duì)過(guò)多的segment進(jìn)行合并。


    段合并的執(zhí)行通過(guò)MergeScheduler進(jìn)行管理。mergeScheduler也包含了多種管理策略,包括NoMergeScheduler、SerialMergeScheduler和ConcurrentMergeScheduler。


    1) merge操作首先需要通過(guò)updatePendingMerges方法根據(jù)段的合并策略查詢需要合并的段。段合并策略分為很多種,本文僅介紹兩種Lucene默認(rèn)使用的段合并策略:TieredMergePolicyLogMergePolicy。


    • TieredMergePolicy:先通過(guò)OneMerge打分機(jī)制對(duì)IndexWriter提供的段集進(jìn)行排序,然后在排序后的段集中選取部分(可能不連續(xù))段來(lái)生成一個(gè)待合并段集,即非相鄰的段文件(Non-adjacent Segment)。

    • LogMergePolicy:定長(zhǎng)的合并方式,通過(guò)maxLevel、LEVEL_LOG_SPAN、levelBottom參數(shù)將連續(xù)的段分為不同的層級(jí),再通過(guò)mergeFactor從每個(gè)層級(jí)中選取段進(jìn)行合并。


    private synchronized boolean updatePendingMerges(MergePolicy mergePolicy, MergeTrigger trigger, int maxNumSegments)  throws IOException {
    final MergePolicy.MergeSpecification spec; // 查詢需要合并的段 if (maxNumSegments != UNBOUNDED_MAX_MERGE_SEGMENTS) { assert trigger == MergeTrigger.EXPLICIT || trigger == MergeTrigger.MERGE_FINISHED : "Expected EXPLICT or MERGE_FINISHED as trigger even with maxNumSegments set but was: " + trigger.name();
    spec = mergePolicy.findForcedMerges(segmentInfos, maxNumSegments, Collections.unmodifiableMap(segmentsToMerge), this); newMergesFound = spec != null; if (newMergesFound) { final int numMerges = spec.merges.size(); for(int i=0;i<numMerges;i++) { final MergePolicy.OneMerge merge = spec.merges.get(i); merge.maxNumSegments = maxNumSegments; } } } else { spec = mergePolicy.findMerges(trigger, segmentInfos, this); } // 注冊(cè)所有需要合并的段 newMergesFound = spec != null; if (newMergesFound) { final int numMerges = spec.merges.size(); for(int i=0;i<numMerges;i++) { registerMerge(spec.merges.get(i)); } } return newMergesFound;}


    2)通過(guò)ConcurrentMergeScheduler類中的merge方法創(chuàng)建用戶合并的線程MergeThread并啟動(dòng)。

    @Overridepublic synchronized void merge(IndexWriter writer, MergeTrigger trigger, boolean newMergesFound) throws IOException {  ……  while (true) {    ……    // 取出注冊(cè)的后選段    OneMerge merge = writer.getNextMerge();    boolean success = false;    try {      // 構(gòu)建用于合并的線程MergeThread       final MergeThread newMergeThread = getMergeThread(writer, merge);      mergeThreads.add(newMergeThread);
    updateIOThrottle(newMergeThread.merge, newMergeThread.rateLimiter);
    if (verbose()) { message(" launch new thread [" + newMergeThread.getName() + "]"); } // 啟用線程 newMergeThread.start(); updateMergeThreads();
    success = true; } finally { if (!success) { writer.mergeFinish(merge); } } }}


    3)通過(guò)doMerge方法執(zhí)行merge操作;

    public void merge(MergePolicy.OneMerge merge) throws IOException {  ……      try {        // 用于處理merge前緩存任務(wù)及新段相關(guān)信息生成        mergeInit(merge);        // 執(zhí)行段之間的merge操作        mergeMiddle(merge, mergePolicy);        mergeSuccess(merge);        success = true;      } catch (Throwable t) {        handleMergeException(t, merge);      } finally {        // merge完成后的收尾工作        mergeFinish(merge)      }……}


    五、Lucene搜索功能實(shí)現(xiàn)


    5.1 加載索引庫(kù)


    Lucene想要執(zhí)行搜索首先需要將索引段加載到內(nèi)存中,由于加載索引庫(kù)的操作非常耗時(shí),因此僅有當(dāng)索引庫(kù)產(chǎn)生變化時(shí)需要重新加載索引庫(kù)。



    加載索引庫(kù)分為加載段信息和加載文檔信息兩個(gè)部分:


    1)加載段信息:

    • 通過(guò)segments.gen文件獲取段中最大的generation,獲取段整體信息;

    • 讀取.si文件,構(gòu)造SegmentInfo對(duì)象,最后匯總得到SegmentInfos對(duì)象。


    2)加載文檔信息:

    • 讀取段信息,并從.fnm文件中獲取相應(yīng)的FieldInfo,構(gòu)造FieldInfos;

    • 打開(kāi)倒排表的相關(guān)文件和詞典文件;

    • 讀取索引的統(tǒng)計(jì)信息和相關(guān)norms信息;

    • 讀取文檔文件。



    5.2 封裝


    索引庫(kù)加載完成后需要IndexReader封裝進(jìn)IndexSearch,IndexSearch通過(guò)用戶構(gòu)造的Query語(yǔ)句和指定的Similarity文本相似度算法(默認(rèn)BM25)返回用戶需要的結(jié)果。通過(guò)IndexSearch.search方法實(shí)現(xiàn)搜索功能。


    搜索:Query包含多種實(shí)現(xiàn),包括BooleanQuery、PhraseQueryTermQuery、PrefixQuery等多種查詢方法,使用者可根據(jù)項(xiàng)目需求構(gòu)造查詢語(yǔ)句


    排序:IndexSearch除了通過(guò)Similarity計(jì)算文檔相關(guān)性分值排序外,也提供了BoostQuery的方式讓用戶指定關(guān)鍵詞分值,定制排序。Similarity相關(guān)性算法也包含很多種不同的相關(guān)性分值計(jì)算實(shí)現(xiàn),此處暫不做贅述,讀者有需要可自行網(wǎng)上查閱。


    六、總結(jié)


    Lucene作為全文索引工具包,為中小型項(xiàng)目提供了強(qiáng)大的全文檢索功能支持,但Lucene在使用的過(guò)程中存在諸多問(wèn)題:


    • 由于Lucene需要將檢索的索引庫(kù)通過(guò)IndexReader讀取索引信息并加載到內(nèi)存中以實(shí)現(xiàn)其檢索能力,當(dāng)索引量過(guò)大時(shí),會(huì)消耗服務(wù)部署機(jī)器的過(guò)多內(nèi)存。

    • 搜索實(shí)現(xiàn)比較復(fù)雜,需要對(duì)每個(gè)Field的索引、分詞、存儲(chǔ)等信息一一設(shè)置,使用復(fù)雜。

    • Lucene不支持集群。


    Lucene使用時(shí)存在諸多限制,使用起來(lái)也不那么方便,當(dāng)數(shù)據(jù)量增大時(shí)還是盡量選擇ElasticSearch等分布式搜索服務(wù)器作為搜索功能的實(shí)現(xiàn)方案。

    瀏覽 31
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    99青草国产精品视频无码一区 | 久久爆操| 国产一级a毛一级a看无码免费看 | 伊人成人在线 | 激情五月久久 |