在信息檢索(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
Values是附加在document上一種元數據,每一個document可以有多個values,這些values通過不同的數字來標識。Values被設計成在匹配過程中快速地訪問,它們可以用作排序、排隊多餘重複的document和範圍檢索等用途。雖然values並沒有長度限制,但最好讓它們儘可能短,如果你僅僅是想存儲某個字段以便作爲結果顯示,那麼建議您最好將它們保存在document的data中。
Document data
每一個Document只有一個data,可以是任意類型格式的數據,當然在存儲的時候請先轉換爲字符串。這聽上去可能有點古怪,實情是這樣的:如果要存儲的數據是文本格式,則可以直接存儲;如果要存儲的數據是各種的對象,請先序列化成二進制流再保存,而在讀取的時候反序列化讀取。
UTF-8與Unicode
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);