後端技術雜談3:Lucene基礎原理與實踐

  • 2019 年 12 月 9 日
  • 筆記

本文轉自互聯網,侵刪

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈

文章將同步到我的個人部落格:

www.how2playlife.com

該系列博文會介紹常見的後端技術,這對後端工程師來說是一種綜合能力,我們會逐步了解搜索技術,雲計算相關技術、大數據研發等常見的技術喜提,以便讓你更完整地了解後端技術棧的全貌,為後續參與分散式應用的開發和學習做好準備。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫我,歡迎你參與本系列博文的創作和修訂。

一、總論

根據lucene.apache.org/java/docs/i…定義:

Lucene是一個高效的,基於Java的全文檢索庫。

所以在了解Lucene之前要費一番工夫了解一下全文檢索。

那麼什麼叫做全文檢索呢?這要從我們生活中的數據說起。

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

  • 結構化數據:指具有固定格式或有限長度的數據,如資料庫,元數據等。
  • 非結構化數據:指不定長或無固定格式的數據,如郵件,word文檔等。

當然有的地方還會提到第三種,半結構化數據,如XML,HTML等,當根據需要可按結構化數據來處理,也可抽取出純文本按非結構化數據來處理。

非結構化數據又一種叫法叫全文數據。

按照數據的分類,搜索也分為兩種:

  • 對結構化數據的搜索:如對資料庫的搜索,用SQL語句。再如對元數據的搜索,如利用windows搜索對文件名,類型,修改時間進行搜索等。
  • 對非結構化數據的搜索:如利用windows的搜索也可以搜索文件內容,Linux下的grep命令,再如用Google和百度可以搜索大量內容數據。

對非結構化數據也即對全文數據的搜索主要有兩種方法:

一種是順序掃描法(Serial Scanning):所謂順序掃描,比如要找內容包含某一個字元串的文件,就是一個文檔一個文檔的看,對於每一個文檔,從頭看到尾,如果此文檔包含此字元串,則此文檔為我們要找的文件,接著看下一個文件,直到掃描完所有的文件。如利用windows的搜索也可以搜索文件內容,只是相當的慢。如果你有一個80G硬碟,如果想在上面找到一個內容包含某字元串的文件,不花他幾個小時,怕是做不到。Linux下的grep命令也是這一種方式。大家可能覺得這種方法比較原始,但對於小數據量的文件,這種方法還是最直接,最方便的。但是對於大量的文件,這種方法就很慢了。

有人可能會說,對非結構化數據順序掃描很慢,對結構化數據的搜索卻相對較快(由於結構化數據有一定的結構可以採取一定的搜索演算法加快速度),那麼把我們的非結構化數據想辦法弄得有一定結構不就行了嗎?

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

這部分從非結構化數據中提取出的然後重新組織的資訊,我們稱之索引。

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

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

下面這幅圖來自《Lucene in action》,但卻不僅僅描述了Lucene的檢索過程,而是描述了全文檢索的一般過程。

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

  • 索引創建:將現實世界中所有的結構化和非結構化數據提取資訊,創建索引的過程。
  • 搜索索引:就是得到用戶的查詢請求,搜索創建的索引,然後返回結果的過程。

於是全文檢索就存在三個重要問題:

1. 索引裡面究竟存些什麼?(Index)

2. 如何創建索引?(Indexing)

3. 如何對索引進行搜索?(Search)

下面我們順序對每個個問題進行研究。

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

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

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

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

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

由於從字元串到文件的映射是文件到字元串映射的反向過程,於是保存這種資訊的索引稱為反向索引。

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

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

左邊保存的是一系列字元串,稱為詞典。

每個字元串都指向包含此字元串的文檔(Document)鏈表,此文檔鏈表稱為倒排表(Posting List)。

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

比如說,我們要尋找既包含字元串「lucene」又包含字元串「solr」的文檔,我們只需要以下幾步:

1. 取出包含字元串「lucene」的文檔鏈表。

