1. 程式人生 > 實用技巧 >全文檢索技術

全文檢索技術

Lucene

1課程計劃

1、什麼是全文檢索,如何實現全文檢索

2、Lucene實現全文檢索的流程

a)建立索引

b)查詢索引

3、配置開發環境

4、入門程式

5、分析器的分析過程

a)測試分析器的分詞效果

b)第三方中文分析器

6、索引庫維護

a)新增文件

b)刪除文件

c)修改文件

7、索引庫查詢

a)使用Query子類查詢

b)使用QueryParser查詢

2什麼是全文檢索

2.1資料分類

我們生活中的資料總體分為兩種:結構化資料和非結構化資料。

結構化資料:指具有固定格式或有限長度的資料,如資料庫,元資料等。

非結構化資料:指不定長或無固定格式的資料,如郵件,word文件等磁碟上的檔案

2.2結構化資料搜尋

常見的結構化資料也就是資料庫中的資料。在資料庫中搜索很容易實現,通常都是使用sql語句進行查詢,而且能很快的得到查詢結果。

為什麼資料庫搜尋很容易?

因為資料庫中的資料儲存是有規律的,有行有列而且資料格式、資料長度都是固定的。

2.3非結構化資料查詢方法

(1)順序掃描法(Serial Scanning)

所謂順序掃描,比如要找內容包含某一個字串的檔案,就是一個文件一個文件的看,對於每一個文件,從頭看到尾,如果此文件包含此字串,則此文件為我們要找的檔案,接著看下一個檔案,直到掃描完所有的檔案。如利用windows的搜尋也可以搜尋檔案內容,只是相當的慢。

(2)全文檢索(Full-text Search)

將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。這部分從非結構化資料中提取出的然後重新組織的資訊,我們稱之索引

例如:字典。字典的拼音表和部首檢字表就相當於字典的索引,對每一個字的解釋是非結構化的,如果字典沒有音節表和部首檢字表,在茫茫辭海中找一個字只能順序掃描。然而字的某些資訊可以提取出來進行結構化處理,比如讀音,就比較結構化,分聲母和韻母,分別只有幾種可以一一列舉,於是將讀音拿出來按一定的順序排列,每一項讀音都指向此字的詳細解釋的頁數。我們搜尋時按結構化的拼音搜到讀音,然後按其指向的頁數,便可找到我們的非結構化資料——也即對字的解釋。

這種先建立索引,再對索引進行搜尋的過程就叫全文檢索(Full-text Search)。

雖然建立索引的過程也是非常耗時的,但是索引一旦建立就可以多次使用,全文檢索主要處理的是查詢,所以耗時間建立索引是值得的。

2.4如何實現全文檢索

可以使用Lucene實現全文檢索。Lucene是apache下的一個開放原始碼的全文檢索引擎工具包。提供了完整的查詢引擎和索引引擎,部分文字分析引擎。Lucene的目的是為軟體開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能。

2.5全文檢索的應用場景

對於資料量大、資料結構不固定的資料可採用全文檢索方式搜尋,比如百度、Google等搜尋引擎、論壇站內搜尋、電商網站站內搜尋等。

3Lucene實現全文檢索的流程

3.1索引和搜尋流程圖

1、綠色表示索引過程,對要搜尋的原始內容進行索引構建一個索引庫,索引過程包括:

確定原始內容即要搜尋的內容à採集文件à建立文件à分析文件à索引文件

2、紅色表示搜尋過程,從索引庫中搜索內容,搜尋過程包括:

使用者通過搜尋介面à建立查詢à執行搜尋,從索引庫搜尋à渲染搜尋結果

3.2建立索引

對文件索引的過程,將使用者要搜尋的文件內容進行索引,索引儲存在索引庫(index)中。

這裡我們要搜尋的文件是磁碟上的文字檔案,根據案例描述:凡是檔名或檔案內容包括關鍵字的檔案都要找出來,這裡要對檔名和檔案內容建立索引。

3.2.1獲得原始文件

原始文件是指要索引和搜尋的內容。原始內容包括網際網路上的網頁、資料庫中的資料、磁碟上的檔案等。

本案例中的原始內容就是磁碟上的檔案,如下圖:

從網際網路上、資料庫、檔案系統中等獲取需要搜尋的原始資訊,這個過程就是資訊採集,資訊採集的目的是為了對原始內容進行索引。

