全文檢索技術:Lucene 從發芽到入土

全文檢索技術:Lucene

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等搜索引擎、論壇站內搜索、電商網站站內搜索等。

3 Lucene實現全文檢索的流程

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.1 Lucene

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.1 Lucene自帶中文分詞器

l StandardAnalyzer:

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

l SmartChineseAnalyzer

對中文支持較好,但擴展性差,擴展詞庫,禁用詞庫和同義詞庫等不好處理

6.2.2 IKAnalyzer

在這裏插入圖片描述

使用方法:

第一步:把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.1 索引庫的添加

7.1.1 Field域的屬性

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

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

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

****是否存儲****:將Field值存儲在文檔中,存儲在文檔中的Field纔可以從Document中獲取

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

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

Field類 數據類型 Analyzed是否分析 Indexed是否索引 Stored是否存儲 說明
StringField(FieldName, FieldValue,Store.YES)) 字符串 N Y Y或N 這個Field用來構建一個字符串Field,但是不會進行分析,會將整個串存儲在索引中,比如(訂單號,姓名等)是否存儲在文檔中用Store.YES或Store.NO決定
LongPoint(String name, long… point) Long型 Y Y N 可以使用LongPoint、IntPoint等類型存儲數值類型的數據。讓數值類型可以進行索引。但是不能存儲數據,如果想存儲數據還需要使用StoredField。
StoredField(FieldName, FieldValue) 重載方法,支持多種類型 N N Y 這個Field用來構建不同類型Field不分析,不索引,但要Field存儲在文檔中
TextField(FieldName, FieldValue, Store.NO)或TextField(FieldName, reader) 字符串或流 Y Y Y或N 如果是一個Reader, lucene猜測內容比較多,會採用Unstored的策略.
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 指定查詢條件刪除
//根據查詢條件刪除索引
	@Test
	public void deleteIndexByQuery() throws Exception {
		IndexWriter indexWriter = getIndexWriter();
		//創建一個查詢條件
		Query query = new TermQuery(new Term("filename", "apache"));
		//根據查詢條件刪除
		indexWriter.deleteDocuments(query);
		//關閉indexwriter
		indexWriter.close();
	}

7.3 索引庫的修改

原理就是先刪除後添加。

//修改索引庫
@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();
}

8 Lucene索引庫查詢

​ 對要搜索的信息創建Query查詢對象,Lucene會根據Query查詢對象生成最終的查詢語法,類似關係數據庫Sql語法一樣Lucene也有自己的查詢語法,比如:“name:lucene”表示查詢Field的name爲“lucene”的文檔信息。

​ 可通過兩種方法創建查詢對象:

​ 1)使用Lucene提供Query子類

​ 2)使用QueryParse解析查詢表達式

8.1 TermQuery

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();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章