轉(zhuǎn)自:http://www.ibm.com/developerworks/cn/opensource/os-php-sphinxsearch/index.html
搜索車身零件
假定 Body-Parts.com 出售車身零件 —— 擋泥板、鉻、緩沖器等 —— 用于珍貴且值得收藏的汽車。正如在現(xiàn)實世界中,Body Parts 站點的訪問者很可能按制造商(比如保時捷或制造同類零件的第三方制造商)、零件號、產(chǎn)地、車型、年份、條件(二手、全新、翻新)以及描述或者這些屬性的某種組合來搜索零件。
要構(gòu)建 Body Parts 搜索功能,讓我們使用 MySQL V5.0 作為數(shù)據(jù)存儲并使用 Sphinx search 守護程序來提供快速而精確的文本搜索。MySQL V5.0 是一個功能強大的數(shù)據(jù)庫,但是它的增強型全文本搜索功能并不特別豐富。實際上,它僅限于 MyISAM 表 —— 不支持外鍵的一種表格式,因此使用有限。
清單 1 至清單 4 顯示了與此示例相關的 Body Parts 模式的部分代碼。您將分別看到 Model(清單 1)、Assembly(清單 2)、Inventory(清單 3)和 Schematic(清單 4)表。
Model 表
清單 1 中所示的 Model 表十分簡單:label 列將列舉車型的名稱 (“Corvette”);description 使用客戶友好方式進行描述(“兩門跑車;第一年引入”);而 begin_production 和 end_production 分別表示開始生產(chǎn)和結(jié)束生產(chǎn)該車型的年份。由于前述列中的值并不惟一,因此使用一個獨立 ID 表示每四個這樣的元素(label、description、begin_production、end_production),并且是其他表中的外鍵。
清單 1. 車身零件 Model 表
CREATE TABLE Model (
id int(10) unsigned NOT NULL auto_increment,
label varchar(7) NOT NULL,
description varchar(256) NOT NULL,
begin_production int(4) NOT NULL,
end_production int(4) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
下面是 Model 表的一些樣例數(shù)據(jù):
INSERT INTO Model
(`id`, `label`, `description`, `begin_production`, `end_production`)
VALUES
(1,'X Sedan','Four-door performance sedan',1998,1999),
(3,'X Sedan','Four door performance sedan, 1st model year',1995,1997),
(4,'J Convertible','Two-door roadster, metal retracting roof',2002,2005),
(5,'J Convertible','Two-door roadster',2000,2001),
(7,'W Wagon','Four-door, all-wheel drive sport station wagon',2007,0);
Assembly 表
assembly 是一個子系統(tǒng),例如汽車上安裝的傳動裝置或所有玻璃。車主使用部件圖及相關零件列表來查找備件。清單 2 中所示的 Assembly 表也十分簡單:它將把一個惟一 ID 與部件標簽和描述關聯(lián)起來。
清單 2. Assembly 表
CREATE TABLE Assembly (
id int(10) unsigned NOT NULL auto_increment,
label varchar(7) NOT NULL,
description varchar(128) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
繼續(xù)示例,下面是 Assembly 表的一些樣例數(shù)據(jù):
INSERT INTO Assembly
(`id`, `label`, `description`)
VALUES
(1,'5-00','Seats'),
(2,'4-00','Electrical'),
(3,'3-00','Glasses'),
(4,'2-00','Frame'),
(5,'1-00','Engine'),
(7,'101-00','Accessories');
Inventory 表
Inventory 表是汽車零件的典范列表。零件 —— 例如螺釘或燈泡 —— 可能用于每輛汽車和多個部件中,但是零件只在 Inventory 表中顯示一次。Inventory 表中的每行包含:
使用了惟一的 32 位整數(shù) serialno 標識行。
字母數(shù)字零件號(此零件號惟一并且可以用作主鍵。但是,由于它可以包含字母數(shù)字字符,因此它不適于與 Sphinx 結(jié)合使用,Sphinx 要求索引的每條記錄都有一個惟一的 32 位整型鍵)。
文本描述。
價格。
Inventory 表的規(guī)范如清單 3 中所示:
清單 3. Inventory 表
CREATE TABLE Inventory (
id int(10) unsigned NOT NULL auto_increment,
partno varchar(32) NOT NULL,
description varchar(256) NOT NULL,
price float unsigned NOT NULL default '0',
PRIMARY KEY (id),
UNIQUE KEY partno USING BTREE (partno)
) ENGINE=InnoDB;
零件的(部分)列表可能如下面所示:
INSERT INTO `Inventory`
(`id`, `partno`, `description`, `price`)
VALUES
(1,'WIN408','Portal window',423),
(2,'ACC711','Jack kit',110),
(3,'ACC43','Rear-view mirror',55),
(4,'ACC5409','Cigarette lighter',20),
(5,'WIN958','Windshield, front',500),
(6,'765432','Bolt',0.1),
(7,'ENG001','Entire engine',10000),
(8,'ENG088','Cylinder head',55),
(9,'ENG976','Large cylinder head',65);
Schematic 表
Schematic 表將把零件與部件和車型版本綁定在一起。因此,將使用 Schematic 表來查找組裝 1979 J Class 敞篷車引擎的所有零件。Schematic 表中的每行都有一個惟一 ID,一個引用 Inventory 表行的外鍵,一個標識部件的外鍵,以及用于引用 Model 表中特定型號和版本的另一個鍵。各行如清單 4 所示:
清單 4. Schematic 表
CREATE TABLE Schematic (
id int(10) unsigned NOT NULL auto_increment,
partno_id int(10) unsigned NOT NULL,
assembly_id int(10) unsigned NOT NULL,
model_id int(10) unsigned NOT NULL,
PRIMARY KEY (id),
KEY partno_index USING BTREE (partno_id),
KEY assembly_index USING BTREE (assembly_id),
KEY model_index USING BTREE (model_id),
FOREIGN KEY (partno_id) REFERENCES Inventory(id),
FOREIGN KEY (assembly_id) REFERENCES Assembly(id),
FOREIGN KEY (model_id) REFERENCES Model(id)
) ENGINE=InnoDB;
為了強化表的意圖,下面是 Schematic 中的一張小型行列表:
INSERT INTO `Schematic`
(`id`, `partno_id`, `assembly_id`, `model_id`)
VALUES
(1,6,5,1),
(2,8,5,1),
(3,1,3,1),
(4,5,3,1),
(5,8,5,7),
(6,6,5,7),
(7,4,7,3),
(8,9,5,3);
搜索表
定義了這些表后,就可以輕松地響應很多搜索:
顯示特定型號的所有版本
列出裝配特殊型號和版本所需的所有部件
顯示構(gòu)成特定型號和版本的特殊部件的所有零件
但是很多搜索代價較大:
查找所有模型和版本中出現(xiàn)零件號開頭為 “WIN” 的所有零件
查找描述中有 “l(fā)acquer” 或 “paint” 的那些零件
查找描述中有 “black leather” 的所有零件
查找描述中有 “paint” 的所有 2002 J 系列零件
這些搜索中的每個搜索都要求使用長篇的 JOIN 子句或代價高昂的 LIKE 子句,尤其是在 Inventory 表和 Schematic 表十分大時更是如此。而且,復雜的文本搜索完全超出了 MySQL 的能力。要搜索大量文本數(shù)據(jù),請考慮構(gòu)建和使用 Sphinx 索引。
回頁首
集成 Sphinx 軟件
要應用 Sphinx 來解決問題,您必須定義一個或多個數(shù)據(jù)源以及一個或多個索引。
source 將標識數(shù)據(jù)庫來建立索引,提供驗證信息,并且定義查詢用以構(gòu)造每行。數(shù)據(jù)源可以隨意地標識一列或多列作為過濾器,Sphinx 將之稱為組。您將使用組來過濾結(jié)果。例如,單詞描述可能得到 900 個匹配。如果只對特定型號的汽車匹配感興趣,則可以進一步使用型號組進行過濾。
index 將要求獲得數(shù)據(jù)源(即一組數(shù)據(jù)行)并定義應當如何為已從數(shù)據(jù)源中提取出來的數(shù)據(jù)編目。
您將在 sphinx.conf 文件中定義數(shù)據(jù)源和索引。Body Parts 的數(shù)據(jù)源是 MySQL 數(shù)據(jù)庫。清單 5 顯示了名為 catalog 的數(shù)據(jù)源的部分定義 —— 指定連接的數(shù)據(jù)庫以及如何建立連接(主機、套接字、用戶和密碼)的代碼片段。
清單 5. 用于訪問 MySQL 數(shù)據(jù)庫的設置
source catalog
{
type? ? ? ? ? ? ? ? ? ? ? ? ? ? = mysql
sql_host? ? ? ? ? ? ? ? ? ? ? ? = localhost
sql_user? ? ? ? ? ? ? ? ? ? ? ? = reaper
sql_pass? ? ? ? ? ? ? ? ? ? ? ? = s3cr3t
sql_db? ? ? ? ? ? ? ? ? ? ? ? ? = body_parts
sql_sock? ? ? ? ? ? ? ? ? ? ? ? =? /var/run/mysqld/mysqld.sock
sql_port? ? ? ? ? ? ? ? ? ? ? ? = 3306
接下來,創(chuàng)建一個查詢以生成要被索引的行。通常,將創(chuàng)建 SELECT 子句,可能需要把許多表 JOIN 在一起才能得到行。但這里存在一個問題:搜索型號和年份必須使用 Assembly 表,但是零件號和零件描述只能在 Inventory 表中找到。為此,Sphinx 必須能夠把搜索結(jié)果與 32 位整型主鍵綁定在一起。
要獲得右側(cè)表單中的數(shù)據(jù),需要創(chuàng)建一個視圖 —— MySQL V5 中的新結(jié)構(gòu),它將把來自其他表的列整合到單獨的合成虛擬表中。使用視圖,各類搜索所需的所有數(shù)據(jù)都在一個位置,但是活動數(shù)據(jù)實際上存在于其他表中。清單 6 顯示了定義 Catalog 視圖的 SQL。
清單 6. Catalog 視圖將把數(shù)據(jù)整合到虛擬表中
CREATE OR REPLACE VIEW Catalog AS
SELECT
Inventory.id,
Inventory.partno,
Inventory.description,
Assembly.id AS assembly,
Model.id AS model
FROM
Assembly, Inventory, Model, Schematic
WHERE
Schematic.partno_id=Inventory.id
AND Schematic.model_id=Model.id
AND Schematic.assembly_id=Assembly.id;
如果用前面所示的表和數(shù)據(jù)創(chuàng)建名為 body_parts 的數(shù)據(jù)庫,則 Catalog 視圖應當類似以下內(nèi)容:
mysql> use body_parts;
Database changed
mysql> select * from Catalog;
+----+---------+---------------------+----------+-------+
| id | partno? | description? ? ? ? | assembly | model |
+----+---------+---------------------+----------+-------+
|? 6 | 765432? | Bolt? ? ? ? ? ? ? ? |? ? ? ? 5 |? ? 1 |
|? 8 | ENG088? | Cylinder head? ? ? |? ? ? ? 5 |? ? 1 |
|? 1 | WIN408? | Portal window? ? ? |? ? ? ? 3 |? ? 1 |
|? 5 | WIN958? | Windshield, front? |? ? ? ? 3 |? ? 1 |
|? 4 | ACC5409 | Cigarette lighter? |? ? ? ? 7 |? ? 3 |
|? 9 | ENG976? | Large cylinder head |? ? ? ? 5 |? ? 3 |
|? 8 | ENG088? | Cylinder head? ? ? |? ? ? ? 5 |? ? 7 |
|? 6 | 765432? | Bolt? ? ? ? ? ? ? ? |? ? ? ? 5 |? ? 7 |
+----+---------+---------------------+----------+-------+
8 rows in set (0.00 sec)
在視圖中,字段 id 將指回 Inventory 表中的零件條目。partno 和 description 列是要搜索的主要文本,而 assembly 和 model 列用作進一步過濾結(jié)果的組。視圖就緒后,構(gòu)造數(shù)據(jù)源查詢就是小事一樁。清單 7 顯示了 catalog 數(shù)據(jù)源定義的其余部分。
清單 7. 查詢創(chuàng)建待索引的行
# indexer query
# document_id MUST be the very first field
# document_id MUST be positive (non-zero, non-negative)
# document_id MUST fit into 32 bits
# document_id MUST be unique
sql_query? ? ? ? ? ? ? ? ? ? ? = \
SELECT \
id, partno, description, \
assembly, model \
FROM \
Catalog;
sql_group_column? ? ? ? ? ? ? ? = assembly
sql_group_column? ? ? ? ? ? ? ? = model
# document info query
# ONLY used by search utility to display document information
# MUST be able to fetch document info by its id, therefore
# MUST contain '$id' macro
#
sql_query_info? ? ? ? ? = SELECT * FROM Inventory WHERE id=$id
}
sql_query 必須包括后續(xù)查找需要使用的主鍵,并且它必須包括需要索引和用作組的所有字段。兩個 sql_group_column 條目將聲明 Assembly 和 Model 可用于過濾結(jié)果。并且 search 實用程序?qū)⑹褂?sql_query_info 來查找匹配記錄。在查詢中,$id 被替換為 searchd 返回的每個主鍵。
最后一個配置步驟是構(gòu)建索引。清單 8 顯示了數(shù)據(jù)源 catalog 的索引。
清單 8. 描述 catalog 數(shù)據(jù)源的一個可能的索引
index catalog
{
source? ? ? ? ? ? ? ? ? = catalog
path? ? ? ? ? ? ? ? ? ? = /var/data/sphinx/catalog
morphology? ? ? ? ? ? ? = stem_en
min_word_len? ? ? ? ? ? = 3
min_prefix_len? ? ? ? ? = 0
min_infix_len? ? ? ? ? = 3
}
第 1 行將指向 sphinx.conf 文件中的指定數(shù)據(jù)源。第 2 行將定義存儲索引數(shù)據(jù)的位置;按照約定,Sphinx 索引將被存儲到 /var/data/sphinx 中。第 3 行將允許索引使用英文詞法。并且第 5 行至第 7 行將告訴索引器只索引含有三個字符或更多字符的那些單詞,并且為每個這樣的字符的子字符串創(chuàng)建中綴索引(為了便于引用,清單 9 顯示了 Body Parts 的完整示例 sphinx.conf 文件)。
清單 9. Body Parts 的示例 sphinx.conf
source catalog
{
type? ? ? ? ? ? ? ? ? ? ? ? ? ? = mysql
sql_host? ? ? ? ? ? ? ? ? ? ? ? = localhost
sql_user? ? ? ? ? ? ? ? ? ? ? ? = reaper
sql_pass? ? ? ? ? ? ? ? ? ? ? ? = s3cr3t
sql_db? ? ? ? ? ? ? ? ? ? ? ? ? = body_parts
sql_sock? ? ? ? ? ? ? ? ? ? ? ? =? /var/run/mysqld/mysqld.sock
sql_port? ? ? ? ? ? ? ? ? ? ? ? = 3306
# indexer query
# document_id MUST be the very first field
# document_id MUST be positive (non-zero, non-negative)
# document_id MUST fit into 32 bits
# document_id MUST be unique
sql_query? ? ? ? ? ? ? ? ? ? ? = \
SELECT \
id, partno, description, \
assembly, model \
FROM \
Catalog;
sql_group_column? ? ? ? ? ? ? ? = assembly
sql_group_column? ? ? ? ? ? ? ? = model
# document info query
# ONLY used by search utility to display document information
# MUST be able to fetch document info by its id, therefore
# MUST contain '$id' macro
#
sql_query_info? ? ? ? ? = SELECT * FROM Inventory WHERE id=$id
}
index catalog
{
source? ? ? ? ? ? ? ? ? = catalog
path? ? ? ? ? ? ? ? ? ? = /var/data/sphinx/catalog
morphology? ? ? ? ? ? ? = stem_en
min_word_len? ? ? ? ? ? = 3
min_prefix_len? ? ? ? ? = 0
min_infix_len? ? ? ? ? = 3
}
searchd
{
port = 3312
log = /var/log/searchd/searchd.log
query_log = /var/log/searchd/query.log
pid_file = /var/log/searchd/searchd.pid
}
底部的 searchd 部分將配置 searchd 守護程序本身。該部分中的條目不言自明。query.log 尤為有用:它將在運行時顯示每次搜索并顯示結(jié)果,例如搜索的文檔數(shù)和匹配總數(shù)。
回頁首
構(gòu)建和測試索引
您現(xiàn)在已經(jīng)準備好為 Body Parts 應用程序構(gòu)建索引。為此,需要執(zhí)行以下步驟:
鍵入 $ sudo mkdir -p /var/data/sphinx 創(chuàng)建目錄結(jié)構(gòu) /var/data/sphinx
假定 MySQL 正在運行,使用如下所示的代碼運行索引器來創(chuàng)建索引。
清單 10. 創(chuàng)建索引
$ sudo /usr/local/bin/indexer --config /usr/local/etc/sphinx.conf --all
Sphinx 0.9.7
Copyright (c) 2001-2007, Andrew Aksyonoff
using config file '/usr/local/etc/sphinx.conf'...
indexing index 'catalog'...
collected 8 docs, 0.0 MB
sorted 0.0 Mhits, 82.8% done
total 8 docs, 149 bytes
total 0.010 sec, 14900.00 bytes/sec, 800.00 docs/sec
注:-all 參數(shù)將重構(gòu) sphinx.conf 中列出的所有索引。如果不需要重構(gòu)所有索引,您可以使用其他參數(shù)只對部分索引進行重構(gòu)。
您現(xiàn)在可以使用如下所示的代碼用 search 實用程序測試索引(不必運行 searchd 即可使用 search)。
清單 11. 用 search 測試索引
$ /usr/local/bin/search --config /usr/local/etc/sphinx.conf ENG
Sphinx 0.9.7
Copyright (c) 2001-2007, Andrew Aksyonoff
index 'catalog': query 'ENG ': returned 2 matches of 2 total in 0.000 sec
displaying matches:
1. document=8, weight=1, assembly=5, model=7
id=8
partno=ENG088
description=Cylinder head
price=55
2. document=9, weight=1, assembly=5, model=3
id=9
partno=ENG976
description=Large cylinder head
price=65
words:
1. 'eng': 2 documents, 2 hits
$ /usr/local/bin/search --config /usr/local/etc/sphinx.conf wind
Sphinx 0.9.7
Copyright (c) 2001-2007, Andrew Aksyonoff
index 'catalog': query 'wind ': returned 2 matches of 2 total in 0.000 sec
displaying matches:
1. document=1, weight=1, assembly=3, model=1
id=1
partno=WIN408
description=Portal window
price=423
2. document=5, weight=1, assembly=3, model=1
id=5
partno=WIN958
description=Windshield, front
price=500
words:
1. 'wind': 2 documents, 2 hits
$ /usr/local/bin/search \ --config /usr/local/etc/sphinx.conf --filter model 3 ENG
Sphinx 0.9.7
Copyright (c) 2001-2007, Andrew Aksyonoff
index 'catalog': query 'ENG ': returned 1 matches of 1 total in 0.000 sec
displaying matches:
1. document=9, weight=1, assembly=5, model=3
id=9
partno=ENG976
description=Large cylinder head
price=65
words:
1. 'eng': 2 documents, 2 hits
第一條命令 /usr/local/bin/search --config /usr/local/etc/sphinx.conf ENG 在零件號中找到了兩個含有 ENG 的結(jié)果。第二條命令 /usr/local/bin/search --config /usr/local/etc/sphinx.conf wind 在兩個零件描述中找到了子字符串 wind。而第三條命令把結(jié)果限定為 model 為 3 的條目。
回頁首
編寫代碼
最后,您可以編寫 PHP 代碼來調(diào)用 Sphinx 搜索引擎。Sphinx PHP API 非常小并且易于掌握。清單 12 是一個小型 PHP 應用程序,用于調(diào)用 searchd 以得到使用上面所示的最后一條命令得到的相同結(jié)果(“在屬于型號 3 的名稱中找到含有 ‘cylinder’ 的所有零件”)。
清單 12. 從 PHP 調(diào)用 Sphinx 搜索引擎
SetServer( "localhost", 3312 );
$cl->SetMatchMode( SPH_MATCH_ANY? );
$cl->SetFilter( 'model', array( 3 ) );
$result = $cl->Query( 'cylinder', 'catalog' );
if ( $result === false ) {
echo "Query failed: " . $cl->GetLastError() . ".\n";
}
else {
if ( $cl->GetLastWarning() ) {
echo "WARNING: " . $cl->GetLastWarning() . "
";
}
if ( ! empty($result["matches"]) ) {
foreach ( $result["matches"] as $doc => $docinfo ) {
echo "$doc\n";
}
print_r( $result );
}
}
exit;
?>
要測試代碼,需要為 Sphinx 創(chuàng)建 log 目錄,啟動 searchd,然后運行 PHP 應用程序,如下所示:
清單 13. PHP 應用程序
$ sudo mkdir -p /var/log/searchd
$ sudo /usr/local/bin/searchd --config /usr/local/etc/sphinx.conf
$ php search.php
9
Array
(
[fields] => Array
(
[0] => partno
[1] => description
)
[attrs] => Array
(
[assembly] => 1
[model] => 1
)
[matches] => Array
(
[9] => Array
(
[weight] => 1
[attrs] => Array
(
[assembly] => 5
[model] => 3
)
)
)
[total] => 1
[total_found] => 1
[time] => 0.000
[words] => Array
(
[cylind] => Array
(
[docs] => 2
[hits] => 2
)
)
)
輸出為 9:匹配的單行的正確主鍵。如果 Sphinx 找到匹配,相關數(shù)組 $result 將包含名為 results 的元素。瀏覽 print_r() 的輸出以查看返回的其他內(nèi)容。
注意事項:total_found 是在索引中找到的匹配總數(shù),而 found 是返回的結(jié)果數(shù)。這兩者可能不同,因為您可以更改每次返回多少個匹配結(jié)果以及要返回哪批匹配結(jié)果,哪個結(jié)果利于對冗長的結(jié)果列表分頁。請查看 API 調(diào)用 SetLimits()。一個分頁示例是用 $cl->SetLimits( ( $page - 1 ) * SPAN, SPAN ) 調(diào)用搜索引擎返回第一批、第二批、第三批(依此類推)SPAN 匹配結(jié)果,這取決于顯示哪個頁面。