lucene學習總結篇--lucene全文檢索的基本原理和lucene API簡單的使用

後開啟撒打發了發表於2018-01-05

一、常用的jar包

先介紹一下常用的幾個檔案jar包。下面有maven pom.xml參考

lucene-core:其中包括了常用的文件,索引,搜尋,儲存等相關核心程式碼

lucene-analyzers-common:這裡麵包含了各種語言的詞法分析器,用於對檔案內容進行關鍵字切分,提取。

lucene-highlighter:這個jar包主要用於搜尋出的內容高亮顯示。

lucene-queryparser:提供了搜尋相關的程式碼,用於各種搜尋,比如模糊搜尋,範圍搜尋,等等。

<dependencies>

        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>7.2.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>7.2.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>7.2.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-highlighter -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>7.2.0</version>
        </dependency>
        
    </dependencies>

二、基於Java的全文索引/檢索引擎——Lucene

Lucene不是一個完整的全文索引應用,而是是一個用Java寫的全文索引引擎工具包,它可以方便的嵌入到各種應用中實現針對應用的全文索引/檢索功能。

下面引用的內容是參考:
https://www.chedong.com/tech/lucene.html
http://blog.csdn.net/forfuture1978/article/details/4711308
http://lucene.apache.org/java/docs/index.html

Lucene 是一個高效的,基於Java 的全文檢索庫。
所以在瞭解Lucene之前要費一番工夫瞭解一下全文檢索。 那麼什麼叫做全文檢索呢?這要從我們生活中的資料說起。
我們生活中的資料總體分為兩種:結構化資料 和非結構化資料 。

  • 結構化資料: 指具有固定格式或有限長度的資料,如資料庫,後設資料等。
  • 非結構化資料:指不定長或無固定格式的資料,如郵件,word文件等。 當然有的地方還會提到第三種,半結構化資料,如XML,HTML等,當根據需要可按結構化資料來處理,也可抽取出純文字按非結構化資料來處理。

非結構化資料又一種叫法叫全文資料。
按照資料的分類,搜尋也分為兩種:

  • 對結構化資料的搜尋 :如對資料庫的搜尋,用SQL語句。再如對後設資料的搜尋,如利用windows搜尋對檔名,型別,修改時間進行搜尋等。
  • 對非結構化資料的搜尋 :如利用windows的搜尋也可以搜尋檔案內容,Linux下的grep命令,再如用Google和百度可以搜尋大量內容資料。

對非結構化資料也即對全文資料的搜尋主要有兩種方法:
一種是順序掃描法 (Serial Scanning): 所謂順序掃描,比如要找內容包含某一個字串的檔案,就是一個文件一個文件的看,對於每一個文件,從頭看到尾,如果此文件包含此字串,則此文件為我們要找的檔案,接著看下一個檔案,直到掃描完所有的檔案。如利用windows的搜尋也可以搜尋檔案內容,只是相當的慢。如果你有一個80G硬碟,如果想在上面找到一個內容包含某字串的檔案,不花他幾個小時,怕是做不到。Linux下的grep命令也是這一種方式。大家可能覺得這種方法比較原始,但對於小資料量的檔案,這種方法還是最直接,最方便的。但是對於大量的檔案,這種方法就很慢了。 有人可能會說,對非結構化資料順序掃描很慢,對結構化資料的搜尋卻相對較快(由於結構化資料有一定的結構可以採取一定的搜尋演算法加快速度),那麼把我們的非結構化資料想辦法弄得有一定結構不就行了嗎?

這種想法很天然,卻構成了全文檢索的基本思路,也即將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。 這部分從非結構化資料中提取出的然後重新組織的資訊,我們稱之索引 。

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

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

全文檢索大體分兩個過程,索引建立 (Indexing) 和搜尋索引 (Search) 。

  • 索引建立:將現實世界中所有的結構化和非結構化資料提取資訊,建立索引的過程。
  • 搜尋索引:就是得到使用者的查詢請求,搜尋建立的索引,然後返回結果的過程。 於是全文檢索就存在三個重要問題:
  1. 索引裡面究竟存些什麼?(Index)
  2. 如何建立索引?(Indexing)
  3. 如何對索引進行搜尋?(Search)