在Internet上採集資訊的軟體通常稱為爬蟲或蜘蛛,也稱為網路機器人,爬蟲訪問網際網路上的每一個網頁,將獲取到的網頁內容儲存起來。

本案例我們要獲取磁碟上檔案的內容,可以通過檔案流來讀取文字檔案的內容,對於pdf、doc、xls等檔案可通過第三方提供的解析工具讀取檔案內容,比如Apache POI讀取doc和xls的檔案內容。

3.2.2建立文件物件

獲取原始內容的目的是為了索引,在索引前需要將原始內容建立成文件(Document),文件中包括一個一個的域(Field),域中儲存內容。

這裡我們可以將磁碟上的一個檔案當成一個document,Document中包括一些Field(file_name檔名稱、file_path檔案路徑、file_size檔案大小、file_content檔案內容),如下圖:

注意:每個Document可以有多個Field,不同的Document可以有不同的Field,同一個Document可以有相同的Field(域名和域值都相同)

每個文件都有一個唯一的編號,就是文件id。

3.2.3分析文件

將原始內容建立為包含域(Field)的文件(document),需要再對域中的內容進行分析,分析的過程是經過對原始文件提取單詞、將字母轉為小寫、去除標點符號、去除停用詞等過程生成最終的語彙單元,可以將語彙單元理解為一個一個的單詞。

比如下邊的文件經過分析如下:

原文件內容:

Lucene is a Java full-text search engine. Lucene is not a complete

application, but rather a code library and API that can easily be used

to add search capabilities to applications.

分析後得到的語彙單元:

lucene、java、full、search、engine。。。。

每個單詞叫做一個Term,不同的域中拆分出來的相同的單詞是不同的term。term中包含兩部分一部分是文件的域名,另一部分是單詞的內容。

例如:檔名中包含apache和檔案內容中包含的apache是不同的term。

3.2.4建立索引

對所有文件分析得出的語彙單元進行索引,索引的目的是為了搜尋,最終要實現只搜尋被索引的語彙單元從而找到Document(文件)。

注意:建立索引是對語彙單元索引,通過詞語找文件,這種索引的結構叫倒排索引結構

傳統方法是根據檔案找到該檔案的內容,在檔案內容中匹配搜尋關鍵字,這種方法是順序掃描方法,資料量大、搜尋慢。

倒排索引結構是根據內容(詞語)找文件,如下圖:

倒排索引結構也叫反向索引結構,包括索引和文件兩部分,索引即詞彙表,它的規模較小,而文件集合較大。

3.3查詢索引

查詢索引也是搜尋的過程。搜尋就是使用者輸入關鍵字,從索引(index)中進行搜尋的過程。根據關鍵字搜尋索引,根據索引找到對應的文件,從而找到要搜尋的內容(這裡指磁碟上的檔案)。

3.3.1使用者查詢介面

全文檢索系統提供使用者搜尋的介面供使用者提交搜尋的關鍵字,搜尋完成展示搜尋結果。

比如:

Lucene不提供製作使用者搜尋介面的功能,需要根據自己的需求開發搜尋介面。

3.3.2建立查詢

使用者輸入查詢關鍵字執行搜尋之前需要先構建一個查詢物件,查詢物件中可以指定查詢要搜尋的Field文件域、查詢關鍵字等,查詢物件會生成具體的查詢語法,

例如:語法“fileName:lucene”表示要搜尋Field域的內容為“lucene”的文件

3.3.3執行查詢

搜尋索引過程:

根據查詢語法在倒排索引詞典表中分別找出對應搜尋詞的索引,從而找到索引所連結的文件連結串列。

比如搜尋語法為“fileName:lucene”表示搜尋出fileName域中包含Lucene的文件。

搜尋過程就是在索引上查詢域為fileName,並且關鍵字為Lucene的term,並根據term找到文件id列表。

3.3.4渲染結果

以一個友好的介面將查詢結果展示給使用者,使用者根據搜尋結果找自己想要的資訊,為了幫助使用者很快找到自己的結果,提供了很多展示的效果,比如搜尋結果中將關鍵字高亮顯示,百度提供的快照等。

4配置開發環境

4.1Lucene下載

Lucene是開發全文檢索功能的工具包,從官方網站下載lucene-7.4.0,並解壓。

官方網站:http://lucene.apache.org/

版本:lucene-7.4.0

Jdk要求:1.8以上

4.2使用的jar包

lucene-core-7.4.0.jar

lucene-analyzers-common-7.4.0.jar

