摘要:介紹如何優(yōu)化數(shù)值類范圍查詢。查詢過程在中查詢是基于。在中為了查詢的這樣一個條件,會建立基于的倒排鏈。在單查詢上可能相比并沒有明顯優(yōu)勢,甚至會慢一些。所以為了支持高效的數(shù)值類或者多維度查詢,引入類。
前言
Lucene 是一個基于 Java 的全文信息檢索工具包,目前主流的搜索系統(tǒng)Elasticsearch和solr都是基于lucene的索引和搜索能力進行。想要理解搜索系統(tǒng)的實現(xiàn)原理,就需要深入lucene這一層,看看lucene是如何存儲需要檢索的數(shù)據(jù),以及如何完成高效的數(shù)據(jù)檢索。
在數(shù)據(jù)庫中因為有索引的存在,也可以支持很多高效的查詢操作。不過對比lucene,數(shù)據(jù)庫的查詢能力還是會弱很多,本文就將探索下lucene支持哪些查詢,并會重點選取幾類查詢分析lucene內(nèi)部是如何實現(xiàn)的。為了方便大家理解,我們會先簡單介紹下lucene里面的一些基本概念,然后展開lucene中的幾種數(shù)據(jù)存儲結構,理解了他們的存儲原理后就可以方便知道如何基于這些存儲結構來實現(xiàn)高效的搜索。本文重點關注是lucene如何做到傳統(tǒng)數(shù)據(jù)庫較難做到的查詢,對于分詞,打分等功能不會展開介紹。
本文具體會分以下幾部分:
介紹lucene的數(shù)據(jù)模型,細節(jié)可以參閱lucene數(shù)據(jù)模型一文。
介紹lucene中如何存儲需要搜索的term。
介紹lucene的倒排鏈的如何存儲以及如何實現(xiàn)docid的快速查找。
介紹lucene如何實現(xiàn)倒排鏈合并。
介紹lucene如何做范圍查詢和前綴匹配。
介紹lucene如何優(yōu)化數(shù)值類范圍查詢。
Lucene數(shù)據(jù)模型Lucene中包含了四種基本數(shù)據(jù)類型,分別是:
Index:索引,由很多的Document組成。
Document:由很多的Field組成,是Index和Search的最小單位。
Field:由很多的Term組成,包括Field Name和Field Value。
Term:由很多的字節(jié)組成。一般將Text類型的Field Value分詞之后的每個最小單元叫做Term。
在lucene中,讀寫路徑是分離的。寫入的時候創(chuàng)建一個IndexWriter,而讀的時候會創(chuàng)建一個IndexSearcher,
下面是一個簡單的代碼示例,如何使用lucene的IndexWriter建索引以及如何使用indexSearch進行搜索查詢。
Analyzer analyzer = new StandardAnalyzer(); // Store the index in memory: Directory directory = new RAMDirectory(); // To store an index on disk, use this instead: //Directory directory = FSDirectory.open("/tmp/testindex"); IndexWriterConfig config = new IndexWriterConfig(analyzer); IndexWriter iwriter = new IndexWriter(directory, config); Document doc = new Document(); String text = "This is the text to be indexed."; doc.add(new Field("fieldname", text, TextField.TYPE_STORED)); iwriter.addDocument(doc); iwriter.close(); // Now search the index: DirectoryReader ireader = DirectoryReader.open(directory); IndexSearcher isearcher = new IndexSearcher(ireader); // Parse a simple query that searches for "text": QueryParser parser = new QueryParser("fieldname", analyzer); Query query = parser.parse("text"); ScoreDoc[] hits = isearcher.search(query, 1000).scoreDocs; //assertEquals(1, hits.length); // Iterate through the results: for (int i = 0; i < hits.length; i++) { Document hitDoc = isearcher.doc(hits[i].doc); System.out.println(hitDoc.get("fieldname")); } ireader.close(); directory.close();
從這個示例中可以看出,lucene的讀寫有各自的操作類。本文重點關注讀邏輯,在使用IndexSearcher類的時候,需要一個DirectoryReader和QueryParser,其中DirectoryReader需要對應寫入時候的Directory實現(xiàn)。QueryParser主要用來解析你的查詢語句,例如你想查 “A and B",lucene內(nèi)部會有機制解析出是term A和term B的交集查詢。在具體執(zhí)行Search的時候指定一個最大返回的文檔數(shù)目,因為可能會有過多命中,我們可以限制單詞返回的最大文檔數(shù),以及做分頁返回。
下面會詳細介紹一個索引查詢會經(jīng)過幾步,每一步lucene分別做了哪些優(yōu)化實現(xiàn)。
Lucene 查詢過程在lucene中查詢是基于segment。每個segment可以看做是一個獨立的subindex,在建立索引的過程中,lucene會不斷的flush內(nèi)存中的數(shù)據(jù)持久化形成新的segment。多個segment也會不斷的被merge成一個大的segment,在老的segment還有查詢在讀取的時候,不會被刪除,沒有被讀取且被merge的segement會被刪除。這個過程類似于LSM數(shù)據(jù)庫的merge過程。下面我們主要看在一個segment內(nèi)部如何實現(xiàn)高效的查詢。為了方便大家理解,我們以人名字,年齡,學號為例,如何實現(xiàn)查某個名字(有重名)的列表。
在lucene中為了查詢name=XXX的這樣一個條件,會建立基于name的倒排鏈。以上面的數(shù)據(jù)為例,倒排鏈如下:
姓名
Alice | [1,2,3]
---- | --- |
Alan | [4,5]
如果我們還希望按照年齡查詢,例如想查年齡=18的列表,我們還可以建立另一個倒排鏈:
18 | [1,5]
---| --- |
20 | [2]
21 | [3,4]
在這里,Alice,Alan,18,這些都是term。所以倒排本質(zhì)上就是基于term的反向列表,方便進行屬性查找。到這里我們有個很自然的問題,如果term非常多,如何快速拿到這個倒排鏈呢?在lucene里面就引入了term dictonary的概念,也就是term的字典。term字典里我們可以按照term進行排序,那么用一個二分查找就可以定為這個term所在的地址。這樣的復雜度是logN,在term很多,內(nèi)存放不下的時候,效率還是需要進一步提升。可以用一個hashmap,當有一個term進入,hash繼續(xù)查找倒排鏈。這里hashmap的方式可以看做是term dictionary的一個index。 從lucene4開始,為了方便實現(xiàn)rangequery或者前綴,后綴等復雜的查詢語句,lucene使用FST數(shù)據(jù)結構來存儲term字典,下面就詳細介紹下FST的存儲結構。
FST
我們就用Alice和Alan這兩個單詞為例,來看下FST的構造過程。首先對所有的單詞做一下排序為“Alice”,“Alan”。
插入“Alan”
插入“Alice”
這樣你就得到了一個有向無環(huán)圖,有這樣一個數(shù)據(jù)結構,就可以很快查找某個人名是否存在。FST在單term查詢上可能相比hashmap并沒有明顯優(yōu)勢,甚至會慢一些。但是在范圍,前綴搜索以及壓縮率上都有明顯的優(yōu)勢。
在通過FST定位到倒排鏈后,有一件事情需要做,就是倒排鏈的合并。因為查詢條件可能不止一個,例如上面我們想找name="alan" and age="18"的列表。lucene是如何實現(xiàn)倒排鏈的合并呢。這里就需要看一下倒排鏈存儲的數(shù)據(jù)結構
SkipList
為了能夠快速查找docid,lucene采用了SkipList這一數(shù)據(jù)結構。SkipList有以下幾個特征:
元素排序的,對應到我們的倒排鏈,lucene是按照docid進行排序,從小到大。
跳躍有一個固定的間隔,這個是需要建立SkipList的時候指定好,例如下圖以間隔是3
SkipList的層次,這個是指整個SkipList有幾層
有了這個SkipList以后比如我們要查找docid=12,原來可能需要一個個掃原始鏈表,1,2,3,5,7,8,10,12。有了SkipList以后先訪問第一層看到是然后大于12,進入第0層走到3,8,發(fā)現(xiàn)15大于12,然后進入原鏈表的8繼續(xù)向下經(jīng)過10和12。
有了FST和SkipList的介紹以后,我們大體上可以畫一個下面的圖來說明lucene是如何實現(xiàn)整個倒排結構的:
有了這張圖,我們可以理解為什么基于lucene可以快速進行倒排鏈的查找和docid查找,下面就來看一下有了這些后如何進行倒排鏈合并返回最后的結果。
倒排合并
假如我們的查詢條件是name = “Alice”,那么按照之前的介紹,首先在term字典中定位是否存在這個term,如果存在的話進入這個term的倒排鏈,并根據(jù)參數(shù)設定返回分頁返回結果即可。這類查詢,在數(shù)據(jù)庫中使用二級索引也是可以滿足,那lucene的優(yōu)勢在哪呢。假如我們有多個條件,例如我們需要按名字或者年齡多帶帶查詢,也需要進行組合 name = "Alice" and age = "18"的查詢,那么使用傳統(tǒng)二級索引方案,你可能需要建立兩張索引表,然后分別查詢結果后進行合并,這樣如果age = 18的結果過多的話,查詢合并會很耗時。那么在lucene這兩個倒排鏈是怎么合并呢。
假如我們有下面三個倒排鏈需要進行合并。
在lucene中會采用下列順序進行合并:
在termA開始遍歷,得到第一個元素docId=1
Set currentDocId=1
在termB中 search(currentDocId) = 1 (返回大于等于currentDocId的一個doc),
因為currentDocId ==1,繼續(xù)
如果currentDocId 和返回的不相等,執(zhí)行2,然后繼續(xù)
到termC后依然符合,返回結果
currentDocId = termC的nextItem
然后繼續(xù)步驟3 依次循環(huán)。直到某個倒排鏈到末尾。
整個合并步驟我可以發(fā)現(xiàn),如果某個鏈很短,會大幅減少比對次數(shù),并且由于SkipList結構的存在,在某個倒排中定位某個docid的速度會比較快不需要一個個遍歷。可以很快的返回最終的結果。從倒排的定位,查詢,合并整個流程組成了lucene的查詢過程,和傳統(tǒng)數(shù)據(jù)庫的索引相比,lucene合并過程中的優(yōu)化減少了讀取數(shù)據(jù)的IO,倒排合并的靈活性也解決了傳統(tǒng)索引較難支持多條件查詢的問題。
BKDTree
在lucene中如果想做范圍查找,根據(jù)上面的FST模型可以看出來,需要遍歷FST找到包含這個range的一個點然后進入對應的倒排鏈,然后進行求并集操作。但是如果是數(shù)值類型,比如是浮點數(shù),那么潛在的term可能會非常多,這樣查詢起來效率會很低。所以為了支持高效的數(shù)值類或者多維度查詢,lucene引入類BKDTree。BKDTree是基于KDTree,對數(shù)據(jù)進行按照維度劃分建立一棵二叉樹確保樹兩邊節(jié)點數(shù)目平衡。在一維的場景下,KDTree就會退化成一個二叉搜索樹,在二叉搜索樹中如果我們想查找一個區(qū)間,logN的復雜度就會訪問到葉子結點得到對應的倒排鏈。如下圖所示:
如果是多維,kdtree的建立流程會發(fā)生一些變化。
比如我們以二維為例,建立過程如下:
確定切分維度,這里維度的選取順序是數(shù)據(jù)在這個維度方法最大的維度優(yōu)先。一個直接的理解就是,數(shù)據(jù)分散越開的維度,我們優(yōu)先切分。
切分點的選這個維度最中間的點。
遞歸進行步驟1,2,我們可以設置一個閾值,點的數(shù)目少于多少后就不再切分,直到所有的點都切分好停止。
下圖是一個建立例子:
BKDTree是KDTree的變種,因為可以看出來,KDTree如果有新的節(jié)點加入,或者節(jié)點修改起來,消耗還是比較大。類似于LSM的merge思路,BKD也是多個KDTREE,然后持續(xù)merge最終合并成一個。不過我們可以看到如果你某個term類型使用了BKDTree的索引類型,那么在和普通倒排鏈merge的時候就沒那么高效了所以這里要做一個平衡,一種思路是把另一類term也作為一個維度加入BKDTree索引中。
如何實現(xiàn)返回結果進行排序聚合通過之前介紹可以看出lucene通過倒排的存儲模型實現(xiàn)term的搜索,那對于有時候我們需要拿到另一個屬性的值進行聚合,或者希望返回結果按照另一個屬性進行排序。在lucene4之前需要把結果全部拿到再讀取原文進行排序,這樣效率較低,還比較占用內(nèi)存,為了加速lucene實現(xiàn)了fieldcache,把讀過的field放進內(nèi)存中。這樣可以減少重復的IO,但是也會帶來新的問題,就是占用較多內(nèi)存。新版本的lucene中引入了DocValues,DocValues是一個基于docid的列式存儲。當我們拿到一系列的docid后,進行排序就可以使用這個列式存儲,結合一個堆排序進行。當然額外的列式存儲會占用額外的空間,lucene在建索引的時候可以自行選擇是否需要DocValue存儲和哪些字段需要存儲。
Lucene的代碼目錄結構介紹了lucene中幾個主要的數(shù)據(jù)結構和查找原理后,我們在來看下lucene的代碼結構,后續(xù)可以深入代碼理解細節(jié)。lucene的主要有下面幾個目錄:
analysis模塊主要負責詞法分析及語言處理而形成Term。
codecs模塊主要負責之前提到的一些數(shù)據(jù)結構的實現(xiàn),和一些編碼壓縮算法。包括skiplist,docvalue等。
document模塊主要包括了lucene各類數(shù)據(jù)類型的定義實現(xiàn)。
index模塊主要負責索引的創(chuàng)建,里面有IndexWriter。
store模塊主要負責索引的讀寫。
search模塊主要負責對索引的搜索。
geo模塊主要為geo查詢相關的類實現(xiàn)
util模塊是bkd,fst等數(shù)據(jù)結構實現(xiàn)。
最后本文介紹了lucene中的一些主要數(shù)據(jù)結構,以及如何利用這些數(shù)據(jù)結構實現(xiàn)高效的查找。我們希望通過這些介紹可以加深理解倒排索引和傳統(tǒng)數(shù)據(jù)庫索引的區(qū)別,數(shù)據(jù)庫有時候也可以借助于搜索引擎實現(xiàn)更豐富的查詢語意。除此之外,做為一個搜索庫,如何進行打分,query語句如何進行parse這些我們沒有展開介紹,有興趣的同學可以深入lucene的源碼進一步了解。
詳情請閱讀原文
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/94422.html
摘要:介紹如何優(yōu)化數(shù)值類范圍查詢。查詢過程在中查詢是基于。在中為了查詢的這樣一個條件,會建立基于的倒排鏈。在單查詢上可能相比并沒有明顯優(yōu)勢,甚至會慢一些。所以為了支持高效的數(shù)值類或者多維度查詢,引入類。 前言 Lucene 是一個基于 Java 的全文信息檢索工具包,目前主流的搜索系統(tǒng)Elasticsearch和solr都是基于lucene的索引和搜索能力進行。想要理解搜索系統(tǒng)的實現(xiàn)原理,就...
摘要:因為倒排索引打分機制全文檢索原理分詞原理等等,這些都是不會過時的技術。中,單個倒排索引文件稱為。其中有一個文件,記錄了所有的信息,稱為文檔新寫入時,會生成新的。過程上個過程中在文件系統(tǒng)緩存中,會有意外故障文檔丟失。寫入次怕后,清空。 前言 最近 TL 分享了下 《Elasticsearch基礎整理》,蹭著這個機會。寫個小文鞏固下,本文主要講 ES -> Lucene的底層結構,然后詳細...
閱讀 1111·2021-09-22 16:04
閱讀 1494·2019-08-30 15:43
閱讀 1097·2019-08-29 14:01
閱讀 3438·2019-08-26 12:19
閱讀 3352·2019-08-26 12:15
閱讀 1444·2019-08-26 12:13
閱讀 3264·2019-08-23 17:00
閱讀 1483·2019-08-23 15:38