對原理的一些講解請參考下面連結,講的很清楚: http://blog.csdn.net/forfuture1978/article/details/4711308

Lucene的架構設計


1、索引裡面究竟存些什麼

索引裡面究竟需要存些什麼呢?

首先我們來看為什麼順序掃描的速度慢:

其實是由於我們想要搜尋的資訊和非結構化資料中所儲存的資訊不一致造成的。

非結構化資料中所儲存的資訊是每個檔案包含哪些字串,也即已知檔案,欲求字串相對容易,也即是從檔案到字串的對映。而我們想搜尋的資訊是哪些檔案包含此字串,也即已知字串,欲求檔案,也即從字串到檔案的對映。兩者恰恰相反。於是如果索引總能夠儲存從字串到檔案的對映,則會大大提高搜尋速度。

由於從字串到檔案的對映是檔案到字串對映的反向過程,於是儲存這種資訊的索引稱為反向索引

反向索引的所儲存的資訊一般如下:

假設我的文件集合裡面有100篇文件,為了方便表示,我們為文件編號從1到100,得到下面的結構

inverted index

左邊儲存的是一系列字串,稱為詞典

每個字串都指向包含此字串的文件(Document)連結串列,此文件連結串列稱為倒排表(Posting List)。

有了索引,便使儲存的資訊和要搜尋的資訊一致,可以大大加快搜尋的速度。

比如說,我們要尋找既包含字串“lucene”又包含字串“solr”的文件,我們只需要以下幾步:

1. 取出包含字串“lucene”的文件連結串列。

2. 取出包含字串“solr”的文件連結串列。

3. 通過合併連結串列,找出既包含“lucene”又包含“solr”的檔案。

inverted index merge

看到這個地方,有人可能會說,全文檢索的確加快了搜尋的速度,但是多了索引的過程,兩者加起來不一定比順序掃描快多少。的確,加上索引的過程,全文檢索不一定比順序掃描快,尤其是在資料量小的時候更是如此。而對一個很大量的資料建立索引也是一個很慢的過程。

然而兩者還是有區別的,順序掃描是每次都要掃描,而建立索引的過程僅僅需要一次,以後便是一勞永逸的了,每次搜尋,建立索引的過程不必經過,僅僅搜尋建立好的索引就可以了。

這也是全文搜尋相對於順序掃描的優勢之一:一次索引,多次使用。


2、如何建立索引

全文檢索的索引建立過程一般有以下幾步:

  • 第一步:一些要索引的原文件(Document)。

為了方便說明索引建立過程,這裡特意用兩個檔案為例:

檔案一:Students should be allowed to go out with their friends, but not allowed to drink beer.

檔案二:My friend Jerry went to school to see his students but found them drunk which is not allowed.

  • 第二步:將原文件傳給分次元件(Tokenizer)。

分片語件(Tokenizer)會做以下幾件事情(此過程稱為Tokenize):

1. 將文件分成一個一個單獨的單詞。

2. 去除標點符號。

3. 去除停詞(Stop word)。

所謂停詞(Stop word)就是一種語言中最普通的一些單詞,由於沒有特別的意義,因而大多數情況下不能成為搜尋的關鍵詞,因而建立索引時,這種詞會被去掉而減少索引的大小。

英語中挺詞(Stop word)如:“the”,“a”,“this”等。

對於每一種語言的分片語件(Tokenizer),都有一個停詞(stop word)集合。

經過分詞(Tokenizer)後得到的結果稱為詞元(Token)。

在我們的例子中,便得到以下詞元(Token):

“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,“beer”,“My”,“friend”,“Jerry”,“went”,“school”,“see”,“his”,“students”,“found”,“them”,“drunk”,“allowed”。

  • 第三步:將得到的詞元(Token)傳給語言處理元件(Linguistic Processor)。

語言處理元件(linguistic processor)主要是對得到的詞元(Token)做一些同語言相關的處理。

對於英語,語言處理元件(Linguistic Processor)一般做以下幾點:

1. 變為小寫(Lowercase)。

2. 將單詞縮減為詞根形式,如“cars”到“car”等。這種操作稱為:stemming。

3. 將單詞轉變為詞根形式,如“drove”到“drive”等。這種操作稱為:lemmatization。

 