5入門程式

5.1需求

實現一個檔案的搜尋功能,通過關鍵字搜尋檔案,凡是檔名或檔案內容包括關鍵字的檔案都需要找出來。還可以根據中文詞語進行查詢,並且需要支援多個條件查詢。

本案例中的原始內容就是磁碟上的檔案,如下圖:

5.2建立索引

5.2.1實現步驟

第一步:建立一個java工程,並匯入jar包。

第二步:建立一個indexwriter物件。

1)指定索引庫的存放位置Directory物件

2)指定一個IndexWriterConfig物件。

第二步:建立document物件。

第三步:建立field物件,將field新增到document物件中。

第四步:使用indexwriter物件將document物件寫入索引庫,此過程進行索引建立。並將索引和document物件寫入索引庫。

第五步:關閉IndexWriter物件。

5.2.2程式碼實現

//建立索引
@Test
public void createIndex() throws Exception { 
  //指定索引庫存放的路徑 
  //D:\temp\index   
  Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());   
  //索引庫還可以存放到記憶體中   
  //Directory directory = new RAMDirectory();    
  //建立indexwriterCofig物件   
  IndexWriterConfig config = new IndexWriterConfig();    
  //建立indexwriter物件    
  IndexWriter indexWriter = new IndexWriter(directory, config);   
  //原始文件的路徑    
  File dir = new File("D:\\temp\\searchsource");   
  for (File f : dir.listFiles()) {       
    //檔名        
    String fileName = f.getName();       
    //檔案內容      
    String fileContent = FileUtils.readFileToString(f);     
    //檔案路徑       
    String filePath = f.getPath();       
    //檔案的大小     
    long fileSize  = FileUtils.sizeOf(f);   
    //建立檔名域       
    //第一個引數:域的名稱    
    //第二個引數:域的內容     
    //第三個引數:是否儲存      
    Field fileNameField = new TextField("filename", fileName, Field.Store.YES);  
    //檔案內容域      
    Field fileContentField = new TextField("content", fileContent, Field.Store.YES);     
    //檔案路徑域(不分析、不索引、只儲存)    
    Field filePathField = new TextField("path", filePath, Field.Store.YES);     
    //檔案大小域       
    Field fileSizeField = new TextField("size", fileSize + "", Field.Store.YES); 
    //建立document物件     
    Document document = new Document();     
    document.add(fileNameField);       
    document.add(fileContentField);    
    document.add(filePathField);      
    document.add(fileSizeField);       
    //建立索引,並寫入索引庫       
    indexWriter.addDocument(document); 
  }    
  //關閉indexwriter    
  indexWriter.close();
}

5.2.3使用Luke工具檢視索引檔案

我們使用的luke的版本是luke-7.4.0,跟lucene的版本對應的。可以開啟7.4.0版本的lucene建立的索引庫。需要注意的是此版本的Luke是jdk9編譯的,所以要想執行此工具還需要jdk9才可以。

5.3查詢索引

5.3.1實現步驟

第一步:建立一個Directory物件,也就是索引庫存放的位置。

第二步:建立一個indexReader物件,需要指定Directory物件。

第三步:建立一個indexsearcher物件,需要指定IndexReader物件

第四步:建立一個TermQuery物件,指定查詢的域和查詢的關鍵詞。

第五步:執行查詢。

第六步:返回查詢結果。遍歷查詢結果並輸出。

第七步:關閉IndexReader物件

5.3.2程式碼實現

//查詢索引庫
@Test
public void searchIndex() throws Exception {    
//指定索引庫存放的路徑    
//D:\temp\index    
Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());   
 //建立indexReader物件    
IndexReader indexReader = DirectoryReader.open(directory);    
//建立indexsearcher物件    
IndexSearcher indexSearcher = new IndexSearcher(indexReader);   
 //建立查詢    
Query query = new TermQuery(new Term("filename", "apache"));   
 //執行查詢   
 //第一個引數是查詢物件,第二個引數是查詢結果返回的最大值
    TopDocs topDocs = indexSearcher.search(query, 10);   
 //查詢結果的總條數    
System.out.println("查詢結果的總條數:"+ topDocs.totalHits);    
//遍歷查詢結果    
//topDocs.scoreDocs儲存了document物件的id   
 for (ScoreDoc scoreDoc : topDocs.scoreDocs) {       
 //scoreDoc.doc屬性就是document物件的id        
//根據document的id找到document物件        
Document document = indexSearcher.doc(scoreDoc.doc);      
  System.out.println(document.get("filename"));       
 //System.out.println(document.get("content"));       
 System.out.println(document.get("path"));        
System.out.println(document.get("size"));       
 System.out.println("-------------------------");   
 }    
