Xapian簡明教程(未完成)


第一章 簡介

1.1 簡介

Xapian是一個開源的搜索引擎庫,它可以讓開發者自定義的開發一些高級的的索引和查找因素應用在他們的應用中。

通過閱讀這篇文檔,希望可以幫助你創建第一個你的索引數據庫和了解一些基本概念,並提供了了代碼作為參看。

如果你已經安裝了Xapian,並且只想看實例代碼,可以跳過 “Xapian-核心概念”章節,直接到 “Xapian-Hello World”。

1.2 安裝

安裝Xapian包括兩部分,一部分是Xapian核心庫自身,還有一部分是你要使用Xapian的編程語言。我們寫這個文檔大部分是用PHP作為例子。

這篇文檔使用的是Xapian1.2

1.3 CentOS安裝Xapian

xapian-core

xapian的一些接口,核心庫

cd xapian-core-1.2.15

./configrue -prefix=/opt

error:

Neither uuid/uuid.h nor uuid.h found - required for brass

yum install libuuid-devel

make && make install

xapian-bindings

通過多種語言來調用xapian接口

cd xapian-bindings-1.2.15

./configure --prefix=/usr/local/xapian XAPIAN_CONFIG=/opt/bin/xapian-config --with-php

注意:這里我的PHP版本是PHP5.4 有可能版本不對導致xapian-bindings檢測不到php

make&&make isntall

php-config --extension-dir一下,xapian-bindings已經把xapian.so復制到extensions下面了

修改php.ini

extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xapian.so

php -m|grep xapian 一下已經安裝了xapian擴展了

第二章 核心概念

2.1 並發

Xapian盡管可以用在多線程的程序中,但是它本身並不直接支持多線程。在將Xapian應用在多線程程序中前確保你已經知道一下描述的細節。

Xapian沒有保持任何全局狀態的變量,在線程的程序中並不共享任何對象,所以你可以安全的使用Xapian在多線程編程中。在實踐中,這並不算個問題-每個線程可以創建自己的XapianDatabase對象,並且他們可以各自工作的很好。

如果你真的想訪在不同線程中訪問同一個Xapian對象,那么你需要確保各個進程不是同時訪問(如果你不能保證的話就可能會出問題,比如數據損壞或者數據庫Crash)。有種方法防止多線程同時訪問,可以當線程訪問同一個Xapian對象的時候給線程加一個排他鎖在一個互斥量上。

Xapian不包括線程間鎖有關的代碼,這樣只會增加不必要的開銷。在實踐中,完全可以讓調用者來給那些Xapian指令加鎖,而不是讓Xapian自身做這些事。

注意,Xapian對象將保持一些內部的引用--例如,如果你調用XapianDatabase->get_document(),返回的結果XapianDocument對象將會保持一個引用指向XapianDatatbase對象,所以你沒法安全的使用XapianDatabase對象在一個線程,同時使用XapianDocument對象在另一個線程。

2.2 Indexing 標引

2.2.1 Databases 數據庫

很多Xapian操作是以XapianDatabase對象為中心的。在檢索前,被檢索的文檔要被先放到數據庫中;檢索進程然后根據給定的查詢語句從數據庫中高效選擇出最匹配的結果。文檔被放到數據庫里這個過程叫做標引

大部分信息存儲在數據庫中都是一個從Term(詞項)映射到Document(文檔)列表來存儲的。為了可以展示信息,也需要存儲文檔中的文本或者摘要。數據庫還可以包括一些比如拼寫糾正,同義詞擴展,同時開發者甚至可以存儲任意的鍵值對在數據庫中。

XapianDatabase存儲數據通常格式化為讓檢索程序可以檢索非常快的格式;Xapian沒有用其他數據庫作為存儲引擎。有很多種Xapian數據庫存儲引擎,Xapian1.2系列版本使用最主要的存儲引擎叫做Chert。Chert存儲信息在文件系統中。如果你熟悉數據結構,你可能會對這個后台使用的一種寫時復制的B+樹感興趣,如果你不懂數據結構也沒事兒。

多數存儲引擎發布以后,至少可以保證原來老版本的Xapian存儲引擎還可以用。目前,所有的存儲引擎在同一時間只支持單進程寫入;如果嘗試同時有兩個進程在寫入數據庫將會拋出XapianDabaseLockError異常,說明進程沒加鎖。但多進程同時讀是支持的。

當一個數據庫被一個讀進程打開開始讀數據的時候,這個進程就指向了一個這個數據庫的固定數據庫快照(本質上是 Multi-Version Concurrency Control多版本並發控制),發生更新操作的時候,對於這個讀進程是不可見的,除非他調用了XapianDatabase->reopen()。

目前Xapian的磁盤存儲受到由於多版本並發控制實現的限制--特別是有兩個版本存在並發的情況。所以當一個進程沒有任何限制的訪問自己的數據庫快照的時候,有一個寫進程修改了一部分數據並且提交了,然后寫進程又修改了一部分數據,如果讀進程訪問被寫進程改變的數據的時候,會接收到XapianDatabaseModifiedError。在這種情況下,讀進程可以通過XapianDatabase->reopen()方法將自身快照更新到最新版本。

這些基於磁盤的Xapian存儲引擎,當數據庫被打開並開始寫入數據的時候,數據庫會持有一個數據庫的鎖來確保不會並發的寫入。這個鎖當數據庫寫入進程關閉的時候將被釋放(或者是寫入進程死亡的時候自動釋放鎖)。

一個比較有特色的Xapian鎖原理(至少在POSIX系列操作系統)在於Xapian fork一個子進程來持有鎖,而不是主進程來持有鎖。這樣做是避免鎖由於 the slightly unhelpful semantics of fcntl locks(這句不大理解)被意外釋放。

Xapian還可以把數據庫存儲在遠程機器上,通常通過網絡協議來訪問數據庫。

在檢索的時候,Xapian可以跨數據庫檢索。Xapian會將結果合並到一起。這個特性可以和遠程數據庫相結合來處理數據集合比價大,一台機器處理不了的情況。

Xapian還支持一個特性,可以在不同機器上的數據庫間做同步復制。而且這個復制是增量復制,只復制被修改的數據;這個特性可以用來做冗余備份和負載均衡。

2.2.2 Documents 文檔

Xapian檢索結果返回的都是一個個文檔。當你構建一個新的檢索系統時,很關鍵的一件事是決定在你的系統里存儲什么樣的文檔。這將會有很多可以替換的形式。例如,對於你一搜索網頁的檢索系統,可能會很自然的選擇存儲整張網頁。然而,你可以也可以選擇存儲一個段落,或者是把幾張網頁合成一張存儲。

數據庫區分不同文檔通過一個無符號的32位Int id,也叫Document ID(文檔ID)。

組成文檔有3個單元:data(數據),terms(語詞),values(值)。我們先來討論下Term(語詞)和Data--Values在用於支持很多高級檢索方式的時候很有用。

Document Data 文檔數據

Document data是一些的二進制數據。Xapian對於這些是完全不透明的,除了存儲(通過zlib壓縮)這寫數據,其他什么都干不了,就只能當訪問的時候拿出來。

data可以用來存儲一些文本或者數據庫主鍵的id(這個主鍵id可以在外部數據庫表里查到對應的行),可以理解為衛星數據。