Stemming 和 lemmatization的異同:

  • 相同之處:Stemming和lemmatization都要使詞彙成為詞根形式。
  • 兩者的方式不同:
    • Stemming採用的是“縮減”的方式:“cars”到“car”,“driving”到“drive”。
    • Lemmatization採用的是“轉變”的方式:“drove”到“drove”,“driving”到“drive”。
  • 兩者的演算法不同:
    • Stemming主要是採取某種固定的演算法來做這種縮減,如去除“s”,去除“ing”加“e”,將“ational”變為“ate”,將“tional”變為“tion”。
    • Lemmatization主要是採用儲存某種字典的方式做這種轉變。比如字典中有“driving”到“drive”,“drove”到“drive”,“am, is, are”到“be”的對映,做轉變時,只要查字典就可以了。
  • Stemming和lemmatization不是互斥關係,是有交集的,有的詞利用這兩種方式都能達到相同的轉換。

語言處理元件(linguistic processor)的結果稱為詞(Term)。

在我們的例子中,經過語言處理,得到的詞(Term)如下:

“student”,“allow”,“go”,“their”,“friend”,“allow”,“drink”,“beer”,“my”,“friend”,“jerry”,“go”,“school”,“see”,“his”,“student”,“find”,“them”,“drink”,“allow”。

 

也正是因為有語言處理的步驟,才能使搜尋drove,而drive也能被搜尋出來。

  • 第四步:將得到的詞(Term)傳給索引元件(Indexer)。

索引元件(Indexer)主要做以下幾件事情:

1. 利用得到的詞(Term)建立一個字典。

在我們的例子中字典如下:

Term Document ID
student 1
allow 1
go 1
their 1
friend 1
allow 1
drink 1
beer 1
my 2
friend 2
jerry 2
go 2
school 2
see 2
his 2
student 2
find 2
them 2
drink 2
allow 2

 

2. 對字典按字母順序進行排序。

 

Term Document ID
allow 1
allow 1
allow 2
beer 1
drink 1
drink 2
find 2
friend 1
friend 2
go 1
go 2
his 2
jerry 2
my 2
school 2
see 2
student 1
student 2
their 1
them 2

3. 合併相同的詞(Term)成為文件倒排(Posting List)連結串列。

postinglist

在此表中,有幾個定義:

  • Document Frequency 即文件頻次,表示總共有多少檔案包含此詞(Term)。
  • Frequency 即詞頻率,表示此檔案中包含了幾個此詞(Term)。

所以對詞(Term) “allow”來講,總共有兩篇文件包含此詞(Term),從而詞(Term)後面的文件連結串列總共有兩項,第一項表示包含“allow”的第一篇文件,即1號文件,此文件中,“allow”出現了2次,第二項表示包含“allow”的第二個文件,是2號文件,此文件中,“allow”出現了1次。

到此為止,索引已經建立好了,我們可以通過它很快的找到我們想要的文件。

而且在此過程中,我們驚喜地發現,搜尋“drive”,“driving”,“drove”,“driven”也能夠被搜到。因為在我們的索引中,“driving”,“drove”,“driven”都會經過語言處理而變成“drive”,在搜尋時,如果您輸入“driving”,輸入的查詢語句同樣經過我們這裡的一到三步,從而變為查詢“drive”,從而可以搜尋到想要的文件。


3、如何對索引進行搜尋?

到這裡似乎我們可以宣佈“我們找到想要的文件了”。

然而事情並沒有結束,找到了僅僅是全文檢索的一個方面。不是嗎?如果僅僅只有一個或十個文件包含我們查詢的字串,我們的確找到了。然而如果結果有一千個,甚至成千上萬個呢?那個又是您最想要的檔案呢?

開啟Google吧,比如說您想在微軟找份工作,於是您輸入“Microsoft job”,您卻發現總共有22600000個結果返回。好大的數字呀,突然發現找不到是一個問題,找到的太多也是一個問題。在如此多的結果中,如何將最相關的放在最前面呢?

clip_image002[4]

當然Google做的很不錯,您一下就找到了jobs at Microsoft。想象一下,如果前幾個全部是“Microsoft does a good job at software industry…”將是多麼可怕的事情呀。

如何像Google一樣,在成千上萬的搜尋結果中,找到和查詢語句最相關的呢?

