Lucene筆記1-建立索引

構建索引過程

文檔是Lucene索引和被搜索的最小單位,一個文檔包含一個或者多個域,而域則包含了真正 被搜索 的內容,每個域都有一個標識名稱(如標題,描述)對應一個值(比如 標題:lucene)

建立了文檔和域以后,就可以調用IndexWriter的addDocument方法

倒排索引

其實是表示 ,哪些文檔包含單詞X ,而不是這個文檔包含哪些單詞

基礎入門

既然是索引,那么肯定需要有地方存儲他們,Lucene提供了多種存儲索引的方式,Lucene在文件系統中存儲索引的最基本的抽象實現類是BaseDirectory,其中最常使用的是FSDirectory 和 RAMDirectory,前者是主要用來存儲到文件到文件系統(其中有幾個子類,實現不同的存儲策略,包括Nio,內存映射等等),后者是直接存儲到內存中,適合小型應用或者實驗學習性質的Demo,如果數據量較大的話,內存會吃不住的(官方文檔表示,20G原始數據,大概需要4-6GB的索引結構數據)

使用如下:

valindexConfig:IndexWriterConfig  = new IndexWriterConfig(new StandardAnalyzer());

indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)

//  indexConfig.setInfoStream(System.out)

val directory:Directory = FSDirectory.open(Paths.get(indexPath))

val indexWriter:IndexWriter = new IndexWriter(directory,indexConfig)

這樣就成功實例化了一個IndexWriter,可以對索引進行寫操作,IndexWriter負責創建索引和打開已經存在的索引,向其中更新和刪除索引,不能夠用作讀,如果開辟內存空間,則需要Directory來完成,因為底層的IO抽象在Directory中,不同的場景需要使用不同的Directory實現。同時,也要對IndexWriter進行一些配置,比如設定分析器為 StandardAnalyzer ,設定文件操作模式等等,這需要使用IndexWriterConfig 類(具體操作文檔 http://lucene.apache.org/core/6_4_0/core/org/apache/lucene/index/IndexWriterConfig.html

索引寫入對象準備好以后,就可以開始構建索引了,,要構建索引,首先要了解索引的相關概念,在Lucene中索引相關的概念如下:

Lucene概念

傳統數據庫概念

備注

IndexSearcher

table,讀取的句柄

IndexWriter

table,寫入的句柄

Directory

底層IO寫入的句柄

描述了Lucene索引的存放位置,它的子類負責具體指定索引的存儲路徑

派生出

FSDirectory,RAMDirectory等

DirectoryReader

底層IO讀取句柄 讀取Directory

Document

一條記錄

代表一些域(Field)的集合,你可以將Document對象理解為虛擬文檔-例如Web頁面、E-mail信息或者文本文件

Field

每個字段

分為可被索引的,可切分的,不可被切分的,不可被索引的幾種組合類型

Hits

RecoreSet

結果集

Analyzer

分析器

負責文本分析,從被索引文本文件中提取出語匯單元。對于文本分析器Analyzer,需要注意一點,就是使用哪種Analyzer進行索引創建,查詢的時候也要使用哪種Analyzer查詢,否則查詢結果不正確。

FieldType

域類型,每一個Field存儲類型

描述了Field的各種屬性,在不使用某種具體的Field類型(例如StringField,TextField)時需要用到此類

也就是說一條索引的記錄就是一個document,比如某篇文章,它的全部信息就可以看作為一個document,而其中的作者,標題,編號,摘要就可以看做是各個Field,如果需要寫索引,就要通過IndexWriter實例化的對象去操作。如果需要搜索結果,就需要 IndexSearcher 實例,搜索后得到hits結果集。

創建索引

創建索引具體實現的代碼如下:

def createIndex(mapList: Array[Map[String,String]]): Unit ={

val fieldType = new FieldType()

fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)

fieldType.setStored(true)

fieldType.setTokenized(true)

for(a <- 0 until mapList.length){

var documentation = new Document();

var single = mapList(a)

documentation.add(new Field("goods_id",single("goods_id"),fieldType))

documentation.add(new Field("goods_name",single("goods_name"),fieldType))

documentation.add(new Field("goods_price",single("goods_price"),fieldType))

documentation.add(new Field("goods_seller",single("goods_seller"),fieldType))

indexWriter.addDocument(documentation)

}

indexWriter.commit()

}

//測試調用