通常,你需要展示什么給用戶(或者是想要的到什么檢索結果),就存儲什么為Document data。Xapian沒要求強制序列化以后再存儲到document data:基於你的應用,你可以用\r\n來分割數據,也可也通過什么JSON或者XML序列化方式來存儲你的數據。

2.2.3 Trems 語詞

Terms(語詞) 是Xapian檢索時候的基礎。簡單的來說,一個檢索過程就是對檢索語句中指定的詞和每篇文檔中的語詞作比較,然后返回一個最匹配的列表。

語詞經常就是由一個文本中每個單詞通過變形(最常見的比如全部變成小寫)生成來的。也還有很多種分詞策略。

經常一個語詞在文本出出現很多次。Xapian稱這個次數為within document frequency(文檔內詞頻)並且存儲在數據庫中。這個東西對於檢索結果的權重有影響(比如 the a 這種詞)

It is also possible to store a set of positions along with each term; this allows the positions at which words occur to be used when searching, e.g., in a phrase query. These positions are usually stored as word counts (rather than character or byte counts).

用一個集合來存儲每個語詞在文檔中出現的次序也是很有必要的。對於這段的理解比較模糊。

數據庫還跟蹤記錄了幾對關於語詞的頻率;term frenquency是在文檔中幾個指定詞出現的頻率,coolection frequency是term frenquency的和。

2.2.4 Stemmers 詞根

把一個文本規范化的過程交過stemming(詞根還原)。這個過程將眾多詞的形式轉換為一種形式,例如英文中的將birds轉換為bird。

注意詞根並不一定是一個真實存在的單詞;而只是接近於單詞詞義的一種轉換,使得搜索的時候可以找到他們。例如,happy 和 happiness 可以都轉換為 happi,所以如果一個文檔包含happiness,在檢索的時候檢索happy可以找到這個文檔。

2.2.5 Term Generator 分詞器

Xapian不用讓用戶寫代碼來將文本處理成語詞用來建索引,Xapian提供了一個XapianTermGenerator類。這個類將文本分片,然后處理得出合適的語詞,然后建立語詞到文檔的索引關系。

XapianTermGenerator可以通過配置來處理分詞。他可以存儲詞在文本中的次序,還可以指定特定的前綴到分詞后的語詞,從而允許檢索的時候超找特定字段。它甚至可以添加一些信息到數據庫中用來處理拼寫糾正。

如果你正在使用XapianTermGenerator來處理文本,你非常有可能使用QueryParser(稍后會介紹)來處理檢索。

2.2.6 使用Xapian的identifiers 標示符

每個存儲在Xapian數據庫中的文檔都有一個指定的或者自增的不重復id值。也叫docid,但是Xapian是個檢索系統,不同於mysql可以通過主鍵查詢,你沒法通過docid找到文檔,所以你需要對每個文檔有個標示符。

通常文檔在你創建索引的時候Xapian已經給它指定了一個docid值,有可能你會重建索引,更新一個已經存在的文檔,或者從Xapian索引中刪除一個已經過期的文檔。有兩種方式可以達到你的目的。

一種方式是創建一個你的標示符和Xapian docid的一對一的映射。這種方式是基於你的標示符是32位的無符號整數。

另一種方式是把標示符作為一個特殊的term詞來存儲。可以給這個詞加個前綴(通常是Q)來避免和其他詞沖突。term有個長度限制(在chert存儲引擎中是245 bytes),所以如果你的標示符很長,你需要做些復雜的處理。

2.2.7 Values 值

values(文檔值)給人的感覺像是一種更靈活的terms(語詞)。每個文檔可以有很多值,這些文檔值都是文檔數據的一部分,用於更加高級的檢索。文檔值是你想對文檔數據按照某列排序的值,或者是想檢索一個時間范圍的一個時間值,或者是一個數字用來影響檢索結果現實時候排序的權重。

每個值被存儲在一個數字標號的slot(槽位)上;例如,一個文檔,有一個值代表分類在槽位0上,一個值代表價格在槽位1上,一個值代表文檔的重要程度在槽位10上。槽位的數據可以使是任何32為無符號整數,除了0xffffffff,這個槽位有個特殊的含義(它是Xapian::BAD_VALUENO)。

Xapian把槽位上的內容當做不透明的二進制字符串,甚至是數字類型。Xapian提供了一個工具函數sortable_serialise來序列化數字類型的值,轉換成二進制字符串以后才能對這個槽位上的值用范圍檢索或者排序。

很重要的一點,盡量少的在文檔的值上存儲數據,因為在檢索的時候,文檔值可能都被讀了一遍,文檔值太大會使檢索變慢。有些開發這經常寫很多文檔值,而這些值的數據本來應該存儲在文檔的data區域;能不這么做就別這樣。

2.2.8 標引限制

對於文檔的數據和標引有些限制;達到這些限制的可能很小,但是還是很值得了解的。

Term(語詞)長度

term最大長度是245字節,注意0字節在term中內部是用2個字節編碼的,所以當term中含有0字節,term最大長度就比245字節小了。

term特別長的情況很罕見,但是有一種情況,當把URL作為一個term的時候。一個變通的方式來解決term最大長度的限制的方法是可以把URL做哈希。

文檔數據 長度

文檔數據的最大長度取決於操作系統文件塊大小和一些其他因素,默認每塊是8KB,文檔數據的長度限制在100MB。

文檔值 長度

文檔值長度限制和文檔數據的長度限制差不多,但是,由於性能原因,最好存儲的文檔值長度別太大,讀100MB+ 的文檔值在檢索的時候會比較慢。

文檔ID

文檔ID是32位的,2^32-1,接近4.3億。刪除一個文檔以后,文檔ID在自增方式指定文檔ID的情況下不會被重用。你可以通過壓縮數據庫來回收這些不用的文檔ID

2.3 檢索

2.3.1 查詢

在Xapian中查詢的原理是和你所查文檔的不同有關。可以使用簡單的查找基於文本的term(語詞)或者是一個已經設定的文檔值,這些查詢都可以通過將各種不同的查詢方法進行組合最終實現復雜查詢。

簡單查詢

最基礎的查詢是查找一個文本的term(語詞)。這將會羅列出所有包含這個term(語詞)的文檔。例如,查詢term(語詞)“wood”。

查詢還可以通過指定槽位和值操作符來匹配文檔中的文檔值。

當一個查詢被執行以后,查詢結果是一個和查詢匹配成功文檔的列表和一個表示文檔和查詢匹配度的權值。

邏輯運算符

有多個查詢時,每個查詢產生一個文檔的列表,每個文檔有一個查詢匹配度。這些查詢可以被組合產生一個查詢結構樹,查詢邏輯運算符就像是樹的分支。

最基礎的運算符是邏輯運算符:OR,AND,AND_NOT

OP_OR-匹配文檔列表A和B的並集

OR_AND-匹配文檔列表A和B的交集

OP_AND_NOT-匹配文檔列表A和非B的交集

操作符對於每個匹配得文檔產生一個權值,這個權值是基於兩邊的子查詢有如下規則:

OP_OR-匹配文檔的權值A和B的和

OP_AND-匹配文檔的權值A和B的和

OP_AND_NOT-匹配文檔的權值A

Maybe

作為基礎邏輯運算符的補充,還有一種邏輯運算符OP_AND_MAYBE,這個運算符匹配文檔列表A(不管B批不匹配)。如果只有B匹配,OP_AND_MAYBE不匹配。對於這個操作符,權值是子查詢的和,所以:

