<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 14474字,需瀏覽 29分鐘

           ·

          2021-07-17 10:30


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


          一、Lucene簡介


          1.1 Lucene是什么?


          • Lucene是Apache基金會jakarta項目組的一個子項目;

          • Lucene是一個開放源碼的全文檢索引擎工具包,提供了完整的查詢引擎和索引引擎,部分語種文本分析引擎

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

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


          1.2 Lucene的使用場景


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


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


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

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

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


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


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


          索引的生成分為兩個部分:


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

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

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


          2. 搜索階段:

          • 用戶通過查詢語句向Lucene發(fā)送查詢請求;

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

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


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



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


          3.1 正向索引


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



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


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

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

          • 文檔:Lucene會將文檔寫入段中,一個段中包含多個文檔。

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

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


          3.2 倒排索引


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


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



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


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


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

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

          • 通過將前綴和后綴分開存儲的方式,減少了存放詞所需的空間;

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

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


          具體存儲方式如圖3所示:



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

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

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

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


          3.3 索引查詢及文檔搜索過程


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

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

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

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

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


          文件格式如圖4所示:



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


          四、Lucene的增刪改操作


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

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


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


          4.1. 文檔的新增


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


          ThreadState getAndLock(Thread requestingThread, DocumentsWriter documentsWriter) {  ThreadState threadState = null;  synchronized (this) {    if (freeList.isEmpty()) {      // 如果不存在已創(chuàng)建的空閑ThreadState,則新創(chuàng)建一個      return newThreadState();    } else {      // freeList后進(jìn)先出,僅使用有限的ThreadState操作索引      threadState = freeList.remove(freeList.size()-1);
          // 優(yōu)先使用已經(jīng)初始化過DocumentWriterPerThread的ThreadState,并將其與當(dāng)前 // ThreadState換位,將其移到隊尾優(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來處理文檔中的每個域,processField方法是索引鏈的核心執(zhí)行邏輯。通過用戶對每個域設(shè)置的不同的FieldType進(jìn)行相應(yīng)的索引、分詞、存儲等操作。FieldType中比較重要的是indexOptions:


          • NONE:域信息不會寫入倒排表,索引階段無法通過該域名進(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);}
          // 存儲該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(通過文檔查詢文檔下包含了哪些詞)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有兩個重要的派生類Tokenizer和TokenFilter,其中Tokenizer用于通過java.io.Reader類讀取字符,產(chǎn)生Token流,然后通過任意數(shù)量的TokenFilter來處理這些輸入的Token流,具體源碼如下:


          // invert:對Field進(jìn)行分詞處理首先需要將Field轉(zhuǎn)化為TokenStreamtry (TokenStream stream = tokenStream = field.tokenStream(docState.analyzer, tokenStream))// TokenStream在不同分詞器下實現(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) { // 通過復(fù)用策略,如果TokenStreamComponents中已經(jīng)存在Component則復(fù)用。 TokenStreamComponents components = reuseStrategy.getReusableComponents(this, fieldName); final Reader r = initReader(fieldName, reader); // 如果Component不存在,則根據(jù)分詞器創(chuàng)建對應(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中配置的分詞器,通過策略模式返回分詞器對應(yīng)的分詞組件,針對不同的語言及不同的分詞需求,分詞組件存在很多不同的實現(xiàn)。


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

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

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


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

          // 標(biāo)準(zhǔn)分詞器創(chuàng)建Component過程,涵蓋了標(biāo)準(zhǔn)分詞處理器、Term轉(zhuǎn)化小寫、常用詞過濾三個功能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之后通過TokenStream中的incrementToken方法分析并獲取屬性,再通過TermsHashPerField下的add方法構(gòu)建倒排表,最終將Field的相關(guān)數(shù)據(jù)存儲到類型為FreqProxPostingsArray的freqProxPostingsArray中,以及TermVectorsPostingsArray的termVectorsPostingsArray中,構(gòu)成倒排表;


          // 以LowerCaseFilter為例,通過其下的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下文檔的刪除,首先將要刪除的Term或Query添加到刪除隊列中;

          synchronized long deleteTerms(final Term... terms) throws IOException {  // TODO why is this synchronized?  final DocumentsWriterDeleteQueue deleteQueue = this.deleteQueue;  // 文檔刪除操作是將刪除的詞信息添加到刪除隊列中,根據(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 文檔的更新


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


          4.4 索引Flush


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


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

          • ramBufferSizeMB:文檔內(nèi)容達(dá)到限定值時觸發(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)存信息寫入索引庫。



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


          4.5 索引段Merge


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


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


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


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

          • LogMergePolicy:定長的合并方式,通過maxLevel、LEVEL_LOG_SPAN、levelBottom參數(shù)將連續(xù)的段分為不同的層級,再通過mergeFactor從每個層級中選取段進(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); } // 注冊所有需要合并的段 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)通過ConcurrentMergeScheduler類中的merge方法創(chuàng)建用戶合并的線程MergeThread并啟動。

          @Overridepublic synchronized void merge(IndexWriter writer, MergeTrigger trigger, boolean newMergesFound) throws IOException {  ……  while (true) {    ……    // 取出注冊的后選段    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)通過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搜索功能實現(xiàn)


          5.1 加載索引庫


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



          加載索引庫分為加載段信息和加載文檔信息兩個部分:


          1)加載段信息:

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

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


          2)加載文檔信息:

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

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

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

          • 讀取文檔文件。



          5.2 封裝


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


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


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


          六、總結(jié)


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


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

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

          • Lucene不支持集群。


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

          瀏覽 31
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  超碰2018 | 亚洲视频在线观看中文字幕 | 黑人巨大两根挤进一个 | 国产曹逼性感美女视频 | 久久色无码视频 |