deftestCreateIndex(): Unit ={

val index = new Index("./index_store")

val arrayList = Array(

Map("goods_id" -> "sa2a", "goods_name" -> "xs", "goods_price" -> "19.22", "goods_seller" -> "A&TT")

)

index.createIndex(arrayList)

index.close()

}

一個域是屬于一個document的,一個document可以包含多個域,可以把document理解為數據庫中的一行,而Field是其中的字段,操作如下:

var documentation = new Document();

documentation.add(new Field("goods_id",single("goods_id"),fieldType))

document添加的時候接受一個實現 IndexableField 接口對象,Field 實現了 IndexableField,所以直接創建一個Field即可,完成document操作后,將其寫入到索引中,并提交:

indexWriter.addDocument(documentation)

indexWriter.commit()

這樣就是一個完整的索引建立過程

域類型

首先先創建了FieldType,定義Field的類型,這里定義為以為存儲和Tokenized。

這些字段類型可以由幾個成員函數調用來配置

1.Stored表示要存儲到索引中,要配置為Stored,調用 setStored(boolean value) ,通常我們只存儲一些短小精悍且必要的字段,像標題,id,url這種,而文章正文這樣的大篇幅數據一般不存儲與索引。

  1. 如果要這個Field的值在進入之前先通過分析器過濾調用setTokenized(boolean value)

3.設定索引的類型使用 setIndexOptions(IndexOptions value) ,需要傳入索引參數,是一個枚舉,有這些值如下

DOCS

只有文檔會被索引,詞頻和位置都會被省略

DOCS_AND_FREQS

文檔和詞頻被索引,位置省略

DOCS_AND_FREQS_AND_POSITIONS

文檔 詞頻 位置都被索引

DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS

除了文檔和詞頻位置還有偏移量也會被索引

NONE

不索引

有些字段我們需要在查詢的時候返回,但不希望它進入索引影響查詢效率,可以用setIndexOptions設為False,同時有些詞我們只需要對其進行過濾即可,比如權限和時間過濾,這種值我們不太需要記錄其出現的頻率(詞頻)和位置(偏移量),所以只需要DOCS級別即可,通常對于要索引的字段我們都設置為DOCS_AND_FREQS_AND_POSITIONS.

匯總起來,操作代碼如下:

val fieldType = new FieldType()

fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)

fieldType.setStored(true)

fieldType.setTokenized(true)

設定好FieldType后,就可以用類型去創建域,代碼如下

var f1 = new Field("goods_id",single("goods_id"),fieldType)

注意,老版本的Lucene是通過存儲選項+索引選項+項向量組合起來決定一個Field的性質,調用起來比較復雜,新版使用一個新的類FieldType并使用一些方法來設定,比較清晰和方便

多值域問題

在某些場景下,我們需要一個作用域有多個值,比如作者信息,很多時候作者不止一個人,需要向一個Field里面寫入多個值,這種情況只需要直接向一個Document寫入多個相同名字相同但是值不同的Field即可,至于這些Field該如何定義優先級,可以在分析的時候進行干預(見后文)。

Lucene處理Field的時候還設計了針對多種IO的構造函數,除了string外,TokenStream/Reader還可以針對占用內存空間較大的Field進行分析,避免一次讀入占用內存

加權

加權操作是認為的對結果進行干預,可以在索引期間完成,也可以在搜索期間完成,這里著重描述如何在索引期間加權。

調用加權的操作在3.0+版本上可以在一個Document上面進行,但在6.0+版本上只在文檔中找到了基于Field的操作,如下

var goods_id = new Field("goods_id",single("goods_id"),fieldType)

goods_id.setBoost(1.5F)

加權值高的Field會更比較低的更加優先被搜索到,Lucene通過查詢語句的匹配程度來對搜索結果進行排名,每個匹配的文檔都有一個評分,加權數是評分的一個重要因素。

Lucene會基于域的語匯單元來計算加權值(更短的域有較高的加權,這里隱含了如果越短,則優先級可能越高),這些加權會被合并量化為一個單一的字節值(加權基準Norms),并且存儲,在搜索的時候被加載到內存,還原為浮點數,然后用于計算評分。Norms可以在搜索時候用IndexReader.setNorm進行修改.

對于域比較多的文檔來說,加載norms信息會占用大量內存空間,可以在FieldType進行設定,關閉norms相關操作。

fieldType.setOmitNorms(false)

索引非字符串類型