如何判斷搜尋出的文件和查詢語句的相關性呢?

這要回到我們第三個問題:如何對索引進行搜尋?

搜尋主要分為以下幾步:

  • 第一步:使用者輸入查詢語句。

查詢語句同我們普通的語言一樣,也是有一定語法的。

不同的查詢語句有不同的語法,如SQL語句就有一定的語法。

查詢語句的語法根據全文檢索系統的實現而不同。最基本的有比如:AND, OR, NOT等。

舉個例子,使用者輸入語句:lucene AND learned NOT hadoop。

說明使用者想找一個包含lucene和learned然而不包括hadoop的文件。

  • 第二步:對查詢語句進行詞法分析,語法分析,及語言處理。

由於查詢語句有語法,因而也要進行語法分析,語法分析及語言處理。

1. 詞法分析主要用來識別單詞和關鍵字。

如上述例子中,經過詞法分析,得到單詞有lucene,learned,hadoop, 關鍵字有AND, NOT。

如果在詞法分析中發現不合法的關鍵字,則會出現錯誤。如lucene AMD learned,其中由於AND拼錯,導致AMD作為一個普通的單詞參與查詢。

2. 語法分析主要是根據查詢語句的語法規則來形成一棵語法樹。

如果發現查詢語句不滿足語法規則,則會報錯。如lucene NOT AND learned,則會出錯。

如上述例子,lucene AND learned NOT hadoop形成的語法樹如下:

語法樹

3. 語言處理同索引過程中的語言處理幾乎相同。

如learned變成learn等。

經過第二步,我們得到一棵經過語言處理的語法樹。

語法樹1

  • 第三步:搜尋索引,得到符合語法樹的文件。

此步驟有分幾小步:

  1. 首先,在反向索引表中,分別找出包含lucene,learn,hadoop的文件連結串列。
  2. 其次,對包含lucene,learn的連結串列進行合併操作,得到既包含lucene又包含learn的文件連結串列。
  3. 然後,將此連結串列與hadoop的文件連結串列進行差操作,去除包含hadoop的文件,從而得到既包含lucene又包含learn而且不包含hadoop的文件連結串列。
  4. 此文件連結串列就是我們要找的文件。

  • 第四步:根據得到的文件和查詢語句的相關性,對結果進行排序。

雖然在上一步,我們得到了想要的文件,然而對於查詢結果應該按照與查詢語句的相關性進行排序,越相關者越靠前。

如何計算文件和查詢語句的相關性呢?

不如我們把查詢語句看作一片短小的文件,對文件與文件之間的相關性(relevance)進行打分(scoring),分數高的相關性好,就應該排在前面。

那麼又怎麼對文件之間的關係進行打分呢?

這可不是一件容易的事情,首先我們看一看判斷人之間的關係吧。

首先看一個人,往往有很多要素,如性格,信仰,愛好,衣著,高矮,胖瘦等等。

其次對於人與人之間的關係,不同的要素重要性不同,性格,信仰,愛好可能重要些,衣著,高矮,胖瘦可能就不那麼重要了,所以具有相同或相似性格,信仰,愛好的人比較容易成為好的朋友,然而衣著,高矮,胖瘦不同的人,也可以成為好的朋友。

因而判斷人與人之間的關係,首先要找出哪些要素對人與人之間的關係最重要,比如性格,信仰,愛好。其次要判斷兩個人的這些要素之間的關係,比如一個人性格開朗,另一個人性格外向,一個人信仰佛教,另一個信仰上帝,一個人愛好打籃球,另一個愛好踢足球。我們發現,兩個人在性格方面都很積極,信仰方面都很善良,愛好方面都愛運動,因而兩個人關係應該會很好。


我們再來看看公司之間的關係吧。

首先看一個公司,有很多人組成,如總經理,經理,技術長,普通員工,保安,門衛等。

其次對於公司與公司之間的關係,不同的人重要性不同,總經理,經理,技術長可能更重要一些,普通員工,保安,門衛可能較不重要一點。所以如果兩個公司總經理,經理,技術長之間關係比較好,兩個公司容易有比較好的關係。然而一位普通員工就算與另一家公司的一位普通員工有血海深仇,怕也難影響兩個公司之間的關係。

