構(gòu)建索引過程
文檔是Lucene索引和被搜索的最小單位效诅,一個文檔包含一個或者多個域彼绷,而域則包含了真正 被搜索 的內(nèi)容余爆,每個域都有一個標(biāo)識名稱(如標(biāo)題,描述)對應(yīng)一個值(比如 標(biāo)題:lucene)
建立了文檔和域以后硕舆,就可以調(diào)用IndexWriter的addDocument方法
倒排索引
其實是表示 秽荞,哪些文檔包含單詞X ,而不是這個文檔包含哪些單詞
基礎(chǔ)入門
既然是索引抚官,那么肯定需要有地方存儲他們扬跋,Lucene提供了多種存儲索引的方式,Lucene在文件系統(tǒng)中存儲索引的最基本的抽象實現(xiàn)類是BaseDirectory凌节,其中最常使用的是FSDirectory 和 RAMDirectory钦听,前者是主要用來存儲到文件到文件系統(tǒng)(其中有幾個子類,實現(xiàn)不同的存儲策略刊咳,包括Nio,內(nèi)存映射等等)彪见,后者是直接存儲到內(nèi)存中儡司,適合小型應(yīng)用或者實驗學(xué)習(xí)性質(zhì)的Demo娱挨,如果數(shù)據(jù)量較大的話,內(nèi)存會吃不住的(官方文檔表示捕犬,20G原始數(shù)據(jù)跷坝,大概需要4-6GB的索引結(jié)構(gòu)數(shù)據(jù))
使用如下:
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,可以對索引進(jìn)行寫操作碉碉,IndexWriter負(fù)責(zé)創(chuàng)建索引和打開已經(jīng)存在的索引柴钻,向其中更新和刪除索引,不能夠用作讀垢粮,如果開辟內(nèi)存空間贴届,則需要Directory來完成,因為底層的IO抽象在Directory中,不同的場景需要使用不同的Directory實現(xiàn)毫蚓。同時占键,也要對IndexWriter進(jìn)行一些配置,比如設(shè)定分析器為 StandardAnalyzer ,設(shè)定文件操作模式等等元潘,這需要使用IndexWriterConfig 類(具體操作文檔 http://lucene.apache.org/core/6_4_0/core/org/apache/lucene/index/IndexWriterConfig.html)
索引寫入對象準(zhǔn)備好以后畔乙,就可以開始構(gòu)建索引了,翩概,要構(gòu)建索引牲距,首先要了解索引的相關(guān)概念,在Lucene中索引相關(guān)的概念如下:
Lucene概念
傳統(tǒng)數(shù)據(jù)庫概念
備注
IndexSearcher
table钥庇,讀取的句柄
IndexWriter
table牍鞠,寫入的句柄
Directory
底層IO寫入的句柄
描述了Lucene索引的存放位置,它的子類負(fù)責(zé)具體指定索引的存儲路徑
派生出
FSDirectory评姨,RAMDirectory等
DirectoryReader
底層IO讀取句柄 讀取Directory
Document
一條記錄
代表一些域(Field)的集合皮服,你可以將Document對象理解為虛擬文檔-例如Web頁面、E-mail信息或者文本文件
Field
每個字段
分為可被索引的参咙,可切分的龄广,不可被切分的,不可被索引的幾種組合類型
Hits
RecoreSet
結(jié)果集
Analyzer
分析器
負(fù)責(zé)文本分析蕴侧,從被索引文本文件中提取出語匯單元择同。對于文本分析器Analyzer,需要注意一點净宵,就是使用哪種Analyzer進(jìn)行索引創(chuàng)建敲才,查詢的時候也要使用哪種Analyzer查詢,否則查詢結(jié)果不正確择葡。
FieldType
域類型紧武,每一個Field存儲類型
描述了Field的各種屬性,在不使用某種具體的Field類型(例如StringField敏储,TextField)時需要用到此類
也就是說一條索引的記錄就是一個document浇雹,比如某篇文章裕膀,它的全部信息就可以看作為一個document,而其中的作者,標(biāo)題祝旷,編號结啼,摘要就可以看做是各個Field贞滨,如果需要寫索引拙吉,就要通過IndexWriter實例化的對象去操作。如果需要搜索結(jié)果缆蝉,就需要 IndexSearcher 實例宇葱,搜索后得到hits結(jié)果集瘦真。
創(chuàng)建索引
創(chuàng)建索引具體實現(xiàn)的代碼如下:
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()
}
//測試調(diào)用
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理解為數(shù)據(jù)庫中的一行吗氏,而Field是其中的字段,操作如下:
var documentation = new Document();
documentation.add(new Field("goods_id",single("goods_id"),fieldType))
document添加的時候接受一個實現(xiàn) IndexableField 接口對象雷逆,F(xiàn)ield 實現(xiàn)了 IndexableField弦讽,所以直接創(chuàng)建一個Field即可,完成document操作后膀哲,將其寫入到索引中往产,并提交:
indexWriter.addDocument(documentation)
indexWriter.commit()
這樣就是一個完整的索引建立過程
域類型
首先先創(chuàng)建了FieldType,定義Field的類型某宪,這里定義為以為存儲和Tokenized仿村。
這些字段類型可以由幾個成員函數(shù)調(diào)用來配置
1.Stored表示要存儲到索引中,要配置為Stored兴喂,調(diào)用 setStored(boolean value) 蔼囊,通常我們只存儲一些短小精悍且必要的字段,像標(biāo)題衣迷,id畏鼓,url這種,而文章正文這樣的大篇幅數(shù)據(jù)一般不存儲與索引壶谒。
- 如果要這個Field的值在進(jìn)入之前先通過分析器過濾調(diào)用setTokenized(boolean value)
3.設(shè)定索引的類型使用 setIndexOptions(IndexOptions value) 云矫,需要傳入索引參數(shù),是一個枚舉汗菜,有這些值如下
DOCS
只有文檔會被索引让禀,詞頻和位置都會被省略
DOCS_AND_FREQS
文檔和詞頻被索引,位置省略
DOCS_AND_FREQS_AND_POSITIONS
文檔 詞頻 位置都被索引
DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS
除了文檔和詞頻位置還有偏移量也會被索引
NONE
不索引
有些字段我們需要在查詢的時候返回陨界,但不希望它進(jìn)入索引影響查詢效率巡揍,可以用setIndexOptions設(shè)為False,同時有些詞我們只需要對其進(jìn)行過濾即可菌瘪,比如權(quán)限和時間過濾腮敌,這種值我們不太需要記錄其出現(xiàn)的頻率(詞頻)和位置(偏移量),所以只需要DOCS級別即可麻车,通常對于要索引的字段我們都設(shè)置為DOCS_AND_FREQS_AND_POSITIONS.
匯總起來缀皱,操作代碼如下:
val fieldType = new FieldType()
fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)
fieldType.setStored(true)
fieldType.setTokenized(true)
設(shè)定好FieldType后,就可以用類型去創(chuàng)建域动猬,代碼如下
var f1 = new Field("goods_id",single("goods_id"),fieldType)
注意,老版本的Lucene是通過存儲選項+索引選項+項向量組合起來決定一個Field的性質(zhì)表箭,調(diào)用起來比較復(fù)雜赁咙,新版使用一個新的類FieldType并使用一些方法來設(shè)定,比較清晰和方便
多值域問題
在某些場景下,我們需要一個作用域有多個值彼水,比如作者信息崔拥,很多時候作者不止一個人,需要向一個Field里面寫入多個值凤覆,這種情況只需要直接向一個Document寫入多個相同名字相同但是值不同的Field即可链瓦,至于這些Field該如何定義優(yōu)先級,可以在分析的時候進(jìn)行干預(yù)(見后文)盯桦。
Lucene處理Field的時候還設(shè)計了針對多種IO的構(gòu)造函數(shù)慈俯,除了string外,TokenStream/Reader還可以針對占用內(nèi)存空間較大的Field進(jìn)行分析拥峦,避免一次讀入占用內(nèi)存
加權(quán)
加權(quán)操作是認(rèn)為的對結(jié)果進(jìn)行干預(yù)贴膘,可以在索引期間完成,也可以在搜索期間完成略号,這里著重描述如何在索引期間加權(quán)刑峡。
調(diào)用加權(quán)的操作在3.0+版本上可以在一個Document上面進(jìn)行,但在6.0+版本上只在文檔中找到了基于Field的操作玄柠,如下
var goods_id = new Field("goods_id",single("goods_id"),fieldType)
goods_id.setBoost(1.5F)
加權(quán)值高的Field會更比較低的更加優(yōu)先被搜索到突梦,Lucene通過查詢語句的匹配程度來對搜索結(jié)果進(jìn)行排名,每個匹配的文檔都有一個評分羽利,加權(quán)數(shù)是評分的一個重要因素阳似。
Lucene會基于域的語匯單元來計算加權(quán)值(更短的域有較高的加權(quán),這里隱含了如果越短铐伴,則優(yōu)先級可能越高)撮奏,這些加權(quán)會被合并量化為一個單一的字節(jié)值(加權(quán)基準(zhǔn)Norms),并且存儲当宴,在搜索的時候被加載到內(nèi)存畜吊,還原為浮點數(shù),然后用于計算評分户矢。Norms可以在搜索時候用IndexReader.setNorm進(jìn)行修改.
對于域比較多的文檔來說玲献,加載norms信息會占用大量內(nèi)存空間,可以在FieldType進(jìn)行設(shè)定梯浪,關(guān)閉norms相關(guān)操作捌年。
fieldType.setOmitNorms(false)
索引非字符串類型
很多場景下都需要索引數(shù)字類型,比如價格挂洛,時間等礼预,一種情況是數(shù)字包含在文字中比如‘我買50塊錢的東西’,50要被索引虏劲,需要選擇一個不丟棄數(shù)字的分析器托酸,比如StandardAnalyzer(而SimpleAnalyzer和StopAnalyzer是反例褒颈,他們會剔除數(shù)字),這樣就可以達(dá)到想要的目標(biāo)励堡。另一種情況是我們直接就想索引一個數(shù)字谷丸,這就需要使用IntPoint等數(shù)據(jù)類型,他們是Field派生出的子類应结,標(biāo)準(zhǔn)文檔上給出了這些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)//也可以通過這種方式進(jìn)行修改
由于這些類型都是繼承Field刨疼,所以執(zhí)行Field相關(guān)的方法,同上鹅龄,我們也可以對一個域添加多個值揩慕,在搜索的時候?qū)@些值的處理方式是or關(guān)系,且排序是不確定的砾层。int也可以處理時間漩绵,將時間轉(zhuǎn)換為Int即可(更精確的時間可以使用LongPoint)。
Field截取
對于一些尺寸未知的文件肛炮,我們需要進(jìn)行截取止吐,從而控制內(nèi)存和硬盤的使用量,對一個IndexWriter調(diào)用API來實現(xiàn)setMaxFieldLength todo找到文檔
實時搜索
很多時候修改了索引以后需要馬上看到效果侨糟,但從新New一個IndexReader會非常的耗時碍扔,3.0+版本讓我們使用indexWriter . getReader(),但目前這個接口已經(jīng)被標(biāo)記為廢棄
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即可。
索引優(yōu)化
當(dāng)多次對一個索引進(jìn)行寫操作時秕重,會產(chǎn)生很多獨立的段不同,當(dāng)搜索時,lucene必須單獨搜索每個段溶耘,然后合并段的搜索結(jié)果二拐,當(dāng)處理大量數(shù)據(jù)時,需要盡量合并這些段凳兵,早起indexWriter提供了一些api百新,不過現(xiàn)在已經(jīng)廢棄,參考https://lucene.apache.org/core/3_5_0/api/core/org/apache/lucene/index/IndexWriter.html#optimize()庐扫,目前indexWriter/IndexReader會自動進(jìn)行優(yōu)化饭望。
Directory子類介紹
前面我們簡單介紹了Directory,這里深入描述下幾種Directory子類的差別和使用范圍:
- SimpleFSDirectory: 直接使用java.io操作文件系統(tǒng)形庭,不能很好的支持多線程铅辞,如要要做到就必須使用外部加鎖,并且不支持按位置讀取萨醒。
2.NIOFSDirectory:使用java nio進(jìn)行文件操作斟珊,異步進(jìn)行,可以很好的支持多線程讀取
3.MMapDirectory:內(nèi)存映射io進(jìn)行文件訪問验靡,不需要用鎖機(jī)制就可以在多線程下很好的運行倍宾,但由于內(nèi)存映射IO消耗的地址空間和索引尺寸是相等雏节,所以在32位jvm上比較雞肋(也就是說索引最多只能4G)胜嗓,推薦64位使用高职,不過由于JVM沒有取消映射關(guān)系的機(jī)制,所以只有在垃圾回收的時候辞州,才會釋放內(nèi)存空間和文件描述符怔锌,會造成一些困擾。
4.RAMDirectory:直接存在內(nèi)存变过,實驗用埃元。
作用用戶調(diào)用我們不用太糾結(jié)選擇,直接使用FSDirectory即可媚狰,他會根據(jù)當(dāng)前環(huán)境來選擇最適合的方式岛杀,如果需要自定,可以考慮自己實例化相關(guān)的類崭孤。
并發(fā)
Lucene在并發(fā)方面有以下的特性:
1.任意只讀的IndexReader可以同時打開一個索引类嗤,無論這些Reader是否在一臺機(jī)器上。最好的辦法是一個jvm內(nèi)只有一個Reader辨宠,多個線程共享進(jìn)行搜索
2.對于一個索引一次只能打開一個Writer遗锣,Lucene提供一個文件鎖來保障這一特性。
3.IndexReader可以在Writer進(jìn)行寫入的時候打開嗤形,他可以看到IndexWriter提交之前的數(shù)據(jù)精偿。
4.IndexReader是線程安全的
可以通過IndexWriter.isLocked來判斷是否有鎖,也有一些方法來自定義鎖的實現(xiàn)赋兵。
參考:
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