1.文檔匹配A和B權值是A+B

2.文檔值匹配A權值是A

過濾器

一個查詢可以被另一個查詢過濾。有兩種方式用來過濾一個查詢是包含還是不包含文檔。

OP_FILTER-匹配兩邊子查詢的文檔,但是權值以左邊為准

OP_AND_NOT-匹配左邊子查詢,但是不匹配右邊子查詢的文檔(權值以左邊為准)

值范圍

當使用文檔值的時候,有三個相關的操作符:

OP_VALUE_LE-匹配給定值比文檔值小的

OP_VALUE_GE-匹配給定值比文檔值大的

OP_VALUE_RANGE-匹配給定值在給定文檔值范圍中間的。(閉區間)

在使用這三個操作符時,決定是包含還是不包含文檔且不影響文檔的權值。

Near和Pharse

兩個額外的常用操作符,NEAR,

2.3.2 查詢分析器

為了讓查詢Xapian數據庫更簡單,Xapian提供了一個XapianQueryParser類,可以把查詢的字符串轉換為查詢對象。例如:

apple AND a NEAR word OR "a phrase" NOT(too difficult)+eh

以上的例子展示了XapianQueryParser如何截獲一些基本的修飾符。語句支持的操作符是在之前就已經定義的。例如:

apple AND pear匹配同時出現apple和pear這兩個詞的文檔。

apple OR pear匹配任何出現apple或pear一個詞的文檔。

apple NOT pear匹配出現apple但是沒有pear的文檔。

分詞

查詢分析器通過一個內部程序將查詢語句轉換成語詞。這和在建立索引時用分詞器將字符串轉換成語詞差不多。同一個數據庫把QueryParser(查詢分析器)和TermGenerator(分詞器)一起使用比較簡單。

通配符

通配符可以匹配任意數量在詞末尾的字符串;例如:

wild* 匹配 wild,wildcard,wildcat

這個特性默認是不可用的;想要開啟他,可以參照后面的 “開啟特性”章節

括號

當一個查詢語句同時包含OR和AND操作符的時候,AND的優先級更高。使用括號可以改變這種優先級。例如,一下的查詢語句:

apple OR pear AND dessert

查詢分析器會這樣切分這條查詢語句:

apple OR (pear AND dessert)

所以,為了改變這種優先級需要使用括號。你需要在查詢語句的開頭這樣寫:

(apple OR pear) AND dessert

默認操作符

當查詢分析器獲得一個查詢語句的時候,他會鏈接各各查詢語句通過默認的操作符OP_OR。這個默認的操作符可在運行時進行修改 。

其他操作符

XapianQueryParser除了提供基礎的邏輯操作符還提供了一些新的操作符,例如:

apple NEAR dessert president "united states" -horse +recipe  +apple dessert

可以看到新的操作符+和-。

“race condition” -horse 匹配所有包含語句race condition但是不包括horse的文檔;

+recipe desert 匹配所有包含recipe的文檔;然后所有包含recipe的文檔根據權重desert排序。

注意的是所有+/-運算符基於默認運算符,以上例子默認運算符是OP_OR。

通過前綴檢索

當數據庫中的文檔已經對語詞設置好了前綴以后,查詢分析器就可以分析一個更符合人類閱讀習慣的語詞來應用到前綴字段上。例如:

author:"whilliam shakespeare" title:juliet

范圍

查詢分析器可以在文檔值上做范圍查詢,按照給定的范圍,匹配文檔值。有很多種類型的范圍查找處理器,在這里討論的兩種時間和數字。

為了使用范圍查詢,需要告訴查詢分析器哪個文檔值是將要被查詢的。然后像如下這樣查詢范圍:

$10..50

5..10$

01/01/1970..01/03/1970

size:3..7

日期時間范圍需要被配置,你可以配置哪種日期格式化方式。

停用詞

Xapian支持指定一個停用詞列表。查詢語句在處理前會移除包含停用詞列表的詞。  

解析標記

XapianQueryParser可以設置可用的操作符,設置的結果是按位或下面幾個標記:

FLAG_BOOLEAN:開啟AND OR () 表達式

FLAG_PHRASE: 開啟段落表達式

FLAG_LOVEHATE: 開啟 +/- 操作符

FLAG_BOOLEAN_ANY_CASE: 開啟大小寫轉換操作

FLAG_WILDCARD: 開啟通配符

默認情況下,XapianQueryParser 開啟的是 FLAG_BOOLEAN,FLAG_PHRASE和FLAG_LOVEHATE。

2.3.3 對結果排序

當你用Xapian完成一次查詢后,你得到的是一個已經排好序的匹配結果。

每一個匹配結果是一個含有排序權值的Xapian文檔。這個權值代表文檔與查詢語句的匹配度。更好的權值意味着更匹配。每個匹配文檔的權值從0開始。也有些其他系統把這個權值叫分數。

權值是通過一定權值策略計算而來的;Xapian自帶了一些不同的策略,盡管默認的已經還不錯,你也可以自己實現。(Xapian用一種叫BM25的策略,這種策略會考慮比如這個匹配得詞在整個數據庫中有多常見,匹配文檔的長度差異等。)

盡管你已經看過如何生成一個簡單的匹配列表,你可能還會需要通過一個基准數字和偏移量在這個列表里找一個匹配得子列表。很多檢索應用提供給用戶分頁瀏覽功能,第一頁可能是從0開始后面10個匹配結果,第二頁是從10開始后面10個匹配結果等等。

一頁匹配結果在Xapian里叫做MSet(match set的簡稱)

可選排序項

有些情況,不是根據權重排序,而是根據其他排序項來是排序。例如,可能想要根據在文檔值里存儲的日期字段來排序。

首先,你需要在文檔值存儲想要按照排序的字段,這步的做法在之前的建立索引章節講過。然后在檢索的時候告訴Xapian按照這個文檔值排序。還有可能是按照多個文檔值排序。

最后,發送查詢請求,返回按照設定排序字段排好序的文檔。

第三章 第一次實踐

3.1 索引

3.1.1 建立一個圖書館目錄

我們要建立一個基於 museum catalogue 數據的簡單檢索系統。

數據字段定義

在CSV文件里,每個字段的含義如下:

id_NUMBER:一個不重復的標示符

ITEM_NAME:一個分類

TITLE:簡短的標題

MARKER:作者的名字

DATE_MAKE:制作時間,可以是時間段,大概的時間,或者是不知道

PLACE_MAKE:制作地點

MATERIALS:制作材料

MEASUREMENTS:規模

DESCRIPTION:簡介

COLLECTION:收藏品來自哪里

3.1.2 用戶想檢索什么

我們可以設想一下用戶又很多可能想要從博物館找到的。比如,他們想找Nates做的,或者在1812年以后的,或者是Hurd-Brown公司做的東西。他們還可能想找任何用銅做的,或者不是用木頭走的,或者一米長的東西。他們可能只關心在國際鐵路博物館,或者是來自國際鐵路博物館的東西。還有可能他們想找個東西根據標題或者簡介有一個明確的詞或者短語來描述-一種用戶最常用的檢索。

為了支持所有上述這些,我們需要用到很多Xapian的特性,但是在開始之前,我們先看一個最簡單的例子:通過標題和簡介的文本檢索。

