Reference: http://www.totogoo.com/article/115/xapian-desc.htmlphp
Xapian的官方網站是http://www.xapian.org,這是一個很是優秀的開源搜索引擎項目,搜索引擎其實只是一個通俗的說法,正式的說法實際上是IR(Information Retrieval)系統。Xapian的License是GPL,這意味着容許使用者自由地修改其源碼併發布之。Xapian的中文資料很是少,能夠說如今互聯網上連一篇完整詳細的Xapian中文介紹文檔,更別說中文API文檔了。其實,Xapian的英文資料也很少,除了官方網站上的Docs和Wiki外,還有一些網站上的郵件列表,在這方面跟Lucene沒得比。固然,Lucene如今已經發展到2.x版本了,而Xapian的最新版本才1.012,國外開源項目通常對版本號控制得比較嚴格,一個項目通常到了1.x纔算穩定和成熟的。html
Xapian由C++編寫,但能夠綁定到Perl, Python, PHP, Java, Tcl, C# 和Ruby甚至更多的語言,Xapian能夠說是STL編程的典範,在這裏您能夠找到熟悉的引用計數型智能指針、容器和迭代器,甚至連命名也跟STL類似,相信必定能引發喜愛C++和STL的你的共鳴(實際上,不多C++程序員徹底不使用STL)。因爲Xapian使用的是STL和C運行時庫,所以具備高度可移值性,官方說法是能夠運行在Linux、 Mac OS X、 FreeBSD、 NetBSD、 OpenBSD、Solaris,、HP-UX,、Tru64和IRIX,,甚至其它的Unix平臺,在Microsoft Windows上也跑得很好。固然,並不能像Java那樣「一次編譯,處處能夠運行」,當移植到其它平臺時,通常來講是須要從新編譯的。至於如何在Windows32位系統下編譯Xapian,請查閱我之前寫的文章《nmake在windows平臺下編譯xapian》。python
依官方的說法,Xapian是一個容許開發人員輕易地添加高級索引和搜索功能到他們的應用系統的高度可修改的工具,它在支持機率論檢索模型的同時也支持布爾型操做查詢集。程序員
從功能特性上來講。Xapian和Lucene有點類似,二者都具備Term、Value(在Lucene裏稱爲SortField)、Posting、Position和Document,不過Xapian沒有Field的概念,這直接致使Xapian在使用上比Lucene麻煩了那麼一點。但這徹底不是問題,經過一些小技巧,徹底能夠本身在Xapian中實現Filed的概念。在Lucene裏還有一個叫Payload的元素,即詞條 (Term) 的元數據或稱載荷。舉一個例子,「回家吃飯吧」和「快回家吃飯」這兩個句子都帶有「吃飯」這個詞語,但在檢索的時候怎樣才能將語氣表達出來呢?雖然能夠添加Term來解決這個問題,但因爲Term的索引信息和存儲信息是分開放的,相對來講I/O性能較差,Payload就是應這個問題而生的,由於Payload信息是直接放在索引裏的。因爲對Xapian的研究還不是很深,Xapian裏是否有相似Payload這個概念,還須要繼續研究。web
搜索的目的是將結果數據展示給終端用戶,搜索引擎與普通的數據庫查詢最大的區別就在於查詢。Xapian提供了多種的查詢機制。算法
機率性搜索排名 – 重要的詞語會比不那麼重要的詞語獲得更多的權重,所以與權重高的詞語關聯的Documents會排到結果列表的更前面。數據庫
相關度反饋 – 經過給予一個或多個Documents, Xapian能夠顯示最相關的Terms以便擴展一個Query,及顯示最相關的Documents。express
詞組和鄰近搜索 — 用戶能夠搜索一個精確短語或指定數組的詞組。編程
全方位的布爾型搜索器,例如 (「stock NOT market」, etc)。windows
支持提取搜索關鍵字的詞幹,例如當搜索「football」的時候,當Documents中含有」footballs」 或」footballer」的時候也被認做符合。這有助於找到相關結果,不然可能錯過之。詞幹提取器如今支持Danish、Dutch、 English、 Finnish、French、 German、 Hungarian、Italian、 Norwegian、Portuguese、Romanian、 Russian、Spanish、Swedish和Turkish。
支持通配符查詢,例如「xap*」。
支持別名查詢,打個比方,C++會自動轉爲CPlusPlus,C#則自動轉爲CSharp。
Xapian支持拼寫糾正,例如xapian會被糾正爲xapain,固然這必須基於詞組已經被索引了。這特性跟Google提供的「你是否是想搜索xxx」有點類似。
Xapian如今的版本默認是使用flint做爲存儲系統,flint是以塊的形式來存儲,默認每塊是8K,理論上每個文件最大能夠達到2048GB。固然,在舊式的文件系統,例如FAT/FAT32是不可能實現的。熟悉Windows內存管理機制的朋友必定知道使用Windows32位系統每一個進程的總虛擬地址空間只有4GB,而用戶模式連2GB都不夠(Windows2003能夠將用戶模式擴展到3GB左右),所以應用程序不可能一次過將整個Database文件讀取到內存中,一般的作法是使用內存映射文件,先預訂地址空間,在真正使用的時候才調撥內存,而內存分頁粒度是4k,也就是說內存中每一頁是4k,而在IA64系統中,內存分頁粒度是8k。在內存中,除了頁外,還有區塊,X86和IA64的內存區塊的粒度都是64k。Xapian這樣存儲數據估計是爲了在各個平臺上都能實現數據對齊,數據對齊對於cpu運算尋址是很是重要的,而8和64都是4的倍數,所以大膽猜測Xapian以8k做爲存儲系統的默認塊大小是爲了在性能和兼容性中取得最平衡和最優值。
Xapian使用unsigned 32-bit ints做爲Documents的id值,所以在每一個Xapian的Database中,最多可容納40億個Documents。而Xapian的Terms和Documents都是使用B-樹來存儲的,其實不少數據庫系統(這裏所指的是關係數據庫)的索引都是用B-樹或B+樹來存儲的,具備增刪改查比較方便迅速的特色,缺點則是若是索引被刪除後的空間不能重複利用,爲了提升性能,一般要常常重建索引。
搜索引擎的性能是用戶很是關心的一部分,Xapian的性能如何?官方的原話以下:The short answer is 「very well」 – a previous version of the software powered BrightStation’s Webtop search engine, which offered a search over around 500 million web pages (around 1.5 terabytes of database files). Searches took less than a second.。在5億個網頁共1.5TB大小的文件中,搜索只須要小於一秒就完事了。固然,這跟運行的平臺和機器是密切相關,在咱們本身構建好Xapian搜索引擎應用後,咱們也能夠測測具體的速度。
Xapian的官方網站上有一個絕佳的使用範例,這個稱爲Omega的項目甚至能夠開箱即用做爲一個CGI應用程序。Omega附帶了Omindex和ScriptIndex這兩個索引生成工具,能夠將硬盤上的html,pdf,圖片甚至視頻影片索引發來並生成Database,經過操做這些由Omindex或ScriptIndex生成的Database,Omega提供了搜索這些文件的功能。 關於《利用Xapian構建本身的搜索引擎》系列
在使用Xapian的過程當中,我通常是查閱http://www.xapian.org/docs/上的Doc、API Doc和Wiki,遇到困難時則查閱Omega的源代碼並互相印證之。實在沒辦法的時候只能從Google上找找一些網站的郵件列表,能夠說是磕磕碰碰地將Xapian的大部分功能玩了一遍。有一些專有名詞我雖然知道大概意思,但沒法準確地翻譯出來,所以《利用Xapian構建本身的搜索引擎》這一系列的內容可能會錯漏百出。不過若是這一系列文章能夠引發你們對Xapian的興趣,它所獲得的批評纔是它最大的價值。
在Xapian1.0以前,是使用quartz做爲database文件格式的,不過自從1.0以後,便改用Flint做爲database的文件格式了。有時候,咱們會將database稱爲「索引」,在Xapian中,索引一般比被索引的documents還要多,這表示Xapian作一個信息檢索系統比作一個信息存儲系統更適合。
Xapian的database是全部用於檢索的信息表的集合,如下的表是必需的:
l posting list table 保存了被每個term索引的document,實際上保存的應該是document在database中的Id,此Id是惟一的。
l record table 保存了每個document所關聯的data,data不能經過query檢索,只能經過document來獲取。
l term list table 保存了索引每一個document的全部的term。
如下的表是可選的,即當有如下的類型的數據須要被存儲的時候纔會出現(在1.0.1之前,position和value表就算是沒有數據的時候也會被建立,而spelling和synonym表是1.0.2後纔出現的)。
l position list table 保存了每個Term出如今每個document中的位置
l value table 保存了每個document的values,values是用做保存、排序或其它做用的。
l spelling table 保存了拼寫糾正的數據。
l synonym table 保存術語的字典,例如NBA、C#或C++等。
以上的每個集合是保存在獨立的文件中,以便容許管理員查看其中的數據。剛剛說了,有一些表不是必需的,例如當您不須要詞組搜索的時候,不必存儲任何的postionlist信息。
若是你看過Xapian的database,你會發現以上的每個表實際上是使用了2到3個文件的,若是您正在使用「flint」做爲database的存儲格式,那麼termlist表會被存儲爲如下三個文件「termlist.baseA」、「termlist.baseB」、「termlist.dB」。在這些文件中,其實只有」.db」文件存儲了真實的數據,「.baseA」和「baseB」文件是用做跟蹤若是於「.dB」文件中查找數據。一般只會出現一個「.baseA」文件和一個「.baseB」文件。
在前一篇《利用Xapian構建本身的搜索引擎:簡介》中提到過,Xapian如今的版本默認是使用flint做爲存儲系統,「.dB」文件是以塊的形式來存儲,默認每塊是8K,第一塊是用做信息頭,若是使用UltraEdit等二進制查看工具,會發現全部「.dB」文件的前三個字節都是0x00。所以,當「.dB」中僅有一條數據的時候整個文件也會有16KB時,切莫大驚小怪。
改變「.dB」文件的默認塊大小會致使性能變化,但結果很難說是好是壞,由於這是跟所承載的硬件平臺與操做系統平臺有關的。通常來講,B樹的分支因子(即每一個結點能容納的關鍵字的數量)越大,B樹的查找性能就越強;但因爲一般狀況下,B樹的結點都是存儲在永久存儲系統(例如硬盤/磁帶)中,每次訪問某個結點都會將整個結點由永久存儲系統讀入到內存中,這是一個博弈的過程:假設一棵數據量很大的B樹,將B樹的分支因子設到很大,這棵B樹會長得很矮,從理論上來講查找性能可能很高。但這樣就帶來了一個弊端,每一個結點所佔的內存很是多,若是在一個併發訪問量很大的IR系統中採用這種方式的話所使用的內存一定是很是可觀的。所以在調整「.dB」文件的默認塊大小的時候必定要充分考慮cpu體系和操做系統平臺,以便調整到最佳性能。
Xapian能保證對database的全部修改都是原子性的,這意味着:
l 從一個獨立的進程(或一個獨立的database對象在同一個進程)角度來看,在讀取數據庫的時候,直到修改爲功提交,全部對數據庫的修改都是不可見的。
l Database在硬盤中的狀態始終是保持一致的。
l 若是在修改的過程當中系統發生中斷,只要硬件不發生故障(硬盤損壞),就算電源被切斷,database應該老是被還原到有效的狀態。
提交一個修改須要幾回的系統調用,以便使全部緩存的修改能刷新到硬盤上,這樣能確保就算系統在任什麼時候候發生錯誤,database也能處於一致的狀態。固然,這樣相對於說會慢了一點(由於系統已經準備好往硬盤上寫數據了),所以將幾回的修改組合在一塊兒往硬盤上寫會有必定的性能提高。
多個修改操做能夠顯式地組合在一個事務中,若是一個應用程序不顯式地使用事務來保護修改操做,Xapian會將這些修改組合在一個事務中,而後批量進行修改。請注意,Xapian如今暫時還不能跨database進行事務操做。
若是要想迅速地生成很是大的database,請使用「DANGEROUS」關鍵字搜索Xapian的郵件列表,其提供了能夠從新編譯Xapian而不採用原子性修改的方法,這功能已經再也不整合在xapian的標準版本中了。
Single writer, multiple reader
Xapian實現了「單寫多讀」的模式,這意味着任什麼時候候,同一時刻只容許一個對象能夠修改database,但容許多個對象能夠讀取database。
在*nix系統下,Xapian使用「lock-files」強制約束來實現此模式,在一個flint database中,每個Xapian的database目錄包含了一個名爲「flintlock」的文件以做鎖定用途。此文件總在存在於database的目錄裏,當database被打開用做寫入的時候,此文件會被fcntl()方法鎖定。每個WritableDatabase打開的時候,都會產生一個子進程以便進行鎖定操做。若是某個database寫入器(通常是指WritableDatabase)尚未機會執行釋放鎖的清除操做便退出了(例如這個WritableDatabase所在的應用程被殺死了),fcntl()產生的鎖會自動被操做系統釋放。
在Microsoft Windows下,使用的是另外一種鎖定的技術,此技術不須要產生了子進程來進行鎖定操做,但同理,當寫入器退出時,操做系統依然會自動釋放鎖。熟悉Windows機制的朋友知道,Windows是使用文件句柄來操做文件的,而文件句柄是屬於內核資源的一種,在任何狀況下,Windows都能保證應用程序在退出時能釋放全部的資源。
Xapian如今能夠工做在一個網絡文件系統中,但存在着大量的潛在問題。所以建議在部署前要大量地在特定的網絡中測試。
請注意,Xapian是很是依賴I/O操做的,除非處於一個性能很是優秀的網絡,在一個網絡文件系統中進行操做會相對的慢一點。
Xapian須要能夠在database的目錄裏建立一個lock file,在某些網絡文件系統中(例如NFS),這須要一個鎖定的守護進程在運行,也就是上面所提到的子進程。
說了一堆的理論,下面咱們來實戰建立一個database。Xapian裏的全部類都處於Xapian這個命名空間裏,Xapian::Database是全部Database的基類,實際上,它只有一個子類,那就是WritableDatabase。從面向對象的角度來看,這兩個類設計得很是好,Xapian::Database擁有大部分只讀或內存操做的方法,而Xapian::WritableDatabase則擁有事務操做,刷新數據到硬盤等方法。
有幾種建立Database的方法:
l Flint 若是你是使用遠程後端(指網絡文件系統),請使用Xapian::Flint::open()方法來建立database。使用此方法你能獲得更多的控制,例如建立只讀的database,或建立可寫的database。
l Auto auto並非一種database格式,你能夠建立一個「database存根」文件,此文件能列出一到多個database的路徑,這些路徑能夠做爲Xapian::Database的構造函數的參數,從而被自動檢測是哪一種類型的database。還有,若是將一個文件路徑名稱而非目錄名稱做爲參數傳入到Xapian::Database的構造函數中,Xapian::Database會認爲你傳入了一個「database存根」文件;固然,你也可使用Xapian::Auto::open_stub()來顯式打開一個存根文件。上面說的可能有點繞口,「database存根文件」的格式是每一個database一行,例如:
remote localhost:23876 flint /var/spool/xapian/webindex
這下該明白了。
l Inmemory 還能夠建立內存databse,這種類型的database是保存在內存中。請注意,經過Xapian::InMemory::open()返回的類型是WritableDatabase,這意味着這是一個可刷新到硬盤上的database,它最初是爲測試之用,但在創建臨時的小數據庫也多是有用的。
實際上,建立一個database還有更通用的方法,例如經過將一個database所在的完整目錄做爲一個字符串傳入到Xapian::Database的構造函數中實例化一個對象後,便可獲得一個只讀的database。而Xapian::WritableDatabase則複雜一點,除了要傳入database的路徑外,還須要設定如何打開database。有如下幾個參數:
l Xapian::DB_CREATE_OR_OPEN 打開以便讀寫,若是不存在則建立。
l Xapian::DB_CREATE 老是建立新的database,若是存在則失敗。
l Xapian::DB_CREATE_OR_OVERWRITE 若是database存在的話則覆蓋之,若是不存在則建立。
l Xapian::DB_OPEN 打開以便讀寫,若是不存在則失敗。
成功打開一個datababse是Xapian全部後續操做如檢索,寫入的基礎。你甚至能夠將經過add_database()方法則多個database組合在一塊兒訪問;若是想將database刷新到硬盤中,則執行flush()方法則可。最後,若是不想使用database了,將database對象銷燬便可。
在這一章裏,彷佛並無多少具體的操做,但database是Xapian的存儲系統,在Xapian全部操做的基礎,只有清楚明白了Xapian的存儲方式才能更好更高效地構造本身的搜索引擎。同時,若是您以前並無對大型的文件存儲系統有所瞭解的話,這篇文件能夠多多少少帶給您一些啓示。在下一章裏,我會繼續介紹Document和Term等Xapian的組成部分。
裏指出database是Xapian的基礎,而這一篇裏講到的documents、terms和values則是索引和查詢的必要組成部分。
在信息檢索(IR)中,咱們企圖要獲取的項稱之爲「document」,每個document是被一個terms集合所描述的。「document」和「term」這兩個詞彙是IR中的術語,它們是來自「圖書館管理學」的。一般一個document認爲是一塊文本,. Usually a document is thought of as a piece of text, most likely in a machine readable form, 而一個term則是一個詞語或短語以用做描述document的,在document中大多數會存在着多個term,例如某個document是跟口腔衛生相關的,那麼可能會存在着如下的terms:「tooth」、「teeth」、「toothbrush」、「decay」、 「cavity」、「plaque」或「diet」等等。
若是在一個IR系統中,存在一個名爲D的document,此document被一個名爲t的term所描述,那麼t被認爲索引了D,能夠用如下式子表示:t->D。在實際應用的一個IR系統中一般是多個documents,如D1, D2, D3 …組成的集合,且有多個term,如t1, t2, t3 …組成的集合,從而有如下關係:ti -> Dj。
若是某個特定的term索引了某個特定的document,那麼稱之爲posting,說白了posting就是帶position信息的term,在相關度檢索中可能有必定的用途的。
給定一個名爲D的document,存在着一個terms列表索引着它,咱們稱之爲D的term list。
給定一個名爲t的term,它索引着一個documents列表,這稱之爲t的posting list(使用「Document list」可能會在叫法上更一致,但聽起來過於空泛)。
在一個存在於計算機的IR系統中,terms是存儲於索引文件中的。term能夠用做有效地查找它的posting list,在posting list裏,每個document帶有一個很短的標識符,就是document id。簡單來講,一個posting list能夠被認爲是一個由document ids組成的集合,而term list則是一個字符串組成的集合。在某些IR系統的內部是使用數字來表示term的,所以在這些系統中,term list則是數字組成的集合,而Xapian則不是這樣,它使用原汁原味的term,而使用前綴來壓縮存儲空間。
Terms不必定是要是document中出現的詞語,一般它們會被轉換爲小寫,並且每每它們被詞幹提取算法處理過,所以經過一個值爲「connect」的term可能會檢索出一系列的詞語,例如「connect」、「connects」、「connection」或「connected」等,而一個詞語也可能產生多個的terms,例如你會將提取出的詞幹和未提取的詞語都索引發來。固然,這可能只適用於英語、法語或拉丁語等歐美系列的語言,而中文的分詞則有很大的區別,總的來講,歐美語系的語言分詞與中文分詞有如下的區別:
l 拿英語來講,一般狀況下英語的每個詞語之間是用空格來隔開的,而中文則否則,甚至能夠極端到整篇文章都不出現空格或標點符號。
l 像上面提到的,「connect」、「connects」、「connection」或「connected」分別的意思「動詞性質的鏈接」、「動詞性質的第三人稱的鏈接」、「名稱性質的鏈接」或「鏈接的過去式」,但在中文裏,用「鏈接」就能夠表示所有了,幾乎不須要詞幹提取。這意味着英語的各類詞性大部分是有章可循的,而中文的詞性則是天馬行空的。
l 第二點只是中文分詞很是困難的一個縮影,要徹底正確地標識出某個句子的語意是很困難的,例如「中華人民共和國成立了」這個句子,能夠分出「中華」、「華人」、「人民」、「共和國」、「成立」等詞語,不過其中「華人」跟這個句子其實關係不大。咋一眼看上去很簡單,但機器那有這麼容易懂這其中的奧妙呢?
Values是附加在document上一種元數據,每個document能夠有多個values,這些values經過不一樣的數字來標識。Values被設計成在匹配過程當中快速地訪問,它們能夠用做排序、排隊多餘重複的document和範圍檢索等用途。雖然values並無長度限制,但最好讓它們儘量短,若是你僅僅是想存儲某個字段以便做爲結果顯示,那麼建議您最好將它們保存在document的data中。
每個Document只有一個data,能夠是任意類型格式的數據,固然在存儲的時候請先轉換爲字符串。這聽上去可能有點古怪,實情是這樣的:若是要存儲的數據是文本格式,則能夠直接存儲;若是要存儲的數據是各類的對象,請先序列化成二進制流再保存,而在讀取的時候反序列化讀取。
Xapian裏的全部東西是用UTF-8來保存的,UTF-8是Unicode的一種實現。如今不少人用VC爲了方即是將編碼設成「未設置」或「多字節」的,也就是說用的是系統內碼(GB2312/GBK/ GB18030),這樣的話則將數據保存到Xapian前要先轉碼爲UTF-8,而從Xapian裏讀出的數據則要轉碼爲GB2312/GBK/ GB18030才能正確顯示,這裏推薦用iconv,這是一個很是方便的庫。 分詞
不少文章都說如今的中文分詞已經很成熟的,但據實際考察,google或百度等大公司的分詞引擎都是本身開發或有專門的公司開發的,的確已經算比較成熟。但市場上提供免費甚至開源的分詞引擎很少,中科院研發的ictclas30分詞精確度和分詞速度都很是不錯,並且還有詞性標註和自定義添加詞的功能,惋惜不開源。另外比較受歡迎的還有libmmseg和SCWS,所以都是開源的,不過經測試libmmseg的分詞精度彷佛不高,而SCWS因爲使用了大量的遞歸,在生成詞庫的時候常常致使棧溢出(我是用vc2005編譯的),須要本身將遞歸修改成循環,從演示的狀況來看,SCWS的分詞精度來算能夠。 實戰
因爲Xapian並不像Lucene那樣有Field的概念,所以通常採用以大寫字母做爲Term和posting的前綴,但單個字母的前綴對程序員太不友好了,因此通常的作法是自定義一個用戶前綴到term前綴的映射,如Title=>T,而Xapian的QueryParser也支持這種映射,QueryParser是查詢解釋器,能將一段字符串解釋爲Xapian的Query,後面會陸續提到。
添加document的例子:
Xapian::Document doc;
doc.add_term(「K你好」);
doc.add_term(「K那裏」);
//posting是帶position的term doc.add_posting(「K吃飯」, 14); doc.add_posting(「K玩耍」, 8); /* 這裏最好先用一個map<string, int>放置value的名稱和索引的配對 這裏使用起來像Lucene的SortField同樣了。 */ doc.add_value(1, 「1」); doc.set_data(「你好啊,在那裏玩耍呢?還沒吃飯嗎?」); //建立一個可寫的db Xapian::WritableDatabase db(「c://db」); //將document加入到db中,返回document的id,此id在db中是惟一的 Xapian::docid id = db.add_document(doc); //刷新到硬盤中 db.flush(); 獲取document信息的例子: //獲取 Xapian::Document doc = db.get_document(id); string v = doc.get_value(1); printf(v);//輸出 string data = doc.get_data(); printf(data);//輸出」你好啊,在那裏玩耍呢?還沒吃飯嗎?」 for (Xapian::TermIterator iter = doc.termlist_begin(); iter != doc.termlist_end(); ++iter) { printf(*iter);//依次輸出term和posting }
上面的兩個例子比較簡單,若是要想更深刻請查閱Omega的代碼,裏面有更復雜的應用。值得一提的Xapian裏有一個TermGenerator,能夠更方便地索引數據,不過這個類有兩個不知道算不算缺點的特色:首先是依賴Stem,對於中文來講除非本身實現了一個Stem,不然TermGenerator用處不大;另外TermGenerator會自動將生成的term或posting添加「Z」前綴。
在這裏要提一下一個名爲「Xapwrap」的東東,這是某個外國人用python寫的一個封裝Xapian的類庫,裏面某些思想仍是不錯的,只惋惜只兼容Xapian 1.x以前的版本。我本身封裝的類有一部分就是參考Xapwrap的。
下面是一段我正在用的代碼:
//CXapianDocument是封裝過的Xapian::Document void doSegment(CXapianDocument& document, const char* lpszInput, string strUserPrefix) { //先分詞,這裏使用的是中科院的分詞引擎 int nCount = ICTCLAS_GetParagraphProcessAWordCount(lpszInput); result_t *result =(result_t*)malloc(sizeof(result_t)*nCount); //獲取分詞結果 ICTCLAS_ParagraphProcessAW(nCount,result); string termPrefix; //經過用戶前綴取得term前綴,這是我自定義的一個宏 GetTermPrefixFromMap(this->m_userPrefixToTermPrefixMap, strUserPrefix, termPrefix) for (int i=0; i<nCount; i++) { //忽略標點符號,標點符號的詞性標註爲w開頭的 if(result[i].sPOS[0] == ‘w’) { continue; } char buf[100]; memset(buf, 0, 100); int index = result[i].start; memcpy(buf,(void *)(lpszInput+index), result[i].length); //添加posting document.AppendPosting(termPrefix, buf, result[i].start); } free(result); }
通過前面幾篇的介紹,若是再參考一下Omega的話,估計應該能夠順利建立database和往database裏添加document了。有了數據,下一步關心的固然是怎樣將它們查出來,在一個IR系統(不單止Xapian)中,檢索的方式是多元化的,排序則是多樣化的,結果則是人性化的,這就是跟關係數據庫相比的最大優點。因爲內容較多,所以將檢索、排序和取得結果分開講述,這一篇先講述如何檢索。
IR系統有這麼多的好處,所以終端用戶對它是有很高指望的,世事萬物總不會完美的,因而IR系統有三個評價標準:召回率、準確率與查詢效率。三個指標相互矛盾,只有取捨、不能調和,這亦是一個博弈的過程,使用者關心不一樣的指標,天然會採用不一樣的觀點和作法。拿Web搜索引擎來講,查詢效率確定是擺在第一位的,其次才能考慮準確率和召回率。看字面看上去,你們內心估計對準確率還有個譜,但召回率又如何解釋呢?
有時候,準確率也稱爲精度,舉個例子,一個數據庫有500個文檔,其中有50個文檔符合定義的問題。系統檢索到75個文檔,可是隻有45個符合定義的問題。
召回率R=45⁄50=90%
精度P=45⁄75=60%
本例中,系統檢索是比較有效的,召回率爲90%。可是結果有很大的噪音,有近一半的檢索結果是不相關。一般來講,在不犧牲精度的狀況下,得到一個高召回率是很困難的。對於一個檢索系統來說,召回率和精度不可能一箭雙鵰:召回率高時,精度低,精度高時,召回率低。對於搜索引擎系統來說,它能夠經過搜索更多更多的結果來查到更多相關結果,從而提升召回率(查全率),但也會致使查到更多不相關結果,從而下降了搜索結果的精度(查準率)。由於沒有一個搜索引擎系統可以蒐集到全部的WEB網頁,因此召回率很難計算。因此通常來講,不會單獨的使用召回率或精度,而是在其中一個值固定的基礎上,討論另外一個值。如當召回率爲60%時的精度值變化狀況。所以在召回率與準確率中,Web搜索引擎會更傾向於後者,由於終端用戶最想獲得的他們要想獲得的數據,而不是一堆似是而非的數據。
可是,對於一個傳統的圖書信息檢索系統,狀況會大不相同——書籍與文章有良好的關鍵字索引,包括標題、做者、摘要、正文、收錄時間等定義明確的結構化數據,文檔集合相對穩定而且規模相對較小,想更深一層,終端用戶可能只知道某圖書名的其中一兩個字,那麼若是在較低的召回率下,此用戶可能會鎩羽而歸。
說到這裏咱們應該差很少知道IR系統在不一樣的應用場合下是有不一樣的準確率和召回率做爲評價指標的,而準確率和召回率則是由分詞策略直接影響的,拿咱們最關心的中文分詞來講,分詞策略通常有如下幾種:
l 第一種,默認的單字切分。這種分詞策略實現起來最簡單,舉個例子,有如下句子:「咱們在吃飯呢」,則按字切分爲[我]、[們]、[在]、[吃]、[飯]、[呢]。按這種方法分詞所獲得的term是最少的,由於咱們所使用的漢字就那麼幾千個,但隨便所索引的數據量的增大,索引文件的增加比例卻比下面的幾種模型都要大,雖然其召回率是很高的,但精確率卻很是低,並且通常狀況下性能也是最差的。
l 第二種,二元切分,即以句子中的每兩個字都做爲一個詞語。繼續拿「咱們在吃飯呢」這個句子做例子,用二元切分法會獲得如下詞:[咱們]、[們在]、[在吃]、[吃飯]、[飯呢]。這種切分方法比第一種要好,精確率提升了,召回率也沒下降多少(實際上二者都不高,太中庸了)。
l 第三種:按照詞義切分。這種方法要用到詞典,常見的有正向最大切分法和逆向最大切分法等。咱們再拿「咱們在吃飯呢」做爲例子。使用正向切分法最終獲得詞語可能以下:[咱們]、[在吃]、[飯]、[呢],而使用逆向最大切分法則可能最終獲得如下詞語:[咱們]、[在]、[吃飯]、[呢]。只要處理好在龐大的詞典中查找詞語的性能,基於詞典的分詞結果會挺不錯。
l 第四種:基於統計機率切分。這種方法根據一個機率模型,能夠從一個現有的詞得出下一個詞成立的機率,也以「咱們在吃飯呢」這個句子舉個可能不恰當的例子,假設已經存在[咱們]這個詞語,那麼根據機率統計模型能夠得出[吃飯]這個詞語成立的機率。固然,實際應用中的模型要複雜得多,例如著名的隱馬爾科夫模型。
在實際的中文分詞應用中,通常會將按詞典切分和基於統計機率切分綜合起來,以便消除歧義,提升精確率。
前面提到,按單字切分的查詢性能可能反而是最差的,咋一眼看上去,這種分詞方式低精度高召回率是沒錯,但爲何說它性能很差呢。爲了方便解釋,咱們假設有兩萬篇文章須要被存儲和索引,假設文章裏全部內容都是漢字,咱們經常使用的漢字有4000~5000個,那麼最理想的狀況下平均每一個漢字索引了4~5篇文章,惋惜實際上有不少漢字的出現頻率是很是高的,就拿上面的[我]、[們]、[在]、[吃]、[飯]、[呢]這幾個漢字來講,在每篇文章中出現的機率估計至少得有70%~80%。
常見的存儲方式是將索引和數據(即文章內容)分開存放,以各類樹(紅黑樹、AVL樹或B樹)來存儲索引,每一個結點除了保存父結點和兒子結點的指針外,通常還會保存其索引的文章的Id(在Xapian裏就是DocId),經過這個Id能夠很快地找到文章內容。在Xapian中,DocId是以32位無符號整數來表示的,佔4個字節,若是「我」字在兩萬篇文章中出現的機率是50%,那麼「我」字這個結點就至少佔了41000個字節,差很少足足40K!若是某天咱們的永久存儲體和內存的速度同樣快了,這種存儲方式問題其實還不大,但因爲咱們如今廣泛使用硬盤/磁帶機來保存永久數據,商用的硬盤/磁帶機的結構是使用由機械臂控制的磁頭來讀寫盤片來存取數據的,爲了減小磁頭定位的次數,硬盤/磁帶機會設計成按頁讀取,每頁佔2~2字節,雖然通過這樣的精心設計,但硬盤/磁帶機的存取速度仍是比主存慢5個數量級左右,這就是I/O是最耗性能的緣由,也是咱們每天說的「數據庫是瓶頸」的緣由所在。
很明顯,若是按上述的推論,「我」這個結點要佔10個以上磁盤頁,這太瘋狂了。若是經過分詞技術將文章切分爲多個詞語,那麼每一個詞語所索引文章一定減小。前面提到大部分的IR系統或數據庫系統的索引都是以B樹的形式來存儲的,B樹是一種硬盤I/O性能很是好的數據結構,其特色是通常每一個結點的大小和硬盤上每頁的大小是同樣的,每一個結點能存放n個關鍵字,而每一個結點又有n+1個子女,也就是說,在一棵高度爲2的B*樹中,最多隻須要讀取2個結點就能夠到達目標結點,也就是說控制磁頭的機械臂只移動了兩次。在這個時候,良好的數據結構的優越性就顯示出來了。
固然,這只是純粹以硬盤/磁帶機爲中心來討論,在實際應用中架構會更加良好,並且若是隻有兩萬篇文章,當咱們的主內存足夠大的時候,甚至能夠一次過將全部文章讀到內存中以免進行硬盤I/O操做,只是這樣也帶來了寫入數據時很是緩慢的尷尬。如今的數據庫或IR系統的數據文件動輒幾個GB,所以怎樣最大限度避免進行頻繁的硬盤I/O讀寫仍是放在提升性能的第一位的。
不過千萬別覺得IR系統一切都比關係數據庫要好,IR系統的其中一個弱點是插入、修改和刪除都相對緩慢,由於是中間要通過多層的工序處理,因此IR系統的首要任務是檢索,其次纔是存儲。
雖然IR系統會幫咱們分詞,但有時候咱們卻想「幫助」IR系統理解咱們要搜索什麼。例如,咱們可能會在百度或Google的搜索欄裏輸入:「咱們吃飯」來尋找咱們感興趣的關於「咱們」和「吃飯」的文章,而不是直接輸入「咱們吃飯」來搜索文章。這兩種的輸入獲得的結果是徹底不一樣的,由於「咱們吃飯」已經成爲了Google的IR系統裏的其中一個term了。
像「咱們吃飯」這樣的輸入,其實就是布爾型檢索。在Xapian裏,則是將多個terms用AND、 OR或AND_NOT鏈接起來,舉個例子:
t1 索引了 documents 1 2 3 5 8 t2 索引了 documents 2 3 6 那麼: t1 AND t2 檢索得 2 3 t1 OR t2 檢索得 1 2 3 5 6 8 t1 AND_NOT t2 檢索得 1 5 8 t2 AND_NOT t1 檢索得 6
在不少系統中,這些documents並無根據它們之間的相關度來排序的;但在Xapian裏,布爾型風格的查詢均可以在檢索得出documents集合結果後,而後使用機率性的排序。
布爾型檢索是最經常使用的,但在IR系統中,其還沒能擔大旗,由於使用布爾型檢索獲得的結果並無按任何機制使其能變得對用戶更友好,在這種狀況下,用戶必須對這個IR系統有充分的瞭解才能更有效地使用之。雖然如此,但只有純粹的布爾型檢索的IR系統依然活得好好的。
相關度是機率模型裏的核心概念,能夠將documents的集合按相關度來排列。本質上,當某個document是用戶須要的,那麼它則是相關的,不然即是不相關的,在理想狀態下,檢索到的document都是相關的,而沒檢索到的則是一個都不相關的,這是一個黑與白的概念。不過檢索不多是完美的,所以會出現風馬牛不相及的狀況,因而便用相關度來表示,指兩個事物間存在相互聯繫的百分比,這是一個很是複雜的理論。
Xapian默認的排序模式稱爲BM25Weight,這是一種將詞頻和document等元素出現的頻率經過一個固定的公式得出排序權重的模式,權重越高則相關度越高,若是不想使用BM25Weight做爲排序模式,可使用BoolWeight,BoolWeight模式裏的各類元素的權重都爲0。排序會在後續文章裏繼續講述。
默認狀況下,Xapian可使用任意組合的複雜的布爾型查詢表達式來縮小檢索的範圍,而後將結果按機率性排序(某些布爾型系統只容許將查詢表達式限制爲某種格式)。
布爾型檢索和機率性檢索有兩種組合的方式:
先用布爾型檢索獲得全部documents中的某個子集,而後在這個子集中再使用機率性檢索。
先進行機率性檢索,而後使用布爾型檢索過濾查詢結果。
這兩種方式的結果仍是有稍稍區別的。舉個例子,在某個database裏包含了英文和法文兩種documents,「grand」這個詞語在這兩種語言中都存在(意思都差很少),但在法文中更常見,不過若是使用第一種方式,先用布爾型檢索先限定出英文子集,這個詞語則會獲得更多的權重。
第一種方法更精確,不過執行效率不高,Xapian特意優化了第二種方法,別覺得Xapian真的先進行機率性檢索再進行布爾型檢索的,實際上Xapian是同時執行這兩種操做的。在Xapian內部進行了幾種優化,例如若是經過幾率性檢索能得出結果,Xapian就會取消正在執行的布爾型AND操做。這些優化方法通過評測能夠提升幾倍的性能,而且在執行多個Terms查詢時會有更好的表現。
在IR系統中,終端用戶按某種系統約定的格式輸入,這些輸入便稱爲「查詢」。而後IR系統將此輸入轉交給查詢器,查詢器也是IR系統的一部分,其能夠解析「查詢「,匹配documents和對結果集進行排序,而後返回結果給終端用戶。
在Xapian中,Query類便起着「查詢」的做用,Query類的生成方法有兩種,第一種是由QueryParser類解析查詢字符串生成,別一種則是建立多個表示不一樣描述表達式的Query類,而後再將這些Query按需組合起來。
如下是Xapian::QueryParser支持的語法,其實這些語法跟其它IR系統的語法亦很類似。
l AND
expression And expression提取這兩個表達式所匹配的documents的交集。
l OR
expression OR expression提取這兩個表達式匹配的documents的並集。
l NOT
expression NOT expression提取只符合左邊的表達式的documents集合。
若是FLAG_PURE_NOT標誌被設置,那麼NOTexpression表達式不提取匹配符合此表達式的documents。
l XOR
expression XORexpression 只提取左表達式和右表達式其中一個表達式匹配的documents,而不提取二者都匹配的documents。
l 組合表達式
可使用括號將上述布爾操做符括起來從而控制其優先級,例如:(one OR two) AND three。
l +和–
一組標記了+或-操做符的terms只提取匹配全部的+terms,而不匹配全部的-terms。若是terms不標記+或-操做符會有助於documents的排名。
l NEAR
one NEAR two NEAR three會提取符合這三個關鍵字的詞距在10之間的documents,詞距從那裏來?在《利用Xapian構建本身的搜索引擎:Document、Term和Value》這篇文章裏就曾介紹過可使用Document類的add_posting方法來添加帶詞距的terms。
NEAR默認的詞距是10,可使用NEAR/n來設置,例如one NEAR/6 two。
l ADJ
ADJ跟NEAR很類似,不過ADJ兩邊的terms是按順序來比較的。所以one ADJ two ADJ three是表示one與two與three之間的詞距都是10。
l 短語搜索
一個短語是被雙引號括着的,能夠用在文件名或郵件地址等地方。
l 使用字段名的形式
若是database裏的terms已經添加了前綴,那麼可使用QueryParser的add_prefix方法來設置前綴map。例如QueryParser.add_prefix(「subject」, 「S」)這樣便將subject映射到S,若是某個term的值爲「S標題」,那麼可使用「subject:標題」這樣的表達式來檢索結果。這時你們可能會記起Google也支持這種語法,例如在Google的搜索欄裏輸入「Site:www.wlstock.com股票」時,只會檢索出www.wlstock.com裏的關於股票的網頁,這功能其實亦實現了Lucene的Field功能。
l 範圍搜索
範圍搜索在Xapian中是由Xapian::ValueRangeProcessor類來支持的,在Xapian 1.0.0之後纔出現。從Xapian::ValueRangeProcessor的名字能夠知道,其只能搜索Value的範圍,而不能搜索terms的範圍。
Xapian::ValueRangeProcessor是一個抽象基類,所以在實際應用中要使用其子類,Xapian提供了三個開箱即用的Xapian::ValueRangeProcessor的子類,分別是StringValueRangeProcessor、DateValueRangeProcessor和NumberValueRangeProcessor,若是以爲這三個類不能知足需求,亦能夠繼承Xapian::ValueRangeProcessor來建立本身的子類。
當使用Xapian::ValueRangeProcessor的子類時,應該將開始範圍和結束範圍傳給它,若是Xapian::ValueRangeProcessor的子類沒法明白傳進來的範圍,它會返回Xapian::BAD_VALUENO。
下面僅以StringValueRangeProcessor舉例,當database裏將用戶名保存在Number爲4的Value中(Value是經過數字來標識的,詳細請看《利用Xapian構建本身的搜索引擎:Document、Term和Value》),那麼能夠這樣組織查詢表達式:mars asimov..bradbury,只是這樣固然還不夠,還須要建立一個StringValueRangeProcessor
Xapian::QueryParser qp;
Xapian::StringValueRangeProcessor author_proc(4);
qp.add_valuerangeprocessor(&author_proc);
當QueryParser解析查詢表達式時會使用OP_VALUE_RANGE標誌,所以QueryParser生成的query會返回如下描述:
Xapian::Query(mars:(pos=1) FILTER (VALUE_RANGE 4 asimov bradbury)
(VALUE_RANGE 4 asimov Bradbury)這個子表達式使用僅僅匹配Number爲4的Value的值是>= asimov 和<= bradbury(使用字符串比較)。
值範圍搜索並不複雜,更多的介紹請看http://www.xapian.org/docs/valueranges.html。
l 別名
QueryParser亦支持別名檢索,使用這樣的語法:~term。如何添加別名,後面會介紹。
l 通配符
QueryParser支持以「」結尾的通配符,所以「wildc」能夠匹配「wildcard」、「wildcarded」、「wildcards」、「wildcat」、「wildcats」。不過這功能默認是關閉的,能夠將Xapian::QueryParser::FLAG_WILDCARD
做爲標誌傳到Xapian::QueryParser::parse_query(query_string, flags)來開啓按如下步驟來開啓。 Query
若是不想使用字符串形式的查詢表達式,能夠用下面這些操做符將多個Query組合起來:
OP_AND 等同於QueryParser所支持的AND OP_OR 等同於QueryParser所支持的OR OP_AND_NOT 等同於QueryParser所支持的AND_NOT OP_XOR 等同於QueryParser所支持的XOR OP_AND_MAYBE 只返回左邊子表達式匹配的documents,不過兩邊的表達式所匹配的documents都加入權重計算。 OP_FILTER 做用跟AND類似,不過僅僅左邊的表達式匹配的documents才加入權重計算。 OP_NEAR 等同於QueryParser所支持的NEAR OP_PHRASE 等同於QueryParser所支持的ADJ OP_VALUE_RANGE 等同於QueryParser所支持的範圍搜索 OP_SCALE_WEIGHT 給子表達式指定權重,若是權重爲0,則此表達式爲純布爾型查詢 OP_ELITE_SET 做用跟OP_OR 很類似,不過有時候性能比OP_OR 要好。這裏有詳細的解釋:http://trac.xapian.org/wiki/FAQ/EliteSet OP_VALUE_GE 返回大於或等於給定的document value OP_VALUE_LE 返回小於或等於給定的document value
l 如何建立一個只包含一個term的Query
可使用默認的構造函數:Xapian::Query query(term);
亦可使用多參數的構造函數:
Xapian::Query(conststring & tname_,
Xapian::termcountwqf_ = 1,
Xapian::termpostermpos = 0) 其中wqf的全稱是WithinQuery Frequency,能夠指定此term在query中的權重。若是整個查詢只包含了一個term,這參數用處不大;但當組合查詢時,威力便顯出來了,由於能夠便取得的結果集跟這個term是更相關的。
而term_pos是指term在query中的位置,一樣若是整個查詢中只包含了一個term則用處不大,所以通常用在詞組搜索中。
l 將多個Query組合起來查詢
經過上面所說的Query操做符將Query組合起來,這時要用到Xapian::Query的另外一個構造函數:
Xapian::Query(Xapian::Query::opop_,
const Xapian::Query &left,
const Xapian::Query &right)
l 機率性查詢
一個普通的機率性查詢實際上是將terms用Xapian::Query::OP_OR鏈接起來。例如:
Xapian::Queryquery(「regulation」));
query = Xapian::Query(Xapian::Query::OP_OR,query, Xapian::Query(「import」));
query = Xapian::Query(Xapian::Query::OP_OR,query, Xapian::Query(「export」));
query = Xapian::Query(Xapian::Query::OP_OR,query, Xapian::Query(「canned」));
query =Xapian::Query(Xapian::Query::OP_OR,query, Xapian::Query(「fish」));
不過這樣的風格太臃腫了,能夠用下面這種清爽一點的風格:
vector terms;
terms.push_back(「regulation」);
terms.push_back(「import」);
terms.push_back(「export」);
terms.push_back(「canned」);
terms.push_back(「fish」);
Xapian::Query query(Xapian::Query::OP_OR, terms.begin(), terms.end());
l 布爾型查詢
假設有這樣的布爾型查詢表達式:
(‘EEC’ – ‘France’) and (‘1989’ or ‘1991’ or ‘1992’) and ‘Corporate Law’
This could be built up as bquery like this,那麼則用Query來表示則以下
Xapian::Querybquery1(Xapian::Query::OP_AND_NOT,」EEC」, 「France」);
Xapian::Querybquery2(「1989」);
bquery2 = Xapian::Query(Xapian::Query::OP_OR,bquery2, 「1991」);
bquery2 = Xapian::Query(Xapian::Query::OP_OR,bquery2, 「1992」);
Xapian::Querybquery3(「Corporate Law」);
Xapian::Query bquery(Xapian::Query::OP_AND, bquery1, Xapian::Query(Xapian::Query::OP_AND(bquery2, bquery3)));
還能夠將上面建立的bquery對象附加到另外一個機率性查詢做爲布爾型過濾器用來過濾結果集:
query =Xapian::Query(Xapian::Query::OP_FILTER,query, bquery);
l +和– 操做符
例若有這樣的查詢表達式:regulation import export +canned +fish –japan
轉化爲Query則是以下:
vector plus_terms;
vector minus_terms;
vector normal_terms;
plus_terms.push_back(「canned」);
plus_terms.push_back(「fish」);
minus_terms.push_back(「japan」);
normal_terms.push_back(「regulation」);
normal_terms.push_back(「import」);
normal_terms.push_back(「export」);
Xapian::Queryquery(Xapian::Query::OP_AND_MAYBE,
Xapian::Query(Xapian::Query::OP_AND,plus_terms.begin(),plus_terms.end());
Xapian::Query(Xapian::Query::OP_OR,normal_terms.begin(),normal_terms.end()));
query = Xapian::Query(Xapian::Query::OP_AND_NOT,
query,
Xapian::Query(Xapian::Query::OP_OR,minus_terms.begin(),minus_terms.end())); 實戰
當使用QueryParser類或Query類建立了Query對象後,只須要實例化一個查詢器就可使用這些Query對象了。例:
Xapian::Databasedb(「Index」);
Enquireenquire(db);
enquire.set_query(query);