2. 取出包含字元串「solr」的文檔鏈表。

3. 通過合併鏈表,找出既包含「lucene」又包含「solr」的文件。

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

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

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

三、如何創建索引

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

第一步:一些要索引的原文檔(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)鏈表。

在此表中,有幾個定義:

  • 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」,從而可以搜索到想要的文檔。

三、如何對索引進行搜索?

到這裡似乎我們可以宣布「我們找到想要的文檔了」。

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

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

當然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. 首先,在反向索引表中,分別找出包含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小),找工作越有競爭力。人的價值在於不可替代性就是這個道理。

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

這僅僅只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)是一維。

如圖:

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

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

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

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

相關性打分公式如下:

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

t10

t11

.477

.477

.176

.176

.176

.477

.954

.176

.176

.176

.176

.176

.176

.477

.176

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

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

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

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

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

此圖參照www.lucene.com.cn/about.htm中文章《開放源程式碼的全文檢索引擎Lucene》

1. 索引過程:

1) 有一系列被索引文件

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

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

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

2. 搜索過程:

a) 用戶輸入查詢語句。

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

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

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

e) 利用查詢樹搜索索引,從而得到每個詞(Term)的文檔鏈表,對文檔鏈表進行交,差,並得到結果文檔。

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

g) 返回查詢結果給用戶。

下面我們可以進入Lucene的世界了。

CSDN中此文章鏈接為blog.csdn.net/forfuture19…

Javaeye中此文章鏈接為forfuture1978.javaeye.com/blog/546771

Spring Boot 中使用 Java API 調用 lucene

Github 程式碼

程式碼我已放到 Github ,導入spring-boot-lucene-demo 項目

github spring-boot-lucene-demo

添加依賴

<!--對分詞索引查詢解析--><dependency>    <groupId>org.apache.lucene</groupId>    lucene-queryparser    <version>7.1.0</version></dependency> <!--高亮 --><dependency>    <groupId>org.apache.lucene</groupId>    lucene-highlighter    <version>7.1.0</version></dependency> <!--smartcn 中文分詞器 SmartChineseAnalyzer  smartcn分詞器 需要lucene依賴 且和lucene版本同步--><dependency>    <groupId>org.apache.lucene</groupId>    lucene-analyzers-smartcn    <version>7.1.0</version></dependency> <!--ik-analyzer 中文分詞器--><dependency>    <groupId>cn.bestwu</groupId>    ik-analyzers    <version>5.1.0</version></dependency> <!--MMSeg4j 分詞器--><dependency>    <groupId>com.chenlb.mmseg4j</groupId>    mmseg4j-solr    <version>2.4.0</version>    <exclusions>        <exclusion>            <groupId>org.apache.solr</groupId>            solr-core        </exclusion>    </exclusions></dependency>  

配置 lucene

private Directory directory; private IndexReader indexReader; private IndexSearcher indexSearcher; @Beforepublic void setUp() throws IOException {    //索引存放的位置,設置在當前目錄中    directory = FSDirectory.open(Paths.get("indexDir/"));     //創建索引的讀取器    indexReader = DirectoryReader.open(directory);     //創建一個索引的查找器,來檢索索引庫    indexSearcher = new IndexSearcher(indexReader);} @Afterpublic void tearDown() throws Exception {    indexReader.close();} ** * 執行查詢,並列印查詢到的記錄數 * * @param query * @throws IOException */public void executeQuery(Query query) throws IOException {     TopDocs topDocs = indexSearcher.search(query, 100);     //列印查詢到的記錄數    System.out.println("總共查詢到" + topDocs.totalHits + "個文檔");    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {         //取得對應的文檔對象        Document document = indexSearcher.doc(scoreDoc.doc);        System.out.println("id:" + document.get("id"));        System.out.println("title:" + document.get("title"));        System.out.println("content:" + document.get("content"));    }} /** * 分詞列印 * * @param analyzer * @param text * @throws IOException */public void printAnalyzerDoc(Analyzer analyzer, String text) throws IOException {     TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(text));    CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);    try {        tokenStream.reset();        while (tokenStream.incrementToken()) {            System.out.println(charTermAttribute.toString());        }        tokenStream.end();    } finally {        tokenStream.close();        analyzer.close();    }}  

創建索引

@Testpublic void indexWriterTest() throws IOException {    long start = System.currentTimeMillis();     //索引存放的位置,設置在當前目錄中    Directory directory = FSDirectory.open(Paths.get("indexDir/"));     //在 6.6 以上版本中 version 不再是必要的,並且,存在無參構造方法,可以直接使用默認的 StandardAnalyzer 分詞器。    Version version = Version.LUCENE_7_1_0;     //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = new IKAnalyzer();//中文分詞     //創建索引寫入配置    IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);     //創建索引寫入對象    IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);     //創建Document對象,存儲索引     Document doc = new Document();     int id = 1;     //將欄位加入到doc中    doc.add(new IntPoint("id", id));    doc.add(new StringField("title", "Spark", Field.Store.YES));    doc.add(new TextField("content", "Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎", Field.Store.YES));    doc.add(new StoredField("id", id));     //將doc對象保存到索引庫中    indexWriter.addDocument(doc);     indexWriter.commit();    //關閉流    indexWriter.close();     long end = System.currentTimeMillis();    System.out.println("索引花費了" + (end - start) + " 毫秒");}  

響應

17:58:14.655 [main] DEBUG org.wltea.analyzer.dic.Dictionary - 載入擴展詞典:ext.dic17:58:14.660 [main] DEBUG org.wltea.analyzer.dic.Dictionary - 載入擴展停止詞典:stopword.dic索引花費了879 毫秒  

刪除文檔

@Testpublic void deleteDocumentsTest() throws IOException {    //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = new IKAnalyzer();//中文分詞     //創建索引寫入配置    IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);     //創建索引寫入對象    IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);     // 刪除title中含有關鍵詞「Spark」的文檔    long count = indexWriter.deleteDocuments(new Term("title", "Spark"));     //  除此之外IndexWriter還提供了以下方法:    // DeleteDocuments(Query query):根據Query條件來刪除單個或多個Document    // DeleteDocuments(Query[] queries):根據Query條件來刪除單個或多個Document    // DeleteDocuments(Term term):根據Term來刪除單個或多個Document    // DeleteDocuments(Term[] terms):根據Term來刪除單個或多個Document    // DeleteAll():刪除所有的Document     //使用IndexWriter進行Document刪除操作時,文檔並不會立即被刪除,而是把這個刪除動作快取起來,當IndexWriter.Commit()或IndexWriter.Close()時,刪除操作才會被真正執行。     indexWriter.commit();    indexWriter.close();     System.out.println("刪除完成:" + count);}  

響應

刪除完成:1  

更新文檔

/** * 測試更新 * 實際上就是刪除後新增一條 * * @throws IOException */@Testpublic void updateDocumentTest() throws IOException {    //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = new IKAnalyzer();//中文分詞     //創建索引寫入配置    IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);     //創建索引寫入對象    IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);     Document doc = new Document();     int id = 1;     doc.add(new IntPoint("id", id));    doc.add(new StringField("title", "Spark", Field.Store.YES));    doc.add(new TextField("content", "Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎", Field.Store.YES));    doc.add(new StoredField("id", id));     long count = indexWriter.updateDocument(new Term("id", "1"), doc);    System.out.println("更新文檔:" + count);    indexWriter.close();}  

響應

更新文檔:1  

按詞條搜索

/** * 按詞條搜索 * <p> * TermQuery是最簡單、也是最常用的Query。TermQuery可以理解成為「詞條搜索」, * 在搜索引擎中最基本的搜索就是在索引中搜索某一詞條,而TermQuery就是用來完成這項工作的。 * 在Lucene中詞條是最基本的搜索單位,從本質上來講一個詞條其實就是一個名/值對。 * 只不過這個「名」是欄位名,而「值」則表示欄位中所包含的某個關鍵字。 * * @throws IOException */@Testpublic void termQueryTest() throws IOException {     String searchField = "title";    //這是一個條件查詢的api,用於添加條件    TermQuery query = new TermQuery(new Term(searchField, "Spark"));     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

多條件查詢

/** * 多條件查詢 * * BooleanQuery也是實際開發過程中經常使用的一種Query。 * 它其實是一個組合的Query,在使用時可以把各種Query對象添加進去並標明它們之間的邏輯關係。 * BooleanQuery本身來講是一個布爾子句的容器,它提供了專門的API方法往其中添加子句, * 並標明它們之間的關係,以下程式碼為BooleanQuery提供的用於添加子句的API介面: * * @throws IOException */@Testpublic void BooleanQueryTest() throws IOException {     String searchField1 = "title";    String searchField2 = "content";    Query query1 = new TermQuery(new Term(searchField1, "Spark"));    Query query2 = new TermQuery(new Term(searchField2, "Apache"));    BooleanQuery.Builder builder = new BooleanQuery.Builder();     // BooleanClause用於表示布爾查詢子句關係的類,    // 包 括:    // BooleanClause.Occur.MUST,    // BooleanClause.Occur.MUST_NOT,    // BooleanClause.Occur.SHOULD。    // 必須包含,不能包含,可以包含三種.有以下6種組合:    //    // 1.MUST和MUST:取得連個查詢子句的交集。    // 2.MUST和MUST_NOT:表示查詢結果中不能包含MUST_NOT所對應得查詢子句的檢索結果。    // 3.SHOULD與MUST_NOT:連用時,功能同MUST和MUST_NOT。    // 4.SHOULD與MUST連用時,結果為MUST子句的檢索結果,但是SHOULD可影響排序。    // 5.SHOULD與SHOULD:表示「或」關係,最終檢索結果為所有檢索子句的並集。    // 6.MUST_NOT和MUST_NOT:無意義,檢索無結果。     builder.add(query1, BooleanClause.Occur.SHOULD);    builder.add(query2, BooleanClause.Occur.SHOULD);     BooleanQuery query = builder.build();     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

匹配前綴

/** * 匹配前綴 * <p> * PrefixQuery用於匹配其索引開始以指定的字元串的文檔。就是文檔中存在xxx% * <p> * * @throws IOException */@Testpublic void prefixQueryTest() throws IOException {    String searchField = "title";    Term term = new Term(searchField, "Spar");    Query query = new PrefixQuery(term);     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

短語搜索

/** * 短語搜索 * <p> * 所謂PhraseQuery,就是通過短語來檢索,比如我想查「big car」這個短語, * 那麼如果待匹配的document的指定項里包含了"big car"這個短語, * 這個document就算匹配成功。可如果待匹配的句子里包含的是「big black car」, * 那麼就無法匹配成功了,如果也想讓這個匹配,就需要設定slop, * 先給出slop的概念:slop是指兩個項的位置之間允許的最大間隔距離 * * @throws IOException */@Testpublic void phraseQueryTest() throws IOException {     String searchField = "content";    String query1 = "apache";    String query2 = "spark";     PhraseQuery.Builder builder = new PhraseQuery.Builder();    builder.add(new Term(searchField, query1));    builder.add(new Term(searchField, query2));    builder.setSlop(0);    PhraseQuery phraseQuery = builder.build();     //執行查詢,並列印查詢到的記錄數    executeQuery(phraseQuery);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

相近詞語搜索

/** * 相近詞語搜索 * <p> * FuzzyQuery是一種模糊查詢,它可以簡單地識別兩個相近的詞語。 * * @throws IOException */@Testpublic void fuzzyQueryTest() throws IOException {     String searchField = "content";    Term t = new Term(searchField, "大規模");    Query query = new FuzzyQuery(t);     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

通配符搜索

/** * 通配符搜索 * <p> * Lucene也提供了通配符的查詢,這就是WildcardQuery。 * 通配符「?」代表1個字元,而「*」則代表0至多個字元。 * * @throws IOException */@Testpublic void wildcardQueryTest() throws IOException {    String searchField = "content";    Term term = new Term(searchField, "大*規模");    Query query = new WildcardQuery(term);     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

分詞查詢

/** * 分詞查詢 * * @throws IOException * @throws ParseException */@Testpublic void queryParserTest() throws IOException, ParseException {    //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = new IKAnalyzer();//中文分詞     String searchField = "content";     //指定搜索欄位和分析器    QueryParser parser = new QueryParser(searchField, analyzer);     //用戶輸入內容    Query query = parser.parse("計算引擎");     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

多個 Field 分詞查詢

/** * 多個 Field 分詞查詢 * * @throws IOException * @throws ParseException */@Testpublic void multiFieldQueryParserTest() throws IOException, ParseException {    //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = new IKAnalyzer();//中文分詞     String[] filedStr = new String[]{"title", "content"};     //指定搜索欄位和分析器    QueryParser queryParser = new MultiFieldQueryParser(filedStr, analyzer);     //用戶輸入內容    Query query = queryParser.parse("Spark");     //執行查詢,並列印查詢到的記錄數    executeQuery(query);}  

響應

總共查詢到1個文檔id:1title:Sparkcontent:Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

中文分詞器

/** * IKAnalyzer  中文分詞器 * SmartChineseAnalyzer  smartcn分詞器 需要lucene依賴 且和lucene版本同步 * * @throws IOException */@Testpublic void AnalyzerTest() throws IOException {    //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = null;    String text = "Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎";     analyzer = new IKAnalyzer();//IKAnalyzer 中文分詞    printAnalyzerDoc(analyzer, text);    System.out.println();     analyzer = new ComplexAnalyzer();//MMSeg4j 中文分詞    printAnalyzerDoc(analyzer, text);    System.out.println();     analyzer = new SmartChineseAnalyzer();//Lucene 中文分詞器    printAnalyzerDoc(analyzer, text);}  

三種分詞響應

apachespark專為大規模規模模數數據處理數據處理而設設計快速通用計算引擎  
apachespark是專為大規模數據處理而設計的快速通用的計算引擎  
apachspark是專為大規模數據處理而設計的快速通用的計算引擎  

高亮處理

/** * 高亮處理 * * @throws IOException */@Testpublic void HighlighterTest() throws IOException, ParseException, InvalidTokenOffsetsException {    //Analyzer analyzer = new StandardAnalyzer(); // 標準分詞器,適用於英文    //Analyzer analyzer = new SmartChineseAnalyzer();//中文分詞    //Analyzer analyzer = new ComplexAnalyzer();//中文分詞    //Analyzer analyzer = new IKAnalyzer();//中文分詞     Analyzer analyzer = new IKAnalyzer();//中文分詞     String searchField = "content";    String text = "Apache Spark 大規模數據處理";     //指定搜索欄位和分析器    QueryParser parser = new QueryParser(searchField, analyzer);     //用戶輸入內容    Query query = parser.parse(text);     TopDocs topDocs = indexSearcher.search(query, 100);     // 關鍵字高亮顯示的html標籤,需要導入lucene-highlighter-xxx.jar    SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("", "");    Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));     for (ScoreDoc scoreDoc : topDocs.scoreDocs) {         //取得對應的文檔對象        Document document = indexSearcher.doc(scoreDoc.doc);         // 內容增加高亮顯示        TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(document.get("content")));        String content = highlighter.getBestFragment(tokenStream, document.get("content"));         System.out.println(content);    } }  

響應

Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎!  

程式碼我已放到 Github ,導入spring-boot-lucene-demo 項目

github spring-boot-lucene-demo

  • 作者:Peng Lei
  • 出處:Segment Fault PengLei `Blog 專欄
  • 版權歸作者所有,轉載請註明出處

Tags: