Xapian:檢索

經過前面幾篇的介紹,如果再參考一下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%,那麼“我”字這個結點就至少佔了4*1000個字節,差不多足足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系統有充分的瞭解才能更有效地使用之。雖然如此,但只有純粹的布爾型檢索的IR系統依然活得好好的。

相關度是概率模型裏的核心概念,可以將documents的集合按相關度來排列。本質上,當某個document是用戶需要的,那麼它則是相關的,否則便是不相關的,在理想狀態下,檢索到的document都是相關的,而沒檢索到的則是一個都不相關的,這是一個黑與白的概念。不過檢索很少是完美的,因此會出現風馬牛不相及的情況,於是便用相關度來表示,指兩個事物間存在相互聯繫的百分比,這是一個非常複雜的理論。

Xapian默認的排序模式稱爲BM25Weight,這是一種將詞頻和document等元素出現的頻率通過一個固定的公式得出排序權重的模式,權重越高則相關度越高,如果不想使用BM25Weight作爲排序模式,可以使用BoolWeight,BoolWeight模式裏的各種元素的權重都爲0。排序會在後續文章裏繼續講述。

組合檢索

默認情況下,Xapian可以使用任意組合的複雜的布爾型查詢表達式來縮小檢索的範圍,然後將結果按概率性排序(某些布爾型系統只允許將查詢表達式限制爲某種格式)。

布爾型檢索和概率性檢索有兩種組合的方式:

  • 先用布爾型檢索得到所有documents中的某個子集,然後在這個子集中再使用概率性檢索。
  • 先進行概率性檢索,然後使用布爾型檢索過濾查詢結果。

這兩種方式的結果還是有稍稍區別的。舉個例子,在某個database裏包含了英文和法文兩種documents,“grand”這個詞語在這兩種語言中都存在(意思都差不多),但在法文中更常見,不過如果使用第一種方式,先用布爾型檢索先限定出英文子集,這個詞語則會得到更多的權重。

      第一種方法更精確,不過執行效率不高,Xapian特地優化了第二種方法,別以爲Xapian真的先進行概率性檢索再進行布爾型檢索的,實際上Xapian是同時執行這兩種操作的。在Xapian內部進行了幾種優化,例如如果通過概率性檢索能得出結果,Xapian就會取消正在執行的布爾型AND操作。這些優化方法經過評測可以提高几倍的性能,並且在執行多個Terms查詢時會有更好的表現。

QueryParser

      在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::termposterm_pos_ = 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 <string>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 <string>plus_terms;

   vector <string>minus_terms;

   vector <string>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);

當然,要想取得結果集、對結果集排序或擴展查詢還需要更多的功夫,會在下一篇裏繼續講述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章