因而判斷公司與公司之間的關係,首先要找出哪些人對公司與公司之間的關係最重要,比如總經理,經理,技術長。其次要判斷這些人之間的關係,不如兩家公司的總經理曾經是同學,經理是老鄉,技術長曾是創業夥伴。我們發現,兩家公司無論總經理,經理,技術長,關係都很好,因而兩家公司關係應該會很好。

 

分析了兩種關係,下面看一下如何判斷文件之間的關係了。

首先,一個文件有很多詞(Term)組成,如search, lucene, full-text, this, a, what等。

其次對於文件之間的關係,不同的Term重要性不同,比如對於本篇文件,search, Lucene, full-text就相對重要一些,this, a , what可能相對不重要一些。所以如果兩篇文件都包含search, Lucene,fulltext,這兩篇文件的相關性好一些,然而就算一篇文件包含this, a, what,另一篇文件不包含this, a, what,也不能影響兩篇文件的相關性。

因而判斷文件之間的關係,首先找出哪些詞(Term)對文件之間的關係最重要,如search, Lucene, fulltext。然後判斷這些詞(Term)之間的關係。

 

找出詞(Term)對文件的重要性的過程稱為計算詞的權重(Term weight)的過程。

計算詞的權重(term weight)有兩個引數,第一個是詞(Term),第二個是文件(Document)。

詞的權重(Term weight)表示此詞(Term)在此文件中的重要程度,越重要的詞(Term)有越大的權重(Term weight),因而在計算文件之間的相關性中將發揮更大的作用。

判斷詞(Term)之間的關係從而得到文件相關性的過程應用一種叫做向量空間模型的演算法(Vector Space Model)。

下面仔細分析一下這兩個過程:

1. 計算權重(Term weight)的過程。

影響一個詞(Term)在一篇文件中的重要性主要有兩個因素:

  • Term Frequency (tf):即此Term在此文件中出現了多少次。tf 越大說明越重要。
  • Document Frequency (df):即有多少文件包含次Term。df 越大說明越不重要。

容易理解嗎?詞(Term)在文件中出現的次數越多,說明此詞(Term)對該文件越重要,如“搜尋”這個詞,在本文件中出現的次數很多,說明本文件主要就是講這方面的事的。然而在一篇英語文件中,this出現的次數更多,就說明越重要嗎?不是的,這是由第二個因素進行調整,第二個因素說明,有越多的文件包含此詞(Term), 說明此詞(Term)太普通,不足以區分這些文件,因而重要性越低。

這也如我們程式設計師所學的技術,對於程式設計師本身來說,這項技術掌握越深越好(掌握越深說明花時間看的越多,tf越大),找工作時越有競爭力。然而對於所有程式設計師來說,這項技術懂得的人越少越好(懂得的人少df小),找工作越有競爭力。人的價值在於不可替代性就是這個道理。

道理明白了,我們來看看公式:

image

image

這僅僅只term weight計算公式的簡單典型實現。實現全文檢索系統的人會有自己的實現,Lucene就與此稍有不同。

 

2. 判斷Term之間的關係從而得到文件相關性的過程,也即向量空間模型的演算法(VSM)。

我們把文件看作一系列詞(Term),每一個詞(Term)都有一個權重(Term weight),不同的詞(Term)根據自己在文件中的權重來影響文件相關性的打分計算。

於是我們把所有此文件中詞(term)的權重(term weight) 看作一個向量。

Document = {term1, term2, …… ,term N}

Document Vector = {weight1, weight2, …… ,weight N}

同樣我們把查詢語句看作一個簡單的文件,也用向量來表示。

Query = {term1, term 2, …… , term N}

Query Vector = {weight1, weight2, …… , weight N}

我們把所有搜尋出的文件向量及查詢向量放到一個N維空間中,每個詞(term)是一維。

如圖:

vsm

我們認為兩個向量之間的夾角越小,相關性越大。

所以我們計算夾角的餘弦值作為相關性的打分,夾角越小,餘弦值越大,打分越高,相關性越大。

有人可能會問,查詢語句一般是很短的,包含的詞(Term)是很少的,因而查詢向量的維數很小,而文件很長,包含詞(Term)很多,文件向量維數很大。你的圖中兩者維數怎麼都是N呢?

