現(xiàn)Word與PDF全文檢索:Elasticsearch 7.x文檔解析、IK分詞與高亮查詢實(shí)戰(zhàn))
1. 從一份合同翻找說起為什么Word和PDF的全文檢索值得單獨(dú)做前陣子幫一個(gè)做企業(yè)文檔管理的朋友處理一個(gè)需求他們內(nèi)部有幾千份Word合同和PDF技術(shù)方案平時(shí)找一份文件只能靠文件名可文件名往往起得極其隨意比如“最終版”“修改版2”“掃描件001”。真正想找“里面提到違約金比例的那份合同”只能一份份點(diǎn)開用眼睛掃效率低到讓人崩潰。這個(gè)場(chǎng)景其實(shí)非常普遍——只要一個(gè)團(tuán)隊(duì)積累的文檔超過幾百份文件名檢索就徹底失效了必須深入到文件內(nèi)部的文字內(nèi)容去搜。這就是全文內(nèi)容檢索要解決的問題把Word、PDF這類二進(jìn)制文檔里的文字“抽”出來存進(jìn)一個(gè)專門做搜索的引擎里之后用戶輸入任意關(guān)鍵詞系統(tǒng)就能定位到具體是哪份文件、甚至哪一頁、哪一段命中了。Elasticsearch 7.x 就是干這件事的成熟選擇它自帶分詞、倒排索引、高亮、打分排序省去了自己寫檢索算法的巨大工作量。而Java作為企業(yè)后端的主力語言用Java去對(duì)接Elasticsearch做文檔檢索是很多中后臺(tái)系統(tǒng)的標(biāo)準(zhǔn)技術(shù)棧。這篇文章適合誰看如果你手上有Java基礎(chǔ)、大致聽說過Elasticsearch但沒真正落地過文檔檢索或者你已經(jīng)能往ES里塞結(jié)構(gòu)化數(shù)據(jù)、但一碰到Word和PDF就卡在“怎么把內(nèi)容弄進(jìn)去”這一步那這篇內(nèi)容就是為你準(zhǔn)備的。我會(huì)把整個(gè)鏈路拆開講文檔怎么解析、內(nèi)容怎么清洗、索引怎么設(shè)計(jì)、查詢?cè)趺磳憽⒏吡猎趺捶祷匾约拔以趯?shí)際項(xiàng)目里踩過的那些坑。全程圍繞Elasticsearch 7.x的Java API展開不繞彎子。需要先說明一點(diǎn)全文檢索的效果七成取決于“內(nèi)容抽取”這一步干得干不干凈剩下三成才是ES的查詢調(diào)優(yōu)。很多人一上來就研究ES的查詢語法結(jié)果發(fā)現(xiàn)搜出來的東西驢唇不對(duì)馬嘴根子其實(shí)在解析環(huán)節(jié)。所以下面的內(nèi)容我會(huì)把解析部分講得比查詢部分更細(xì)。2. 文檔解析這一關(guān)Word和PDF的內(nèi)容抽取到底難在哪2.1 為什么不能直接把文件丟給Elasticsearch剛接觸這個(gè)需求的人常有一個(gè)直覺Elasticsearch不是能存文檔嗎那我把Word文件直接傳進(jìn)去不就行了這個(gè)想法會(huì)撞墻。Elasticsearch存儲(chǔ)和索引的基本單位是JSON文檔它的字段類型是文本、數(shù)值、日期這些它根本不認(rèn)識(shí).docx和.pdf的二進(jìn)制格式。你如果硬把二進(jìn)制字節(jié)塞進(jìn)一個(gè)字段ES只會(huì)把它當(dāng)成一堆亂碼字符分詞器面對(duì)亂碼什么也切不出來檢索自然無從談起。所以正確的鏈路是先用解析庫把Word/PDF里的純文本抽出來變成普通字符串再把這個(gè)字符串作為字段值寫進(jìn)ES。ES負(fù)責(zé)的是“對(duì)已經(jīng)變成文本的內(nèi)容做索引和檢索”而“把二進(jìn)制變成文本”這件事必須由Java側(cè)的解析庫來完成。理解了這個(gè)分工后面的技術(shù)選型就順理成章了。2.2 Word解析Apache POI的兩套API別用混Java里解析Word最主流的庫是Apache POI。這里有個(gè)新手極易踩的坑POI針對(duì)Word有兩套完全不同的API對(duì)應(yīng)兩種文件格式。HWPFDocument處理老的.doc格式Word 97-2003基于OLE2復(fù)合文檔結(jié)構(gòu)。XWPFDocument處理新的.docx格式Word 2007基于OOXML的ZIP包結(jié)構(gòu)。這兩個(gè)類不通用。你拿XWPFDocument去打開一個(gè).doc文件會(huì)直接拋異常反過來也一樣。實(shí)際項(xiàng)目里兩種格式都可能存在所以穩(wěn)妥的做法是先判斷文件擴(kuò)展名或魔數(shù)再?zèng)Q定用哪個(gè)解析器。我一般會(huì)封裝一個(gè)統(tǒng)一的入口方法內(nèi)部做分發(fā)對(duì)外只暴露“給我一個(gè)文件還我一段文本”。用XWPFDocument抽取段落文本的核心邏輯很直接遍歷getParagraphs()拿到每個(gè)段落調(diào)getText()。但這里有個(gè)細(xì)節(jié)——表格里的文字不在段落列表里。很多合同、報(bào)價(jià)單的關(guān)鍵信息恰恰在表格中如果你只抽段落表格內(nèi)容會(huì)全部丟失。正確做法是同時(shí)遍歷getTables()對(duì)每個(gè)表格再遍歷行和單元格把單元格里的段落文本也拼進(jìn)來。這個(gè)坑我在第一個(gè)項(xiàng)目里就踩了導(dǎo)致用戶搜合同金額怎么都搜不到排查半天才發(fā)現(xiàn)金額在表格里。2.3 PDF解析為什么我最終選了PDFBox而不是別的PDF的解析比Word更麻煩因?yàn)镻DF本質(zhì)上是一種“排版描述語言”它記錄的是“在坐標(biāo)(x,y)畫這幾個(gè)字”而不是“這里有一段話”。所以從PDF里還原出有意義的文本流本身就是個(gè)有損的過程。Java里常見的PDF文本抽取庫有PDFBox和iText。iText功能強(qiáng)但許可證對(duì)商業(yè)使用有約束需要仔細(xì)評(píng)估PDFBox是Apache基金會(huì)的開源項(xiàng)目許可證寬松抽取純文本夠用所以企業(yè)項(xiàng)目里我用PDFBox居多。核心類是PDDocument和PDFTextStrippertry (PDDocument document PDDocument.load(new File(filePath))) { PDFTextStripper stripper new PDFTextStripper(); stripper.setSortByPosition(true); // 關(guān)鍵按坐標(biāo)排序 String text stripper.getText(document); }setSortByPosition(true)這行非常關(guān)鍵。PDF里的文字對(duì)象在文件中的存儲(chǔ)順序和它在頁面上視覺呈現(xiàn)的順序經(jīng)常不一致。如果不按坐標(biāo)排序抽出來的文本可能是錯(cuò)亂的一句話被拆得七零八落檢索時(shí)自然匹配不上。開啟這個(gè)選項(xiàng)后PDFBox會(huì)按文字在頁面上的實(shí)際位置從上到下、從左到右重新排列抽出來的文本可讀性會(huì)好很多。2.4 掃描件PDF一個(gè)必須提前和需求方說清楚的邊界有一類PDF是掃描件——本質(zhì)上是圖片里面根本沒有文字對(duì)象只有一張張掃描圖像。這種文件用PDFBox抽出來是空字符串因?yàn)榇_實(shí)沒有文字可抽。要處理它必須上OCR光學(xué)字符識(shí)別比如Tesseract配合圖像預(yù)處理。但OCR是另一個(gè)技術(shù)棧識(shí)別率受掃描質(zhì)量影響很大而且速度慢。我的經(jīng)驗(yàn)是在項(xiàng)目啟動(dòng)階段就要和需求方確認(rèn)文檔庫里有沒有掃描件、占比多少。如果有且占比不小要么單獨(dú)做OCR流程要么明確告知這部分文件不支持全文檢索。最怕的是悶頭做完上線后用戶拿一份掃描件搜不到回頭質(zhì)疑系統(tǒng)有bug。提前把邊界講清楚比事后解釋省事得多。3. 把文本變成可檢索的索引字段映射與中文分詞的取舍3.1 索引Mapping設(shè)計(jì)哪些字段該建、該用什么類型內(nèi)容抽出來之后下一步是設(shè)計(jì)索引的Mapping。一個(gè)實(shí)用的文檔檢索索引我通常會(huì)放這幾類字段字段名類型作用是否分詞fileNametext keyword子字段文件名既要能全文搜也要能精確匹配text分詞keyword不分詞contenttext文檔正文檢索的主戰(zhàn)場(chǎng)分詞fileTypekeyword文件類型doc/docx/pdf不分詞filePathkeyword文件存儲(chǔ)路徑不分詞fileSizelong文件大小可用于過濾不分詞createTimedate入庫時(shí)間用于排序不分詞這里有個(gè)高頻考點(diǎn)fileName為什么要同時(shí)建text和keyword因?yàn)檫@兩種查詢需求都存在。用戶可能想“模糊搜文件名里帶‘合同’的”這需要text分詞也可能想“精確找名叫‘2024年度采購(gòu)合同.docx’的那份”這需要keyword精確匹配。ES的fields多字段特性正好滿足一個(gè)字段值同時(shí)以兩種方式索引查詢時(shí)按需選用。content字段是重點(diǎn)它承載全文檢索。這里要注意一個(gè)默認(rèn)值陷阱ES的text字段默認(rèn)對(duì)單個(gè)字段的索引內(nèi)容有長(zhǎng)度限制雖然一般文檔不會(huì)超但如果你把整本書的內(nèi)容塞進(jìn)一個(gè)字段可能觸發(fā)max相關(guān)的問題。對(duì)于超長(zhǎng)文檔可以考慮按段落或按頁拆成多個(gè)子文檔或者用nested結(jié)構(gòu)不過這會(huì)增加復(fù)雜度普通企業(yè)文檔場(chǎng)景一般不需要。3.2 中文分詞默認(rèn)分詞器為什么不能用這是中文全文檢索里最容易被忽視、后果又最嚴(yán)重的一環(huán)。Elasticsearch默認(rèn)的standard分詞器是按字符切分的對(duì)英文沒問題對(duì)中文就是災(zāi)難——它會(huì)把“采購(gòu)合同管理”切成“采”“購(gòu)”“合”“同”“管”“理”一個(gè)個(gè)單字。這樣索引之后你搜“合同”能匹配到但搜“采購(gòu)合同”這種詞組時(shí)因?yàn)樗饕锔緵]有“采購(gòu)合同”這個(gè)詞條匹配效果會(huì)大打折扣而且單字索引會(huì)讓索引體積暴漲、查詢變慢。正確做法是安裝中文分詞插件。業(yè)界最常用的是IK分詞器elasticsearch-analysis-ik它提供兩種模式ik_smart粗粒度切出的詞少而整適合大多數(shù)檢索場(chǎng)景。ik_max_word細(xì)粒度會(huì)把一個(gè)詞的所有可能組合都切出來召回率高但索引更大。我的實(shí)踐是建索引時(shí)用ik_max_word盡量多切保證召回查詢時(shí)用ik_smart按用戶意圖切保證精度。這種“索引細(xì)、查詢粗”的搭配是IK的經(jīng)典用法能兼顧召回和準(zhǔn)確。Mapping里這樣寫content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }注意IK分詞器是第三方插件需要單獨(dú)下載對(duì)應(yīng)ES版本的安裝包解壓到ES的plugins目錄后重啟節(jié)點(diǎn)。版本號(hào)必須和ES主版本嚴(yán)格對(duì)應(yīng)7.x的插件裝到7.x的ES上否則節(jié)點(diǎn)起不來。3.3 自定義詞典讓專業(yè)術(shù)語不被切碎IK自帶的主詞典是通用詞庫遇到行業(yè)術(shù)語經(jīng)常切錯(cuò)。比如“履約保證金”可能被切成“履約”“保證金”如果用戶習(xí)慣整體搜“履約保證金”就可能匹配不上。解決辦法是配置自定義詞典在IK的配置目錄下建一個(gè)custom.dic文件每行寫一個(gè)詞然后在IKAnalyzer.cfg.xml里引用它重啟后生效。自定義詞典的維護(hù)是個(gè)持續(xù)活兒。我的建議是上線初期先跑一批真實(shí)查詢?nèi)罩究茨男┰~被切錯(cuò)了再逐步往詞典里加。不要一開始就想窮舉所有術(shù)語那樣既累又容易漏。4. Java客戶端對(duì)接從單條入庫到批量索引的完整實(shí)現(xiàn)4.1 客戶端選型RestHighLevelClient還是新版的Java API ClientElasticsearch 7.x時(shí)代Java側(cè)官方主推的客戶端是RestHighLevelClient它基于HTTP REST協(xié)議通過RestClient做底層通信。7.x后期官方推出了新的Elasticsearch Java API Client但7.x版本上RestHighLevelClient依然是文檔最全、社區(qū)案例最多的選擇所以本文以它為主。引入依賴Mavendependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version7.17.0/version /dependency版本號(hào)要和你的ES服務(wù)端版本保持一致7.x內(nèi)部小版本盡量對(duì)齊避免協(xié)議不兼容。初始化客戶端RestHighLevelClient client new RestHighLevelClient( RestClient.builder(new HttpHost(es-host, 9200, http)) );生產(chǎn)環(huán)境建議配置多個(gè)節(jié)點(diǎn)地址客戶端會(huì)自動(dòng)做負(fù)載均衡和故障轉(zhuǎn)移。用完記得client.close()釋放連接。4.2 單條文檔入庫IndexRequest的正確姿勢(shì)把一份解析好的文檔寫進(jìn)ES用IndexRequestIndexRequest request new IndexRequest(doc_index); request.id(docId); // 用文檔唯一ID避免重復(fù)入庫 MapString, Object doc new HashMap(); doc.put(fileName, fileName); doc.put(content, content); doc.put(fileType, fileType); doc.put(createTime, new Date()); request.source(doc, XContentType.JSON); IndexResponse response client.index(request, RequestOptions.DEFAULT);這里有個(gè)設(shè)計(jì)決策值得說用文檔自身的唯一標(biāo)識(shí)比如文件路徑的哈?;驍?shù)據(jù)庫主鍵作為ES文檔id。這樣做的好處是重復(fù)入庫時(shí)ES會(huì)覆蓋而不是新增天然冪等。如果讓ES自動(dòng)生成id同一份文件解析兩次就會(huì)產(chǎn)生兩條重復(fù)記錄檢索結(jié)果里出現(xiàn)重復(fù)項(xiàng)體驗(yàn)很差。4.3 批量入庫BulkRequest才是生產(chǎn)環(huán)境的答案單條入庫適合調(diào)試生產(chǎn)環(huán)境幾千上萬份文檔必須用BulkRequest批量提交。批量的大小有講究太小則網(wǎng)絡(luò)往返次數(shù)多太大則單次請(qǐng)求體過大容易超時(shí)。我的經(jīng)驗(yàn)值是每批500到1000條或者累計(jì)請(qǐng)求體控制在5到10MB左右哪個(gè)先到就提交。BulkRequest bulkRequest new BulkRequest(); for (Document doc : docList) { IndexRequest req new IndexRequest(doc_index).id(doc.getId()); req.source(buildMap(doc), XContentType.JSON); bulkRequest.add(req); if (bulkRequest.numberOfActions() 500) { client.bulk(bulkRequest, RequestOptions.DEFAULT); bulkRequest new BulkRequest(); } } if (bulkRequest.numberOfActions() 0) { client.bulk(bulkRequest, RequestOptions.DEFAULT); }批量提交后一定要檢查響應(yīng)里的失敗項(xiàng)。BulkResponse.hasFailures()為true時(shí)要遍歷BulkItemResponse找出哪些文檔失敗了、失敗原因是什么。我見過有人批量寫完不看響應(yīng)結(jié)果部分文檔因?yàn)樽侄晤愋筒黄ヅ浔混o默拒絕檢索時(shí)才發(fā)現(xiàn)少了一批文件。4.4 解析與入庫的解耦別讓一個(gè)壞文件拖垮整批實(shí)際文檔庫里總有那么幾份“問題文件”——損壞的PDF、加密的Word、格式異常的docx。如果解析邏輯和入庫邏輯寫在一個(gè)循環(huán)里一份文件解析拋異常整個(gè)批次就中斷了。正確做法是把解析和入庫解耦解析階段對(duì)每份文件單獨(dú)try-catch解析失敗的記錄到日志和一張“失敗清單”表里成功的才進(jìn)入入庫隊(duì)列。這樣一份壞文件不會(huì)影響其他幾百份正常文件。更進(jìn)一步解析本身是CPU密集型操作入庫是IO密集型操作兩者可以用不同的線程池并行。解析線程池負(fù)責(zé)把文件變成文本入庫線程池負(fù)責(zé)批量寫ES中間用阻塞隊(duì)列銜接。這個(gè)架構(gòu)在文檔量大的時(shí)候能顯著提升吞吐。5. 查詢與高亮讓用戶一眼看到命中的那句話5.1 多字段查詢文件名和正文一起搜用戶輸入一個(gè)關(guān)鍵詞通常希望文件名和正文都參與匹配。用MultiMatchQuery可以一次搜多個(gè)字段MultiMatchQueryBuilder mmq QueryBuilders.multiMatchQuery(keyword, fileName, content) .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) .field(fileName, 2.0f); // 文件名權(quán)重更高BEST_FIELDS策略會(huì)取各字段中得分最高的作為最終得分適合字段之間內(nèi)容差異大的場(chǎng)景。給fileName加2倍權(quán)重是因?yàn)槲募型日拿懈嚓P(guān)——用戶搜“采購(gòu)合同”文件名就叫這個(gè)的大概率比正文里順帶提一句的更符合意圖。5.2 高亮把命中的詞用標(biāo)簽包起來高亮是全文檢索體驗(yàn)的關(guān)鍵。用戶不想在一大段文字里自己找關(guān)鍵詞而是希望系統(tǒng)直接把命中的片段標(biāo)出來。ES的高亮功能通過HighlightBuilder實(shí)現(xiàn)HighlightBuilder highlightBuilder new HighlightBuilder(); highlightBuilder.field(content); highlightBuilder.preTags(em classhl); highlightBuilder.postTags(/em); highlightBuilder.fragmentSize(150); // 每個(gè)片段150字符 highlightBuilder.numberOfFragments(3); // 最多返回3個(gè)片段fragmentSize控制返回的片段長(zhǎng)度太小則上下文不足太大則前端展示不下。150字符左右是我常用的值能覆蓋一句話的上下文。numberOfFragments控制返回幾個(gè)片段一般3個(gè)足夠多了前端也展示不過來。拿到高亮結(jié)果后前端把em標(biāo)簽渲染成高亮樣式即可。注意高亮片段里的HTML標(biāo)簽要做轉(zhuǎn)義處理防止文檔內(nèi)容里本身含有尖括號(hào)導(dǎo)致XSS問題。5.3 分頁與排序別用fromsize做深分頁普通分頁用from和size沒問題SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); sourceBuilder.from(pageNum * pageSize); sourceBuilder.size(pageSize);但要注意from size的深分頁比如翻到第1000頁性能很差因?yàn)镋S需要在每個(gè)分片上取出fromsize條再協(xié)調(diào)節(jié)點(diǎn)合并排序。如果確實(shí)有深分頁需求應(yīng)該用search_after配合排序字段來做游標(biāo)式翻頁。普通企業(yè)文檔檢索用戶翻到幾十頁就頂天了fromsize夠用但心里要有這根弦。排序方面默認(rèn)按相關(guān)性得分_score降序。如果用戶想按時(shí)間排可以加一個(gè)createTime的排序字段。注意一旦指定了自定義排序_score就不再是默認(rèn)排序依據(jù)如果還想保留相關(guān)性排序要把_score顯式加進(jìn)排序列表。6. 那些文檔里不寫、但上線后一定會(huì)遇到的問題6.1 大文件解析的內(nèi)存溢出一份幾百頁的PDF用PDFBox一次性load進(jìn)內(nèi)存再抽取很容易把JVM堆撐爆尤其是并發(fā)解析多份大文件時(shí)。解決辦法有兩個(gè)方向一是限制單次解析的文件大小超過閾值比如50MB的走單獨(dú)的低并發(fā)通道二是用PDFBox的流式讀取邊讀邊抽不把整個(gè)文檔對(duì)象常駐內(nèi)存。Word的XWPFDocument同理超大docx也要小心。6.2 編碼問題導(dǎo)致的亂碼有些老Word文檔或PDF內(nèi)部編碼不規(guī)范抽出來的中文是亂碼。這種情況往往不是解析庫的鍋而是文檔本身的字符編碼就有問題。處理辦法是在抽取后做一次編碼檢測(cè)和修正或者對(duì)明顯亂碼的內(nèi)容打標(biāo)記、跳過入庫。指望100%還原所有文檔的編碼是不現(xiàn)實(shí)的接受一定比例的失敗率把失敗清單暴露給運(yùn)維比追求完美更實(shí)際。6.3 索引膨脹與段合并文檔內(nèi)容多、分詞細(xì)索引體積會(huì)漲得很快。ES底層是Lucene每次寫入會(huì)產(chǎn)生新的段segment段太多會(huì)影響查詢性能。ES會(huì)自動(dòng)做段合并但在大批量導(dǎo)入期間可以臨時(shí)調(diào)大index.refresh_interval比如從默認(rèn)1秒改成30秒減少刷新次數(shù)提升導(dǎo)入速度。導(dǎo)入完成后再改回來。這個(gè)參數(shù)調(diào)整在批量導(dǎo)入場(chǎng)景下效果很明顯。6.4 增量更新文件改了怎么辦文檔庫不是一成不變的文件會(huì)被修改、新增、刪除。全量重建索引代價(jià)太大必須做增量。我的做法是用文件的最后修改時(shí)間或內(nèi)容哈希作為版本標(biāo)識(shí)定時(shí)掃描文檔目錄只處理有變化的文件。內(nèi)容哈希變了才重新解析入庫沒變的跳過。刪除的文件則根據(jù)路徑去ES里刪對(duì)應(yīng)id的文檔。這套機(jī)制配合定時(shí)任務(wù)能做到準(zhǔn)實(shí)時(shí)同步。6.5 檢索結(jié)果的相關(guān)性調(diào)優(yōu)上線后經(jīng)常收到“搜出來的不是我想要的”這類反饋。相關(guān)性調(diào)優(yōu)是個(gè)細(xì)活常見手段包括調(diào)整字段權(quán)重boost、用function_score結(jié)合時(shí)間衰減新文檔得分略高、對(duì)完全匹配的短語用match_phrase提升精度。我的建議是先把基礎(chǔ)檢索跑通收集一批真實(shí)的“搜不準(zhǔn)”案例再針對(duì)性調(diào)不要一上來就堆一堆復(fù)雜的打分函數(shù)那樣反而難以維護(hù)。7. 我在實(shí)際項(xiàng)目里沉淀下來的幾條經(jīng)驗(yàn)做這類文檔檢索系統(tǒng)技術(shù)選型其實(shí)不復(fù)雜難的是把工程細(xì)節(jié)處理干凈。我最大的體會(huì)是解析環(huán)節(jié)的健壯性決定了整個(gè)系統(tǒng)的可用性。ES再?gòu)?qiáng)喂進(jìn)去的是垃圾文本搜出來的也是垃圾。所以我在解析層投入的精力往往比查詢層還多——異常捕獲、失敗重試、失敗清單、格式兼容這些“臟活”才是項(xiàng)目能不能穩(wěn)定跑起來的關(guān)鍵。另一個(gè)體會(huì)是關(guān)于中文分詞。很多人第一次做中文檢索搜出來的結(jié)果莫名其妙排查半天才發(fā)現(xiàn)是分詞器沒配對(duì)。記住那個(gè)口訣索引用細(xì)粒度、查詢用粗粒度專業(yè)術(shù)語進(jìn)自定義詞典。這三條做到中文檢索的準(zhǔn)確率能上一個(gè)臺(tái)階。最后說個(gè)容易被忽略的點(diǎn)給檢索結(jié)果加一個(gè)“為什么這條會(huì)命中”的反饋。比如高亮片段、命中字段提示。用戶看到命中的上下文才會(huì)信任這個(gè)系統(tǒng)。如果只返回一個(gè)文件名列表用戶點(diǎn)進(jìn)去發(fā)現(xiàn)關(guān)鍵詞在很不起眼的地方會(huì)覺得系統(tǒng)“亂搜”。把命中依據(jù)透明化體驗(yàn)會(huì)好很多。這套方案我已經(jīng)在幾個(gè)內(nèi)部文檔系統(tǒng)里落地過從幾千份到幾萬份文檔的規(guī)模都跑得動(dòng)。核心鏈路就是解析抽文本、清洗去噪、IK分詞建索引、多字段查詢加高亮。把這條鏈路走通剩下的就是根據(jù)具體業(yè)務(wù)做調(diào)優(yōu)了。如果你正準(zhǔn)備做類似的東西建議先用幾十份真實(shí)文檔跑通全流程再考慮規(guī)模和性能的事別一上來就設(shè)計(jì)過度復(fù)雜的架構(gòu)。