//關閉indexreader物件   
 indexReader.close();
}

6分析器

6.1分析器的分詞效果

//檢視標準分析器的分詞效果
@Test
public void testTokenStream() throws Exception {    
//建立一個標準分析器物件  
  Analyzer analyzer = new StandardAnalyzer();   
 //獲得tokenStream物件   
 //第一個引數:域名,可以隨便給一個    
//第二個引數:要分析的文字內容 
   TokenStream tokenStream = analyzer.tokenStream("test", "The Spring Framework provides a comprehensive programming and configuration model.");    
//新增一個引用,可以獲得每個關鍵詞   
 CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);    
//新增一個偏移量的引用,記錄了關鍵詞的開始位置以及結束位置   
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
//將指標調整到列表的頭部 tokenStream.reset(); //遍歷關鍵詞列表,通過incrementToken方法判斷列表是否結束 while(tokenStream.incrementToken()) { //關鍵詞的起始位置 System.out.println("start->" + offsetAttribute.startOffset()); //取關鍵詞 System.out.println(charTermAttribute); //結束位置 System.out.println("end->" + offsetAttribute.endOffset()); } tokenStream.close(); }

6.2中文分析器

6.2.1Lucene自帶中文分詞器

lStandardAnalyzer:

單字分詞:就是按照中文一個字一個字地進行分詞。如:“我愛中國”,效果:“我”、“愛”、“中”、“國”。

lSmartChineseAnalyzer

對中文支援較好,但擴充套件性差,擴充套件詞庫,禁用詞庫和同義詞庫等不好處理

6.2.2IKAnalyzer

使用方法:

第一步:把jar包新增到工程中

第二步:把配置檔案和擴充套件詞典和停用詞詞典新增到classpath下

注意:hotword.dic和ext_stopword.dic檔案的格式為UTF-8,注意是無BOM的UTF-8編碼。

也就是說禁止使用windows記事本編輯擴充套件詞典檔案

使用EditPlus.exe儲存為無BOM的UTF-8編碼格式,如下圖:

6.3使用自定義分析器

@Test
public void addDocument() throws Exception {   
 //索引庫存放路徑    
Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());    
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());    
//建立一個indexwriter物件   
 IndexWriter indexWriter = new IndexWriter(directory, config);

//...

}


7
索引庫的維護

7.1索引庫的新增

7.1.1Field域的屬性

是否分析:是否對域的內容進行分詞處理。前提是我們要對域的內容進行查詢。

是否索引:將Field分析後的詞或整個Field值進行索引,只有索引方可搜尋到。

比如:商品名稱、商品簡介分析後進行索引,訂單號、身份證號不用分析但也要索引,這些將來都要作為查詢條件。

是否儲存:將Field值儲存在文件中,儲存在文件中的Field才可以從Document中獲取

比如:商品名稱、訂單號,凡是將來要從Document中獲取的Field都要儲存。

是否儲存的標準:是否要將內容展示給使用者

7.1.2新增文件程式碼實現

//新增索引
@Test
public void addDocument() throws Exception {    
  //索引庫存放路徑        
  Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());      
  IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());     
  //建立一個indexwriter物件       
  IndexWriter indexWriter = new IndexWriter(directory, config);         
//建立一個Document物件 Document document = new Document(); //向document物件中新增域。 //不同的document可以有不同的域,同一個document可以有相同的域。 document.add(new TextField("filename", "新新增的文件", Field.Store.YES)); document.add(new TextField("content", "新新增的文件的內容", Field.Store.NO)); //LongPoint建立索引 document.add(new LongPoint("size", 1000l));
//StoreField儲存資料 document.add(new StoredField("size", 1000l)); //不需要建立索引的就使用StoreField儲存 document.add(new StoredField("path", "d:/temp/1.txt"));
//新增文件到索引庫 indexWriter.addDocument(document); //關閉indexwriter indexWriter.close(); }

7.2索引庫刪除

7.2.1刪除全部

//刪除全部索引
@Test
public void deleteAllIndex() throws Exception {
IndexWriter indexWriter = getIndexWriter();
//刪除全部索引
indexWriter.deleteAll();
//關閉indexwriter
indexWriter.close();
}