在這裡,既然要放到相同的向量空間,自然維數是相同的,不同時,取二者的並集,如果不含某個詞(Term)時,則權重(Term Weight)為0。

 

相關性打分公式如下:

image

舉個例子,查詢語句有11個Term,共有三篇文件搜尋出來。其中各自的權重(Term weight),如下表格。


t1

t2

t3

t4

t5

t6

t7

t8

t9

t10

t11

D1

0

0

.477

0

.477

.176

0

0

0

.176

0

D2

0

.176

0

.477

0

0

0

0

.954

0

.176

D3

0

.176

0

0

0

.176

0

0

0

.176

.176

Q

0

0

0

0

0

.176

0

0

.477

0

.176

於是計算,三篇文件同查詢語句的相關性打分分別為:

image

image

image

 

於是文件二相關性最高,先返回,其次是文件一,最後是文件三。

到此為止,我們可以找到我們最想要的文件了。

說了這麼多,其實還沒有進入到Lucene,而僅僅是資訊檢索技術(Information retrieval)中的基本理論,然而當我們看過Lucene後我們會發現,Lucene是對這種基本理論的一種基本的的實踐。所以在以後分析Lucene的文章中,會常常看到以上理論在Lucene中的應用。

在進入Lucene之前,對上述索引建立和搜尋過程所一個總結,如圖:

此圖參照http://www.lucene.com.cn/about.htm中文章《開放原始碼的全文檢索引擎Lucene》

clip_image016

1. 索引過程:

1) 有一系列被索引檔案

2) 被索引檔案經過語法分析和語言處理形成一系列詞(Term)。

3) 經過索引建立形成詞典和反向索引表。

4) 通過索引儲存將索引寫入硬碟。

2. 搜尋過程:

a) 使用者輸入查詢語句。

b) 對查詢語句經過語法分析和語言分析得到一系列詞(Term)。

c) 通過語法分析得到一個查詢樹。

d) 通過索引儲存將索引讀入到記憶體。

e) 利用查詢樹搜尋索引,從而得到每個詞(Term)的文件連結串列,對文件連結串列進行交,差,並得到結果文件。

f) 將搜尋到的結果文件對查詢的相關性進行排序。

g) 返回查詢結果給使用者。

參考連結:http://blog.csdn.net/forfuture1978/article/details/4711308

三、API的使用

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;

import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.TextField;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;


public class IndexManager{
    private static IndexManager indexManager;
    private static String content="";

    private static String INDEX_DIR = "D:\\lucene\\luceneIndex";
    private static String DATA_DIR = "D:\\lucene\\luceneData";
    private static Analyzer analyzer = null;
    private static Directory directory = null;
    private static IndexWriter indexWriter = null;

    /**
     * 建立索引管理器
     * @return 返回索引管理器物件
     */
    public IndexManager getManager(){
        if(indexManager == null){
            this.indexManager = new IndexManager();
        }
        return indexManager;
    }
    /**
     * 建立當前檔案目錄的索引
     * @param path 當前檔案目錄
     * @return 是否成功
     */
    public static boolean createIndex(String path) throws  Exception{
        Date date1 = new Date();
        List<File> fileList = getFileList(path);
        for (File file : fileList) {
            content = "";
            //獲取檔案字尾
            String type = file.getName().substring(file.getName().lastIndexOf(".")+1);
            if("txt".equalsIgnoreCase(type)){

                content += txt2String(file);

            }

            System.out.println("name :"+file.getName());
            System.out.println("path :"+file.getPath());
            System.out.println("content" + content);

            try{
                analyzer = new StandardAnalyzer();
                directory = FSDirectory.open(Paths.get(INDEX_DIR));

                File indexFile = new File(INDEX_DIR);
                if (!indexFile.exists()) {
                    indexFile.mkdirs();
                }
                IndexWriterConfig config = new IndexWriterConfig(analyzer);
                indexWriter = new IndexWriter(directory, config);

                Document document = new Document();
                document.add(new TextField("filename", file.getName(), Store.YES));
                document.add(new TextField("content", content, Store.YES));
                document.add(new TextField("path", file.getPath(), Store.YES));
                indexWriter.addDocument(document);
                indexWriter.commit();
                closeWriter();
            }catch(Exception e){
                e.printStackTrace();
            }
            content = "";
        }
        Date date2 = new Date();
        System.out.println("建立索引-----耗時:" + (date2.getTime() - date1.getTime()) + "ms\n");
        return true;
    }

    /**
     * 讀取txt檔案的內容
     * @param file 想要讀取的檔案物件
     * @return 返回檔案內容
     */
    public static String txt2String(File file){
        String result = "";
        try{
            BufferedReader br = new BufferedReader(new FileReader(file));//構造一個BufferedReader類來讀取檔案
            String s = null;
            while((s = br.readLine())!=null){//使用readLine方法,一次讀一行
                result = result + "\n" +s;
            }
            br.close();
        }catch(Exception e){
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 查詢索引,返回符合條件的檔案
     * @param text 查詢的字串
     * @return 符合條件的檔案List
     */
    public static void searchIndex(String text){
        Date date1 = new Date();
        try{
            directory = FSDirectory.open(Paths.get(INDEX_DIR));
            analyzer = new StandardAnalyzer();
            DirectoryReader ireader = DirectoryReader.open(directory);
            IndexSearcher isearcher = new IndexSearcher(ireader);

            // QueryParser的第一個引數就是建立index的Field,我們的例子中filename content path
            QueryParser parser = new QueryParser("content", analyzer);
            Query query = parser.parse(text);

            ScoreDoc[] hits = isearcher.search(query, 1, Sort.INDEXORDER).scoreDocs;

            System.out.println(hits.length);

            for (int i = 0; i < hits.length; i++) {
                Document hitDoc = isearcher.doc(hits[i].doc);
                System.out.println("____________________________");
                System.out.println(hitDoc.get("filename"));
                System.out.println(hitDoc.get("content"));
                System.out.println(hitDoc.get("path"));
                System.out.println("____________________________");
            }
            ireader.close();
            directory.close();
        }catch(Exception e){
            e.printStackTrace();
        }
        Date date2 = new Date();
        System.out.println("檢視索引-----耗時:" + (date2.getTime() - date1.getTime()) + "ms\n");
    }
    /**
     * 過濾目錄下的檔案
     * @param dirPath 想要獲取檔案的目錄
     * @return 返回檔案list
     */
    public static List<File> getFileList(String dirPath) {
        File[] files = new File(dirPath).listFiles();
        List<File> fileList = new ArrayList<File>();
        for (File file : files) {
            if (isTxtFile(file.getName())) {
                fileList.add(file);
            }
        }
        return fileList;
    }
    /**
     * 判斷是否為目標檔案,目前支援txt xls doc格式
     * @param fileName 檔名稱
     * @return 如果是檔案型別滿足過濾條件,返回true;否則返回false
     */
    public static boolean isTxtFile(String fileName) {
        if (fileName.lastIndexOf(".txt") > 0) {
            return true;
        }else if (fileName.lastIndexOf(".xls") > 0) {
            return true;
        }else if (fileName.lastIndexOf(".doc") > 0) {
            return true;
        }
        return false;
    }

    public static void closeWriter() throws Exception {
        if (indexWriter != null) {
            indexWriter.close();
        }
    }
    /**
     * 刪除檔案目錄下的所有檔案
     * @param file 要刪除的檔案目錄
     * @return 如果成功,返回true.
     */
    public static boolean deleteDir(File file){
        if(file.isDirectory()){
            File[] files = file.listFiles();
            for(int i=0; i<files.length; i++){
                deleteDir(files[i]);
            }
        }
        file.delete();
        return true;
    }


    // main function
    public static void main(String[] args) throws  Exception{
        File fileIndex = new File(INDEX_DIR);
        if(deleteDir(fileIndex)){
            fileIndex.mkdir();
        }else{
            fileIndex.mkdir();
        }

        createIndex(DATA_DIR);
        searchIndex("test1.txt");
    }
}
執行結果展示:
name :test1.txt
path :D:\lucene\luceneData\test1.txt
content
chenxun is a good man
zhouhuijuan is my love
建立索引-----耗時:612ms

1
____________________________
test1.txt

chenxun is a good man
zhouhuijuan is my love
D:\lucene\luceneData\test1.txt
____________________________
檢視索引-----耗時:130ms


相關文章