在后面的章節,我們要用到和這章相同的數據。

3.1.3 索引策略

為了索引CSV,我們提取每行標題和簡介兩個字段,並且轉化為合適的詞。對於簡單的文本檢索,我們不需要文檔值。

因為我們處理的是文本,並且全部數據庫是英文的,我們可以使用stemming(詞根還原)來處理"sundial"和"sundials"都可以匹配到同一個文檔。這樣用戶不用擔心必須使用確切的語詞。

最后,我們想用一種方式分開檢索兩個字段。在Xapian里使用前綴來完成這個,最基礎的是寫一個短的字符串在詞的前面來表示哪個字段建了索引。和前綴一樣,為了檢索文本在任何一個字段上,我們也要生成沒有前綴的語詞。

如果想和omega(一個檢索系統)或者其他兼容系統交互,有很多習慣性的前綴。用S作為標題的前綴,XD作為簡介的描述。一個習慣前綴列表omega前綴

當你對多個字段建立索引的時候,有前綴和沒有前綴的字段詞的位置要分開。比如說,有個標題”the saints“,還有個簡介”dont like rabbits?keep reading“。如果你對這連個字段建索引時候沒有間隔的話,搜索”saints dont like rabbits“會匹配到文檔,但是這本不應該匹配。通常每個字段間隔100就夠了。

我們用XapianWriteableDatabase類寫數據庫,這個類可以創建,更新,或者覆寫一個數據庫。

我們用Xapian的內置分詞器TermGenerator把文本轉換為詞。分詞器會切分單詞,清理語詞為詞根,然后添加需要的語詞前綴。他還可以幫你搞定詞的位置,包括不同字段間的間距。

3.1.4 我們來寫點代碼

parsecsv.php

<?php
/*
* Retrieves an array containing name => column associations from open file
*
* @param resource $fH Open file resource
*
* @return array Associative array of column name => column number
*/
function get_csv_headers ($fH)
{
    return fgetcsv($fH);
}

/**
* Handles file opening and error reporting if file in unavailable
*
* @param string $file Path of file to open
*
* @return resource Open file handle
*/
function open_file ($file)
{
    // Open the CSV file
    $fH = fopen($file, "r");
    if ($fH === false) {
        die("Failed to open input file {$file} for reading\n");
    }

    return $fH;
}

/**
* Reads a row of data from a CSV file and parses into UTF-8
*
* @param resource $fH Open file handle
* @param array $headers Indexed array of column names
*
* @return mixed False if EOF; indexed array of data otherwise
*/
function parse_csv_row ($fH, $headers)
{
    $row = fgetcsv($fH);
    $data = array();

    if (is_array($row) === false)
    {
        return false;
    }

    foreach ($row as $key => $value) {
        $data[$headers[$key]] = iconv('ISO-8859-1', 'UTF-8', $value);
    }

    return $data;
}
?>

 

index.php