說明:將索引目錄的索引資訊全部刪除,直接徹底刪除,無法恢復。

此方法慎用!!

7.2.2指定查詢條件刪除

7.3索引庫的修改

//根據查詢條件刪除索引
@Test
public void deleteIndexByQuery() throws Exception {
IndexWriter indexWriter = getIndexWriter();
//建立一個查詢條件
Query query = new TermQuery(new Term("filename", "apache"));
//根據查詢條件刪除
indexWriter.deleteDocuments(query);
//關閉indexwriter
indexWriter.close();
}

原理就是先刪除後新增。

//修改索引庫
@Test
public void updateIndex() throws Exception {       
 IndexWriter indexWriter = getIndexWriter();       
 //建立一個Document物件       
 Document document = new Document();     
 //向document物件中新增域。       
 //不同的document可以有不同的域,同一個document可以有相同的域。        document.add(new TextField("filename", "要更新的文件", Field.Store.YES));        
  document.add(new TextField("content", " Lucene 簡介 Lucene 是一個基於 Java 的全文信    +"息檢索工具包," +"它不是一個完整的搜尋應用程式,而是為你的應用程式提供索引和搜尋功能。",                    Field.Store.YES));    
  indexWriter.updateDocument(new Term("content", "java"), document);     
    //關閉indexWriter     
  indexWriter.close();
}

8Lucene索引庫查詢

對要搜尋的資訊建立Query查詢物件,Lucene會根據Query查詢物件生成最終的查詢語法,類似關係資料庫Sql語法一樣Lucene也有自己的查詢語法,比如:“name:lucene”表示查詢Field的name為“lucene”的文件資訊。

可通過兩種方法建立查詢物件:

1)使用Lucene提供Query子類

2)使用QueryParse解析查詢表示式

8.1TermQuery

TermQuery,通過項查詢,TermQuery不使用分析器所以建議匹配不分詞的Field域查詢,比如訂單號、分類ID號等。

指定要查詢的域和要查詢的關鍵詞。

//使用Termquery查詢
@Test
public void testTermQuery() throws Exception {      
    Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());      
    IndexReader indexReader = DirectoryReader.open(directory);        
IndexSearcher indexSearcher = new IndexSearcher(indexReader); //建立查詢物件 Query query = new TermQuery(new Term("content", "lucene")); //執行查詢 TopDocs topDocs = indexSearcher.search(query, 10); //共查詢到的document個數 System.out.println("查詢結果總數量:" + topDocs.totalHits); //遍歷查詢結果 for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document document = indexSearcher.doc(scoreDoc.doc); System.out.println(document.get("filename")); //System.out.println(document.get("content")); System.out.println(document.get("path")); System.out.println(document.get("size")); } //關閉indexreader indexSearcher.getIndexReader().close(); }

8.2數值範圍查詢

@Test
public void testRangeQuery() throws Exception {   
   IndexSearcher indexSearcher = getIndexSearcher();   
   Query query = LongPoint.newRangeQuery("size", 0l, 10000l);    
   printResult(query, indexSearcher); 
}

8.3使用queryparser查詢

通過QueryParser也可以建立Query,QueryParser提供一個Parse方法,此方法可以直接根據查詢語法來查詢。Query物件執行的查詢語法可通過System.out.println(query);查詢。

需要使用到分析器。建議建立索引時使用的分析器和查詢索引時使用的分析器要一致。

需要加入queryParser依賴的jar包。

@Test
public
void testQueryParser() throws Exception { IndexSearcher indexSearcher = getIndexSearcher(); //建立queryparser物件 //第一個引數預設搜尋的域 //第二個引數就是分析器物件 QueryParser queryParser = new QueryParser("content", new IKAnalyzer()); Query query = queryParser.parse("Lucene是java開發的"); //執行查詢 printResult(query, indexSearcher); } private void printResult(Query query, IndexSearcher indexSearcher) throws Exception { //執行查詢 TopDocs topDocs = indexSearcher.search(query, 10); //共查詢到的document個數 System.out.println("查詢結果總數量:" + topDocs.totalHits); //遍歷查詢結果 for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println(document.get("filename"));
//System.out.println(document.get("content"));
System.out.println(document.get("path"));
System.out.println(document.get("size"));
} //關閉indexreader indexSearcher.getIndexReader().close(); }


心有玲曦遇奇緣歡迎大家掃碼關注