很多場景下都需要索引數字類型,比如價格,時間等,一種情況是數字包含在文字中比如‘我買50塊錢的東西’,50要被索引,需要選擇一個不丟棄數字的分析器,比如StandardAnalyzer(而SimpleAnalyzer和StopAnalyzer是反例,他們會剔除數字),這樣就可以達到想要的目標。另一種情況是我們直接就想索引一個數字,這就需要使用IntPoint等數據類型,他們是Field派生出的子類,標準文檔上給出了這些Field:

BinaryDocValuesField, BinaryPoint, DoublePoint, FloatPoint, IntPoint, LegacyDoubleField, LegacyFloatField, LegacyIntField, LegacyLongField, LongPoint, NumericDocValuesField, SortedDocValuesField, SortedNumericDocValuesField, SortedSetDocValuesField, StoredField, StringField, TextField

var price = new IntPoint("price",15,fieldType)

price.setIntValues(15)//也可以通過這種方式進行修改

由于這些類型都是繼承Field,所以執行Field相關的方法,同上,我們也可以對一個域添加多個值,在搜索的時候對這些值的處理方式是or關系,且排序是不確定的。int也可以處理時間,將時間轉換為Int即可(更精確的時間可以使用LongPoint)。

Field截取

對于一些尺寸未知的文件,我們需要進行截取,從而控制內存和硬盤的使用量,對一個IndexWriter調用API來實現setMaxFieldLength todo找到文檔

實時搜索

很多時候修改了索引以后需要馬上看到效果,但從新New一個IndexReader會非常的耗時,3.0+版本讓我們使用indexWriter . getReader(),但目前這個接口已經被標記為廢棄

getReader(int termInfosIndexDivisor)

Deprecated. Please use IndexReader.open(IndexWriter,boolean) instead. Furthermore, this method cannot guarantee the reader (and its sub-readers) will be opened with the termInfosIndexDivisor setting because some of them may have already been opened according to IndexWriterConfig.setReaderTermsIndexDivisor(int). You should set the requested termInfosIndexDivisor through IndexWriterConfig.setReaderTermsIndexDivisor(int) and use getReader().

我們直接使用IndexReader從新打開IndexWriter即可。

索引優化

當多次對一個索引進行寫操作時,會產生很多獨立的段,當搜索時,lucene必須單獨搜索每個段,然后合并段的搜索結果,當處理大量數據時,需要盡量合并這些段,早起indexWriter提供了一些api,不過現在已經廢棄,參考https://lucene.apache.org/core/3_5_0/api/core/org/apache/lucene/index/IndexWriter.html#optimize(),目前indexWriter/IndexReader會自動進行優化。

Directory子類介紹

前面我們簡單介紹了Directory,這里深入描述下幾種Directory子類的差別和使用范圍:

  1. SimpleFSDirectory: 直接使用java.io操作文件系統,不能很好的支持多線程,如要要做到就必須使用外部加鎖,并且不支持按位置讀取。

2.NIOFSDirectory:使用java nio進行文件操作,異步進行,可以很好的支持多線程讀取

3.MMapDirectory:內存映射io進行文件訪問,不需要用鎖機制就可以在多線程下很好的運行,但由于內存映射IO消耗的地址空間和索引尺寸是相等,所以在32位jvm上比較雞肋(也就是說索引最多只能4G),推薦64位使用,不過由于JVM沒有取消映射關系的機制,所以只有在垃圾回收的時候,才會釋放內存空間和文件描述符,會造成一些困擾。

4.RAMDirectory:直接存在內存,實驗用。

作用用戶調用我們不用太糾結選擇,直接使用FSDirectory即可,他會根據當前環境來選擇最適合的方式,如果需要自定,可以考慮自己實例化相關的類。

并發

Lucene在并發方面有以下的特性:

1.任意只讀的IndexReader可以同時打開一個索引,無論這些Reader是否在一臺機器上。最好的辦法是一個jvm內只有一個Reader,多個線程共享進行搜索

2.對于一個索引一次只能打開一個Writer,Lucene提供一個文件鎖來保障這一特性。

3.IndexReader可以在Writer進行寫入的時候打開,他可以看到IndexWriter提交之前的數據。

4.IndexReader是線程安全的

可以通過IndexWriter.isLocked來判斷是否有鎖,也有一些方法來自定義鎖的實現。

參考:

Lucene6.4文檔 http://lucene.apache.org/core/6_4_0/core/index.html

Lucene3.5文檔 https://lucene.apache.org/core/3_5_0/api/core/overview-summary.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容