<?php
require_once("xapian.php");
require_once("parsecsv.php");
function index($datapath, $dbpath) {
    $database = new XapianWritableDatabase($dbpath,Xapian::DB_CREATE_OR_OPEN);
    $indexer = new XapianTermGenerator();
    $indexer->set_stemmer(new XapianStem("en"));
    //打開文件
    $fp = open_file($datapath);
    //讀取CSV文件頭
    $headers = get_csv_headers($fp);
    //一行行讀文本
    while (($row = parse_csv_row($fp, $headers)) !== false) {
        $description = $row['DESCRIPTION'];
        $title = $row['TITLE'];
        $identifier = $row['id_NUMBER'];
        
        //創建文檔
        $doc = new XapianDocument();
        $indexer->set_document($doc);

        //對字段創建索引前綴
        $indexer->index_text($title, 1, 'S');
        $indexer->index_text($description, 1, 'XD');
        //對於普通文本查詢
        $indexer->index_text($title);
        $indexer->increase_termpos();
        $indexer->index_text($description);
        //所有字段存儲到數據庫            
        $doc->set_data(json_encode($row));
        //自己指定docid
        $idterm = "Q".$identifier;
        $doc->add_boolean_term($idterm);
        //添加到數據庫
        $database->replace_document($idterm, $doc);
    }
}
if ($argc < 2) {
    print "Usage: php index.php <source.csv> <target_db_path>\n";
    die();
}
try {
    index($argv[1], $argv[2]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}

 

在shell中運行代碼

php index.php NMSI_object1_20110304.csv db

3.1.5 驗證索引

Xapian通過一個叫delve的工具來監視數據庫,我們可以用這個工具看看剛才建立的數據庫。如果你運行delve db,你會得到一個概況:文檔數量,平均語詞長度,還有一些其他統計信息:

[root@localhost xapian]# delve db
UUID = 162eff82-b134-4f4c-accc-a792a8d85a31
number of documents = 65535
average document length = 75.8239
document length lower bound = 4
document length upper bound = 1484
highest document id ever used = 65535
has positional information = true

你還可以通過Xapian的docid查看某一個文檔的信息(-d 表示同時顯示數據文檔數據):

[root@localhost xapian]# delve -r 1 -d db
Data for record #1:
{"id_NUMBER":"1974-100","ITEM_NAME":"Pocket horizontal sundial","TITLE":"Ansonia Sunwatch (pocket compass dial)","MAKER":"Ansonia Clock Co.","DATE_MADE":"1922-1939","PLACE_MADE":"New York county, New York state, United States","MATERIALS":"","MEASUREMENTS":"","DESCRIPTION":"Ansonia Sunwatch (pocket compass dial)","WHOLE_PART":"WHOLE","COLLECTION":"SCM - Time Measurement"}
Term List for record #1: Q1974-100 Sansonia Scompass Sdial Spocket Ssunwatch XDansonia XDcompass XDdial XDpocket XDsunwatch ZSansonia ZScompass ZSdial ZSpocket ZSsunwatch ZXDansonia ZXDcompass ZXDdial ZXDpocket ZXDsunwatch Zansonia Zcompass Zdial Zpocket Zsunwatch ansonia compass dial pocket sunwatch

你還可以通過語詞來查看對應的文檔和統計信息

[root@localhost xapian]# delve -t Stime db
Posting List for term `Stime' (termfreq 76, collfreq 82, wdf_max 24): 41 56 58 65 364 365 414 476 477 547 554 565 604 639 657 666 673 694 699 715 721 745 756 759 761 778 788 887 888 889 890 891 892 2291 4244 4869 7450 8379 9773 9774 13138 14840 16183 16221 16222 17259 18891 19147 20232 20647 20648 21257 21634 22026 22067 22092 22093 22115 23057 23058 23059 25041 25627 27638 29248 29573 30656 30781 30920 31102 37513 38279 38527 41027 48621 64609

This means you can look documents up by identifier:

因為文檔標識符也是一個語詞,所以可以通過文檔的標識符來查看文檔

[root@localhost xapian]# delve -t Q1974-100 dbPosting List for term `Q1974-100' (termfreq 1, collfreq 0, wdf_max 0): 1

delve是個確認數據庫是不是有某個文檔和語詞很有用的工具。

3.1.6 更新數據庫

如果你回顧驗證數據庫章節,你可能會注意到,“compass”這個詞拼寫錯了,意味着你需要更新這個文檔。

因為我們使用的是CSV中的id_NUMBER字段外部ID作為文檔ID,我們可以傳遞這個ID到XapianDatabase->replace_document方法就可以更新文檔了。對index.php做下修改,只更新數據庫中的一部

分,其他部分是沒有被涉及的。

刪除文檔

通過XapianDatabase->delete_document方法作用在XapianWritableDatabase對象上刪除文檔。這個方法和XapianDatabse->replace_document一樣,可以通過Xapian的docid或者是一個唯一的語詞來刪除文檔。

delete.php

然后我們運行程序,指定根據id_NUMBER字段上的數據指定標示符:

最后,我們預期通過delve會看到在數據庫中少了兩個文檔:

3.2 檢索

現在我們數據庫中已經有了一些數據,是時候寫點代碼來檢索了。

我們想獲得用戶輸入的文本,然后再數據庫里檢索;為了完成這個目標,我們需要把文本轉換成Xapian查詢對象,這個查詢對象是由語詞和像AND,OR這種邏輯操作符組合成的樹。

有很多種方法把用戶輸入的文本轉換為查詢對象,但是最簡單的方式是使用QueryParser(查詢分析器)。

search.php

<?php
include 'xapian.php';
function search($dbpath, $querystring, $offset = 0, $pagesize = 10){    
    //打開數據庫    
    $database = new XapianDatabase($dbpath);
    //創建查詢分析器    
    $qp = new XapianQueryParser();
    $qp->set_stemmer(new XapianStem("en"));
    $qp->set_stemming_strategy(XapianQueryParser::STEM_SOME);
    //配置查詢分析其
    $qp->add_prefix("title","S");
    $qp->add_prefix("desc","XD");
    //處理查詢語句成查詢樹
    $query = $qp->parse_query($querystring);
    //創建查詢對象
    $enquire = new XapianEnquire($database);
    $enquire->set_query($query);
    //獲得查詢結果
    $matches = $enquire->get_mset($offset, $pagesize);
    $start = $matches->begin();
    $end = $matches->end();
    $index = 0;
    
    while (!($start->equals($end))) {
        $doc = $start->get_document();    
        $docid = $start->get_docid();
        $fields = json_decode($doc->get_data());
        $position = $offset + $index + 1;
        print sprintf("%d: #%03d %s\n", $position, $docid, $fields->TITLE);  
        
        $start->next();
        $index++;  
    }
}
try {
    search($argv[1], $argv[2]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}

3.3 運行檢索

運行檢索

[root@localhost xapian]# php search.php db watch
1: #377 Movement of lever watch by the American Watch Co
2: #353 German watch and watch movement
3: #472 Watch No
4: #29397 "Regloscope" electronic watch timer, with microphone
5: #640 Set of three observatory watches with keyless lever movement
6: #530 Hamilton Electric Wrist- Watch, 1957
7: #32561 Combined Watch Pedometer, English, 1795-1805
8: #505 Waterbury watch movement
9: #701 Swiss 8-day watch
10: #8369 Ticka Pocket Watch Camera, 1906-1908

結果顯示了10個匹配數據。

3.4 檢索指定字段

當我們建立索引的時候,我們通過語詞的前綴來區分了標題和簡介字段,這樣可以檢索指定的字段。

通過查詢分析器,可以像title:watch這樣檢索指定的語詞。

給查詢分析器設置查詢詞和前綴的映射

$qp->add_prefix("title","S");
$qp->add_prefix("desc","XD");

這樣可以可以指定在字段內檢索了。

[root@localhost xapian]# php search.php db title:low-loss
1: #18189 Eddystone low-loss variable capacitor
2: #21254 Wearite low loss coil unit with carton
3: #4579 Low-loss electrodynamometer wattmeter, with torsion control,

還可以通過邏輯操作符組合查詢語句。

[root@localhost xapian]# php search.php db description:\"leather case\" AND title:sundial
1: #38525 Circular slide rule, Fowler "Magnum", in leather case with d
2: #36443 Richardson's slide rule, in leather pocket case, with two de
3: #36484 Napier's bones, 17th century
4: #8060 NO DESCRIPTION ON DATABASE.
5: #10286 No description
6: #10453 No description
7: #11145 No description
8: #11158 No description
9: #14235 Soldiers of various descriptions
10: #17462 Mounted photograph of train description transmitter

 

第四章 實踐 

4.1 過濾查詢結果

之前討論了建立博物館數據的索引,我們展示了如何對標題,簡介字段分開建立前綴以達到對某一個字段來檢索。但是不是所有字段都可以通過文本來檢索,有些字段沒有包括一個可以約束的文本,例如,字段可能包括一些特殊的值或者標示符。我們希望通過這些字段的值來過濾檢索結果,而不是把這種特殊字段看成文本。

在博物館數據中,METERIALS字段可以看做一個標示符,而不是一個文本。

4.1.1 標引

當對這類字段標引的時候,我們不是想要對這種字段進行詞干提取,盡管我們想把字段的數據進行大小寫轉換,但是這不大重要。我們也不想查找這些語詞在字段中出現的次數,所以不需要存儲內文檔頻率信息。像這種類型的字段更適合作為一個boolean term(二值語詞),我們使用他是用來直接約束檢索結果,而不是作為檢索權重的一部分。

因此我們用add_boolean_term方法直接添加標示符到XapianDocument。

index_filter.php

<?php
require_once("xapian.php");
require_once("parsecsv.php");
function index($datapath, $dbpath) {
    $database = new XapianWritableDatabase($dbpath,Xapian::DB_CREATE_OR_OPEN);
    $indexer = new XapianTermGenerator();
    $indexer->set_stemmer(new XapianStem("en"));
    //打開文件
    $fp = open_file($datapath);
    //讀取CSV文件頭
    $headers = get_csv_headers($fp);
    //一行行讀文本
    while (($row = parse_csv_row($fp, $headers)) !== false) {
        $description = $row['DESCRIPTION'];
        $title = $row['TITLE'];
        $identifier = $row['id_NUMBER'];
        
        //創建文檔
        $doc = new XapianDocument();
        $indexer->set_document($doc);

        //對字段創建索引前綴
        $indexer->index_text($title, 1, 'S');
        $indexer->index_text($description, 1, 'XD');
        //對於普通文本查詢
        $indexer->index_text($title);
        $indexer->increase_termpos();
        $indexer->index_text($description);
        //對materials字段建索引
        $materials = explode(";", $row['MATERIALS']);
        foreach ($materials as $material) {
            $material = strtolower(trim($material));
            if ($material != '') {
                $doc->add_boolean_term('XM'.$material);
            }
        }
        //所有字段存儲到數據庫            
        $doc->set_data(json_encode($row));
        //自己指定docid
        $idterm = "Q".$identifier;
        $doc->add_boolean_term($idterm);
        //添加到數據庫
        $database->replace_document($idterm, $doc);
    }
}
if ($argc < 2) {
    print "Usage: php index.php <source.csv> <target_db_path>\n";
    die();
}
try {
    index($argv[1], $argv[2]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}

使用delve工具查看索引結果

[root@localhost xapian]# delve -r 3 -1 db
Term List for record #3:
...

XMglass
XMmounted
XMsand
XMtimer
XMwood
...

 

4.1.2 檢索

假設我們提供給用戶一個輸入框,讓用戶輸入想要檢索的文本,然后提供一個復選框選擇meterials。我們想返回匹配到檢索輸入,但是只包含選擇的meterials。

輸入框和復選框都是兩個Query對象,用一個OP_FILTER操作符把兩個Query結合到一起。如果有多個復選框被選擇,需要用OP_OR操作符來合成一個Query對象。

一個復雜的Query樹是通過查詢分析器和手動構建Query對象組合而成的,這樣可以支持更加靈活對查詢結果過濾。
search_filter.php

<?php
include 'xapian.php';
function search($dbpath, $querystring, $material, $offset = 0, $pagesize = 10){    
    //打開數據庫    
    $database = new XapianDatabase($dbpath);
    //創建查詢分析器    
    $qp = new XapianQueryParser();
    $qp->set_stemmer(new XapianStem("en"));
    $qp->set_stemming_strategy(XapianQueryParser::STEM_SOME);
    //配置查詢分析其
    $qp->add_prefix("title","S");
    $qp->add_prefix("desc","XD");
    //處理查詢語句成查詢樹
    $query = $qp->parse_query($querystring);
    //通過字段過濾結果
    if (isset($material)) {
        //根據material創建一個查詢對象
        $material_query = new XapianQuery('XM'.strtolower($material));
        //根據material查詢 過濾$query的查詢結果
        $query = new XapianQuery(XapianQuery::OP_FILTER, $query, $material_query);
    }
    //創建查詢對象
    $enquire = new XapianEnquire($database);
    $enquire->set_query($query);
    //獲得查詢結果
    $matches = $enquire->get_mset($offset, $pagesize);
    $start = $matches->begin();
    $end = $matches->end();
    $index = 0;
    
    while (!($start->equals($end))) {
        $doc = $start->get_document();    
        $docid = $start->get_docid();
        $fields = json_decode($doc->get_data());
        $position = $offset + $index + 1;
        print sprintf("%d: #%03d %s\n", $position, $docid, $fields->TITLE);  
        
        $start->next();
        $index++;  
    }
}
try {
    search($argv[1], $argv[2], $argv[3]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}

通過material為steel的過濾查詢結果

[root@localhost xapian]# php search_filter.php db watch steel
1: #29656 Watch fusee cutting engine.
2: #216 Pocket Watch, late 17th Century
3: #480 Automatic Wrist-watch
4: #10342 'Ticka' watch camera  by Houghton Limited, c. 1908, with ins
5: #160 Cylinder Pocket Watch in Silver and Tortoiseshell Casing, 1800-1810

4.1.3 使用查詢分析器

前面的代碼展示了通過代碼層面過濾查詢結果。這樣做有很好的靈活性,但是有些時候希望用戶可以通過輸入文本來過濾。

可以使用XapianQueryParser.add_boolean_prefix()方法。這個方法告訴查詢分析器一個用來過濾的字段和對應語詞前綴。在materials檢索中,我們只需要添加一行代碼。

 

查詢分析器通過解析"material:"

 

4.1.4 提供給查詢分析器的是什么

開發者經常會把過濾應用在用戶輸入中(例如,通過添加像material:steel在末尾)。這是個不好的方法,因為查詢分析器還包含一些啟發式的處理用戶輸入,這樣對於material:steel就不是很可靠。

提供給查詢分析器的應該是用戶直接的查詢輸入,如果你想應用過濾查詢結果,你應該把過濾應該在查詢分析器的輸出。

4.2 范圍查詢

4.2.1 我只對1980年以后的數據感興趣

在博物館數據中,我們使用了之前的例子,有一個DATE_MADE字段代表東西制作時間,所以有個很自然地事情就是用戶可能想查找在一段時間內制作的東西。假設我們想擴展原系統來滿足這個需求,我們要:

1.解析字段成常量;字段包含年,年時間段(1671-1700),靈活的年表示方法(C. 1936)和帶有說明的年表示(patented 1885)。

2.存儲字段到XapianDatabase

3.在查找時指定日期范圍

4.2.2 Xapian如何支持范圍查詢

如果你回顧一下之前介紹Xapian中的檢索,你會記得一組處理值范圍的操作符:OP_VALUE_LE,OP_VALUE_GE和OP_VALUE_RANGE。所以,你可以通過文檔值處理范圍查詢,通過操作符的組合構造合適的查詢。

因為我們通常想暴露這些功能給用戶,我們想讓用戶可以輸入一個查詢,這個查詢包含一個或多個范圍約束;查詢分析器可以支持做這些,通過使用value range processors,XapianValueRangeProcessor的子類。Xapian自帶了一些標准的rang處理類,或者你也可以編寫你自己的。

文檔值在Xapian被按照字符串排序后,操作符提供比較字符串的處理,我們需要一種方式轉換數字成字符串並存儲他們。Xapian提供了一對工具函數:sortable_serialise和sortable_unserialise,這兩個函數在浮點數和字符串間轉換。

4.2.3 創建文檔值

我們需要一個新的標引程序。index_ranges.php,通過MEASUREMENTS和DATA_MADE創建文檔值。我們可以在槽位0設置最大的規格(幸運的是數據被存儲成毫米和千克,所以我們可以假設毫米永遠是比重量大的),從DATA_MADE中分離出年存儲在槽位1(如果包含多種日期格式,我們選擇第一個可以解析的年份)。

index_ranges.php

<?php
require_once("xapian.php");
require_once("parsecsv.php");
function index($datapath, $dbpath) {
    $database = new XapianWritableDatabase($dbpath,Xapian::DB_CREATE_OR_OPEN);
    $indexer = new XapianTermGenerator();
    $indexer->set_stemmer(new XapianStem("en"));
    //打開文件
    $fp = open_file($datapath);
    //讀取CSV文件頭
    $headers = get_csv_headers($fp);
    //一行行讀文本
    while (($row = parse_csv_row($fp, $headers)) !== false) {
        $description = $row['DESCRIPTION'];
        $title = $row['TITLE'];
        $identifier = $row['id_NUMBER'];
        $measurements = $row['MEASUREMENTS'];
        $dateMade = $row['DATE_MADE'];
        //創建文檔
        $doc = new XapianDocument();
        $indexer->set_document($doc);

        //對字段創建索引前綴
        $indexer->index_text($title, 1, 'S');
        $indexer->index_text($description, 1, 'XD');
        //對於普通文本查詢
        $indexer->index_text($title);
        $indexer->increase_termpos();
        $indexer->index_text($description);
        //所有字段存儲到數據庫            
        $doc->set_data(json_encode($row));
        //添加文檔值
        if (intval($measurements) > 0) {
            $doc->add_value(0, Xapian::sortable_serialise(intval($measurements)));
        }
        if (intval($dateMade)) {
            $doc->add_value(1, Xapian::sortable_serialise(intval($dateMade)));
        }
        //自己指定docid
        $idterm = "Q".$identifier;
        $doc->add_boolean_term($idterm);
        //添加到數據庫
        $database->replace_document($idterm, $doc);
    }
}
if ($argc < 2) {
    print "Usage: php index.php <source.csv> <target_db_path>\n";
    die();
}
try {
    index($argv[1], $argv[2]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}

 

我們可以執行:

php index_range.php NMSI_object1_20110304.csv db

我們可以使用delve檢查創建的文檔值:

delve -V0 db

Value 0 for each document: 314:�� 655:�  1053:� 1114:� 1731:�@ 1812:�@ 2133:� ...

4.2.4 范圍查詢

最簡單的值范圍處理器是XapianStringValueRangeProcessor,但是我們需要兩個XapianNumberValueRangeProcessor實例。

為了區分不同的范圍,我們需要給規格字段指定后綴“mm”,而年份本來就是數字,所以我們需要先告訴查詢分析器后綴:

$qp->add_valuerangeprocessor(
        new XapianNumberValueRangeProcessor(0, 'mm', False)
); 
$qp->add_valuerangeprocessor( new XapianNumberValueRangeProcessor(1, '') );

 

第一個調用有一個參數是false代表mm是一個后綴(默認是前綴)。當使用空的字符串作為第二個參數的時候,前綴后綴就不重要了,所以第二個調用的第三個參數可以跳過。

search_ranges.php

<?php
include 'xapian.php';
function search($dbpath, $querystring, $offset = 0, $pagesize = 10){    
    //打開數據庫    
    $database = new XapianDatabase($dbpath);
    //創建查詢分析器    
    $qp = new XapianQueryParser();
    $qp->set_stemmer(new XapianStem("en"));
    $qp->set_stemming_strategy(XapianQueryParser::STEM_SOME);
    //配置查詢分析其
    $qp->add_prefix("title","S");
    $qp->add_prefix("desc","XD");
    //添加文檔值范圍處理器
    $qp->add_valuerangeprocessor(
        new XapianNumberValueRangeProcessor(0, 'mm', False)
    )
    $qp->add_valuerangeprocessor(
        new XapianNumberValueRangeProcessor(1, '')
    )
    //處理查詢語句成查詢樹
    $query = $qp->parse_query($querystring);
    //創建查詢對象
    $enquire = new XapianEnquire($database);
    $enquire->set_query($query);
    //獲得查詢結果
    $matches = $enquire->get_mset($offset, $pagesize);
    $start = $matches->begin();
    $end = $matches->end();
    $index = 0;
    
    while (!($start->equals($end))) {
        $doc = $start->get_document();    
        $docid = $start->get_docid();
        $fields = json_decode($doc->get_data());
        $position = $offset + $index + 1;
        print sprintf("%d: #%03d %s\n", $position, $docid, $fields->TITLE);  
        
        $start->next();
        $index++;  
    }
}
try {
    search($argv[1], $argv[2]);
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}
View Code

 

我們現在可以通過查詢“..50mm”來約束規格,通過“1980..1989”來約束年份:

[root@localhost xapian]# php search_range.php db ..50mm
1: #1053 Two pieces of glass, partly blown to form contact lenses
2: #1114 Two plastic plano-convex lenses used for correcting the imag
3: #1731 Quartz Fibres by Sir Charles Vernon Boys, 1887-1892
4: #1812 Steel Specimens showing Colours for Tempering, 1801-1900
5: #2133 Ten millimetre cube of synthetic proustite with two polished
6: #2137 Two Helical flash tubes used for pumping a ruby laser c
7: #6017 The Works of Messrs. Perkin & Sons at Greenford Green
8: #6848 [Mounted sepia photos (2) of the birthplace of John Dalton.
9: #8899 Comparative Scales, Compiled from Tables of eminent authorities ...
10: #9099 Ms. laboratory notebook, ca.1956-1959, compiled by Dr. R.P.H

你可以把普通查詢和范圍查詢組合到一起,比如查詢在1960年以后做的clocks

//shell

還可以把兩個范圍查詢組合,例如查詢規格比較大的並且生產與20世紀以后的:

//shell

注意,1000..mm,后綴必須跟在整個查詢的后面;也可以跟在前面(比如1000mm..mm)。同理,100mm..200mm或者100..200mm都是合法的,但是100mm..200是不合法的。這個規則同樣適用於前綴。

如果你輸入了不合法的查詢,查詢分析器會拋出一個QueryParserError:

//shell

4.2.5 處理日期

為了約束日期范圍,我們需要決定如何儲存日期數據到文檔值,和如何想要用戶輸入數據來查詢日期范圍。XapianDateValueRangeProcessor,通過儲存按照"YYYYMMDD"格式化的日期字符串,也可以格式化為美國的日期類型(month/day/year)或者歐洲類型(day/month/year)。

為了展示他是怎么工作的,我們要用一個不同的數據集,因為圖書館數據指給了每個東西制作的年份;我們已經創建了一些數據,美國50個洲的信息。這個CSV文件states.csv

我們需要一個新的標引程序

index_ranges2.php

//code

他存儲個數字通過sortable_srialise函數:在槽位1存儲了這個洲成立的年,槽位3存儲了這個洲的人口。在槽位2存儲了格式為“YYYYMMDD”的這個洲成立的日期。

我們用和之前同樣的方式創建索引

//shell

然后,我們改變值值范圍處理器並添加到查詢分析器上

//code

XapianDateValueRangeProcessor作用在槽位2上,1860是紀元開始(所以兩位表示的年份會被認為從1860年開始)。第二個參數代表是否日期是美國日期類型;因為我們看過數據中的US states,都是美國日期類型,所以這里是true,XapianNumberValueRangeProcessor在前面已經介紹了。

查找所有20世紀成立的洲:

//shell

上面例子使用到了作用在槽位1上的XapianNumberValueRangeProcessor。我們下面查找在1889年11月8號到1890年7月10號之間成立的洲:

//shell

上面例子使用到了作用在槽位2上的XapianDateValueRangeProcessor;這個范圍處理器沒法處理年份范圍的查找,這也是為什么我們要對槽位1和槽位2分別標引。

4.2.6 編寫你自己的ValueRangeProcessor

我們還沒有對人口字段做什么操作。我們想要做的是對人口字段做類似於XapianNumberValueRangeProcessor的行為。如果我們在作用在槽位1(年份)的XapianNumberValueRangeProcessor前面添加,它會把把任何內容都看做是人口范圍,把任何其他的看做年份的范圍。

為了完成這件事,我們需要知道XapianValueRangeProcessor是如何被查詢分析器調用的。每個范圍處理器輪流被傳遞開始和結束范圍。如果它不理解范圍,它會返回Xapian.BAD_VALUENO。如果它理解范圍,它會返回XAPIAN.Query.OP_VALUE_RANGE可用的值,它也可以修改開始和結束值(轉換開始和結束值為正確的格式,讓Xapian.OP_VALUE_RANGE比較使用)。

我們要做的是寫一個XapianValueRangeProcessor的子類來接受數字范圍五十萬到五千萬;這個范圍和數據集里年份的范圍沒有交集,並且可以包含人口數目的全部范圍。如果數字不在這個范圍,我們會返回Xapian.BAD_VALUENO,然后查詢分析器會轉到下一個XapianValueRangeProcessor。

PopulationValueRangeProcessor.class.php

//code

我們然后超找人口超過一千萬的

//shell

或者查詢十八世紀80年代人口超過一千萬的

//shell

想要更完善這個查詢,我們還可以支持“..750k”代表75萬。

4.2.7 性能限制

如果沒有其他的語詞查詢,一個XapianValueRangePrcessor將會引起在整個數據庫里查詢,這意味着會載入全部在給定槽位上的文檔值。在一個小的數據庫上,這不是問題,但是對於一個大的數據庫,它會有性能影響:這可能是個慢查詢。

如果和合適的語詞查詢相結合(例如OP_AND 添加一個或多個語詞),這個性能問題會被減小,因為范圍運算符將會只在已經匹配得結果里執行,這樣避免了全數據庫查詢。

我們可以把文檔值分組,可以提供一個基於語詞的查詢,盡管用戶只關心一個純范圍查詢。例如,把人口按照一定范圍分組,你可以分配一個語詞描述每一組,我們這里使用前綴XP。

Population range   Term

0 - 10 million        XP0

10 - 20 million      XP1

20 - 30 million      XP2

30 - 40 million      XP3

假如用戶查詢“..15000000”,我們先把兩個語詞查詢"XP0"和"XP1"用OP_AND連接,然后把這個查詢和查詢分析器生成的查詢對象用OP_FILTER組合。

4.3 分面搜索 

Xapian提供了可以讓你在匹配的文檔結果中,在動態的根據篩選項生成結果的功能。例如對於分面,顏色,制造商,尺寸都是候選的篩選項。

這個功能有很多潛在的用途,但是最常見的用途是提供給用戶通過指定篩選值或者類別,縮小他們檢索結果的能力。這常被成為分面搜索。

xapian的分面搜索解決的不是分面以后的篩選結果展示問題,根據篩選結果的展示是之前講的filter,range解決的問題。這里的分面獲取的是每個分面詞出現的頻率。

4.3.1 標引

實現分面搜索標引沒有額外的工作需要做,除了確保你希望使用在分面時候的值已經存儲為文檔值。

index_factes.php

//code

這里我們使用兩個文檔值,槽位0儲存收藏品,槽位1儲存制作者的名字。我們已經知道文檔數據來自固定的列表,所以我們不需要擔心在使用兩列的值作為分面前正常化他們的值。下面我們對數據標引:

//shell

4.3.2 檢索 

你需要創建一個XapianValueCountMatchSpy對象來,如下陳述了spy添加到XapianEnquire:

//code

我們在槽位1,也就是作者上使用分面搜索。你得到MSet之后,可以通過spy查詢周到的分面和對應的頻率。

注意,盡管我們通常只顯示10個匹配結果,我們對get_mset()用一個叫做checkatleast的參數,為了使整個數據都被考慮,分面的頻率是正確。這里spy統計分面頻率的時候在10000個記錄里統計。輸出如下:

//shell

注意,spy會按照字母順序返回結果,而不是按照頻率順序。

如果你想操作多個分面,你需要在執行get_mset()前注冊多個XapianValueCountMatchSpy對象,盡管每個添加的spy會有一些性能影響。

4.3.3 通過分面約束

如果你正在使用分面提供給用戶選擇,來縮小檢索結果,你需要應用一個合適的過濾器。

對於單一值,你需要使用XapianQuery.OP_VALUE_RANGE的開始和結束值一樣,或者XpianMatchDecider,但是最高效的是使用一個合適的語詞前綴來過濾。

4.3.4 限制

Xapian的分面搜索能力決定於Xapian搜索時檢查記錄的數量。你可以通過指定get_mset的checkatleast的值來控制這個數字。但是很重要的一點是增加這個值會對查詢性能有影響。

4.4 排序

默認的,xapian的搜過結果是按照相關度遞減排列的。但是,也允許按照其他標准排序,或者是其他標准和相關度的組合。

如果兩個和多個結果比較的排序標准相同,則順序決定於文檔ID。默認的,文檔ID按照升序排列(小的文檔id代表更好,更靠前),但是可以通過使用$enquire->set_docid_order($enquire::DESCENDING)選擇降序排列;如果你對排序不關心,你可以使用$enquire->set_docid_order($enquire::DONET_CARE)將會是最高效的。

也有可能改變相關度的計算公式,了解詳細,請轉到 文檔權重表和打分

4.4.1 根據值排序

你可以根據比較指定的文檔值來對文檔排序。注意比較的文檔值是按字節來比較的,所以1<10<2。如果你想對值編碼用於對數字排序,請使用Xapian->sortable_serialise()在標引的時候對值編碼-以上同樣適用於整型和浮點型值:

$doc->add_value(0, Xapian::sortable_serialise($price));

有三個方法用於指定文檔值如何被用來排序:

$enquire->set_sort_by_value() 指定相關度一點都不影響排序。

$enqure->set_sort_by_value_then_relevance() 指定相關度是用於排序那些文檔值相同的文檔。

$enquire->set_sort_by_relevance_then_value() 指定文檔按照相關度排序,文檔值只用於排序相關度值相同的文檔(注意:權重需要是完全一樣文檔值才決定排序,所以這個方法當使用BM25作為默認的參數不是很有用,因為不同文檔很少有相同的分數)。

我們將會用美國州數據來驗證,代碼使用范圍查詢時候的標引程序:

//shell

這里有三個文檔值:槽位1代表州被承認的年份,槽位2是時間(YYYYMMDD格式),槽位3是州人口數。所以,如果我們想根據年份排序,然后根據相關度排序,我們需要再get_mset前添加如下代碼:

//code

最后的參數false是升序排列,true是降序排列。我們然后執行查詢程序:

//shell

4.4.2 根據多個值排序

有一個標准的子類XapianMultiValueKeyMaker可以根據多個文檔值排序。

我們用這個改寫之前的檢索程序,先根據年份排序,再根據人口降序排序。

//code

正如Enquire方法,add_value的第二個參數控制了使用升序或者降序排列。所以現在我們可以執行一個更復雜的排序檢索:

//shell

4.4.3 生成keys的其他用途

Xapian::KeyMaker可以被繼承來計算排序。例如,“根據地理位置距離排序”,子類可以處理用戶的經緯度,然后對文檔按照距離用戶最近的排序。

為了完成這些,我們將存儲各個州的地理坐標在文檔值。states.csv

//code

把地理信息存儲到文檔值:

//shell

現在我們需要一個KeyMaker;我們讓他返回一個key,這個key是按照距離華盛頓的距離排序的。

執行示例代碼如下:

//shell

4.5 對結果去重

xapian提供從MSet消除復制文檔的功能。這個特性被稱作"collapsing"-很多副本只保留一個結果。

去重總是移除最差排名的文檔(如果是根據相關度排序,是低權值的;如果是根據值排序,是排序中低的)。

判斷兩個文檔一個是不是另一個的復制決定於他們的"collapse key"。如果一個文檔有一個空的colapse key,他將永遠不會被判斷為復制,兩個有相同colapse kye的文檔會被去重。

collapse key是取自一個你指定槽位的文檔值(通過Enquire->set_colapse_key()方法),但是以后你可以通過XapianKeyMaker動態創建colapse key。

4.5.1 性能

去重是在匹配運行時被執行的,所以是很高效的,這種方法比生成一個很大的MSet后再去重好很多。

4.5.2 API

調用$Enquire->set_collapse_key(),第一個參數傳遞文檔值所在的槽位序號,第二個參數設置相同collapse key保存的文檔數量(沒有指定默認是1),例如:

//code

當你得到XapianMSet對象,你可以獲取collapse key通過XapianMSetIterator->get_collapse_key()方法,獲取collapse count通過XapianMSetIterator->get_collapse_count()方法。

注意的是,如果你有可能主動中斷查詢,collapse count將會出錯,是0或者1。

4.5.3 統計

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM