2009-05-02

MySQL 全文檢索引擎 - Sphinx

Sphinx 官方網站



介绍


引用自:Sphinx速成指南

Sphinx 是一個基於SQL的全文檢索引擎,可以結合MySQL,PostgreSQL做全文搜索,它可以提供比數據庫本身更專業的搜索功能,使得應用程序更容易實現專業化的全文檢索。 Sphinx特別為一些腳本語言設計搜索API接口,如PHP,Python,Perl,Ruby等,同時為MySQL也設計了一個存儲引擎插件。


Sphinx的特性:
  • 高速索引(在新款CPU上,近10 MB/秒);
  • 高速搜索(2-4G的文本量中平均查詢速度不到0.1秒);
  • 高可用性(單CPU上最大可支持100 GB的文本,100M文檔);
  • 提供良好的相關性排名
  • 支持分佈式搜索;
  • 提供文檔摘要生成;
  • 提供從MySQL內部的插件式存儲引擎上搜索
  • supports boolean, phrase, and word proximity queries;
  • 支持每個文檔多個全文檢索域(默認最大32個);
  • 支持每個文檔多屬性;
  • 支持斷詞;
  • 支持單字節編碼與UTF-8編碼;
  • supports English stemming, Russian stemming, and Soundex for morphology;
  • 支持MySQL(MyISAM和InnoDB表都支持);
  • 支持PostgreSQL.




重點須知

在你決定是否採用此搜尋引擎前,先瞭解他的應用方式:
  • 提供多種查詢方式與權重計算,具有 BM25 關鍵字計算,權重計算方式無法變更,但能作外部索引加權計算。
  • 非網頁爬蟲的模式,是直接對 DataBase 或 XML 作數據索引。
  • 沒有點擊率的計算,需要自行處理。
  • 搜尋結果不包含原始資料,需要自行處理。



安裝 Sphinx 及 Mysql+SphinxSE

# [安裝 Sphinx 及 Mysql+SphinxSE]
# 
# 下載 Sphinx 及 Mysql
wget http://lxr.mysql.com/archives/mysql-5.1/mysql-5.1.31.tar.gz
wget http://www.sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz

# 解壓縮 Sphinx 及 Mysql
tar zxvf mysql-5.1.31.tar.gz
tar zxvf sphinx-0.9.8.1.tar.gz

# 安裝 Sphinx
cd sphinx-0.9.8.1/
./configure
make -j$(grep processor /proc/cpuinfo |wc -l)
make install

# 複製 SphinxSE Engine 至 Mysql
cd ..
cp -R ./sphinx-0.9.8.1/mysqlse ./mysql-5.1.31/storage/sphinx

# 安裝 Mysql
# 必要函式庫 automake autoconf libtool libncurses5-dev bison
cd mysql-5.1.31/
sh BUILD/autorun.sh
./configure --with-plugins=sphinx
make
make install



範例環境

這裡用一個簡單的 Blog 範例來模擬接下來的設定
主要由文章和回應的架構來說明一些主要的觀念
-- 資料庫: `sphinx_test`
-- 建立日期: 2009-3-30
-- 設計版本: 1.0
--
SET NAMES 'UTF8';

DROP DATABASE IF EXISTS `sphinx_test`;
CREATE DATABASE `sphinx_test` DEFAULT CHARACTER SET utf8 
COLLATE utf8_unicode_ci;
USE `sphinx_test`;

-- @ blog_texts(Blog文章)
CREATE TABLE `blog_texts` (
`BlogId` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`Title` CHAR(80) NOT NULL                     COMMENT '標題',
`Text` TEXT NOT NULL                          COMMENT '文章內容',
`InsertDate` DATETIME NOT NULL                COMMENT '建立日期',
PRIMARY KEY(`BlogId`)  
) ENGINE = INNODB COMMENT = '故事文章';
-- -----------------------------------------------------------------

-- @ blog_comments(Blog回應)
CREATE TABLE `blog_comments` (
`Id` INT UNSIGNED NOT NULL AUTO_INCREMENT  COMMENT '主鍵',
`BlogId` INT UNSIGNED NOT NULL             COMMENT '文章 ID',
`Text` TEXT NOT NULL                       COMMENT '回應內容',
`UpdateDate` DATETIME NOT NULL             COMMENT '更新時間',

PRIMARY KEY(`Id`),  
INDEX(`BlogId`),
FOREIGN KEY(`BlogId`)REFERENCES blog_texts(`BlogId`)ON DELETE CASCADE
) ENGINE = INNODB COMMENT = 'Blog 回應';



sphinx.conf 概述

設定檔的主要架構由四個區塊組成:
  • 定義資料來源的形式
  • 定義索引及分詞方式
  • 配置 indexer 所使用的 memory 及 IO存取設定
  • 配置 searchd 的服務端口及查詢數量等項關設定

source 來源名稱1{
# 資料來源設定
}
index 索引名稱1{
source=來源名稱1
# 索引設定
}

source 來源名稱2{
# 資料來源設定
}
index 索引名稱2{
source = 來源名稱2
# 索引設定
}

indexer{
# indexer 配置選項
}
searchd{
# searchd 配置選項
}



sphinx.conf 配置詳解

預設的 conf 位址: /usr/local/etc/sphinx.conf
#
# Sphinx configuration file sample
#
# WARNING! While this sample file mentions all available options,
# it contains (very) short helper descriptions only. Please refer to
# doc/sphinx.html for details.
#

###############################################################
## data source definition
###############################################################

source s_tit
{
# 資料來源的類型。必要設定項,沒有預設值。
# 已知類型 mysql , pgsql , xmlpipe , xmlpipe2。
type           = mysql

# SQL 主機連接的 IP。必要設定項,沒有預設值。
# 僅適用於SQL數據源(mysql,pgsql)。
sql_host       = localhost

# SQL 主機連接的 port 選擇性設定,預設 mysql(3306),及 pgsql(5432)。
# 僅適用於SQL數據源(mysql,pgsql)。
# 這個設定必須依附在 sql_host 下。
sql_port       = 3306  # optional, default is 3306

# 資料庫用戶名稱,必要設定項,沒有預設值。
sql_user       = root

# 資料庫用戶密碼,必要設定項,沒有預設值。
sql_pass       = 0000

# 資料庫名稱,必要設定項,沒有預設值。
sql_db         = sphinx_test

# 資料庫 local 端連結介面,選擇性設定,預設為空。
#sql_sock       = /tmp/mysql.sock 

# MySQL 傳輸壓縮,可將索引建立時的傳輸量,預設為 0(不壓縮)。
#mysql_connect_flags = 32


# 預先查詢。選擇性,可重複,預設是空。僅適用於SQL數據源(mysql,pgsql)。
# 可用來做索引前的初始設定,如編碼設定、緩衝區大小、是否快取暫存
sql_query_pre  = SET NAMES utf8
sql_query_pre  = SET SESSION query_cache_type=OFF


# 主要文件擷取查詢。必要設定項,沒有預設值。僅適用於SQL數據源(mysql,pgsql)。
# 查詢指令允許使用 JOIN 及子查詢,對於資料表不能使用 AS,但資料欄位可以
sql_query      = SELECT `BlogId`,`Title`,`InsertDate` FROM `blog_texts`


# 設定過濾條目,被設定的欄位將不會列入索引的範圍,之後可以作為查詢時的過濾條件,0.9.8版支援下列類型。
sql_attr_timestamp   = InsertDate
#sql_attr_uint = group_id
#sql_attr_bool = is_deleted # will be packed to 1 bit
#sql_attr_str2ordinal = author_name
#sql_attr_float = long_radians 
#sql_attr_multi = uint tag from ranged-query; \
#   SELECT id, tag FROM tags WHERE id>=$start AND id<=$end; \
#   SELECT MIN(id), MAX(id) FROM tags


# 與 sql_query_pre 的功能相同,但用於索引結束後呼叫的指令  
#sql_query_post = DROP TABLE my_tmp_table


#sql_query_info       =SELECT * FROM `blog_texts` WHERE `BlogId`=$id

}

#------------------------------------------------------------------------
source s_txt
{
type           = mysql
sql_host       = localhost
sql_user       = root
sql_pass       = 0000
sql_db         = sphinx_test
sql_port       = 3306
sql_query_pre  = SET NAMES utf8
sql_query_pre  = SET SESSION query_cache_type=OFF
sql_query      = SELECT `BlogId`,`Text`,`InsertDate` FROM `blog_texts`

sql_attr_timestamp   = InsertDate

sql_ranged_throttle  = 0
sql_query_info       =SELECT * FROM `blog_texts` WHERE `BlogId`=$id
}

#------------------------------------------------------------------------
source s_comment
{
type           = mysql
sql_host       = localhost
sql_user       = root
sql_pass       = 0000
sql_db         = sphinx_test
sql_port       = 3306
sql_query_pre  = SET NAMES utf8
sql_query_pre  = SET group_concat_max_len=1048576
sql_query_pre  = SET SESSION query_cache_type=OFF
sql_query     = \
SELECT \
`blog_comments`.`BlogId`, \
GROUP_CONCAT(`blog_comments`.`Text`)AS`CommentText`, \
COUNT(`blog_comments`.`BlogId`)AS`CommentNum`, \
`blog_texts`.`InsertDate` \
FROM \
`blog_texts` \
INNER JOIN \
`blog_comments` \
ON `blog_texts`.`BlogId`=`blog_comments`.`BlogId` \
GROUP BY `blog_comments`.`BlogId` \

sql_attr_uint        = CommentNum
sql_attr_timestamp   = InsertDate

sql_ranged_throttle  = 0
sql_query_info       =SELECT * FROM `blog_texts` WHERE `BlogId`=$id
}





###############################################################
### index definition
###############################################################

index s_tit
{
# 索引類型。預設為空(索引為簡單本地索引)。設定值有空字串或 "distributed"
#type           = distributed

# 指定索引的來源,必須是上面的來源名稱
source          = s_tit


# 索引記錄存放的目錄
path            = /var/data/s_tit


# 文件屬性值存儲模式,預設值為 extern,可設定的屬性有 'none', 'extern' 及 'inline'. 
docinfo         = extern 


# 鎖定記憶體緩衝區段。預設為 0(不鎖定)
mlock           = 0


# 適用的形態前置處理器名單。預設為空(不應用任何前處理器)。
# 可設定的屬性有 'none', 'stem_en', 'stem_ru', 'stem_enru', 'soundex', and 'metaphone'.
# 這段在中文情況下不能使用,否則會導致無法啟動 searchd 服務。
morphology      = none


# 停用詞文件清單(以空白分隔),預設為空。
# 在清單中的 word 將不會加到索引庫裡。
#stopwords      = /usr/local/sphinx/data/stopwords.txt


# 詞形字典,預設為空。可以設定 word 之間的關連性。
# wordforms.txt 
#   walks > walk
#   walked > walk
#   walking > walk
#
#wordforms      = /usr/local/sphinx/data/wordforms.txt

# 保留字例外文件,預設為空。
# exceptions.txt
#   MS Windows => ms windows
#   Microsoft Windows => ms windows
#   C++ => cplusplus
#   c++ => cplusplus
#   C plus plus => cplusplus
#
#exceptions     = /usr/local/sphinx/data/exceptions.txt

# exceptions 與 wordforms 的區別
#  *exceptions 是區分大小寫的, wordforms沒有;
#  *exceptions 允許檢測序列標記, wordforms 只能處理單一的 word;
#  *exceptions 可以使用 charset_table 中沒有的特殊符號,wordforms 完全遵從 charset_table 中的字符;
#  *exceptions 在大字典上性能會下降,wordforms 則對百萬級的條目應對自如;  



# 最短的關鍵字長度
min_word_len    = 1 


# 編碼格式
charset_type    = utf-8


# 指定 UTF-8 的編碼表
charset_table   = U+FF10..U+FF19->0..9,0..9,U+FF41..U+FF5A->a..z,\
U+FF21..U+FF3A->a..z,A..Z->a..z,a..z,U+0149,U+017F,U+0138,U+00DF,\
U+00FF,U+00C0..U+00D6->U+00E0..U+00F6,U+00E0..U+00F6,\
U+00D8..U+00DE->U+00F8..U+00FE,U+00F8..U+00FE,U+0100->U+0101,\
U+0101,U+0102->U+0103,U+0103,U+0104->U+0105,U+0105,U+0106->U+0107,\
U+0107,U+0108->U+0109,U+0109,U+010A->U+010B,U+010B,U+010C->U+010D,\
U+010D,U+010E->U+010F,U+010F,U+0110->U+0111,U+0111,U+0112->U+0113,\
U+0113,U+0114->U+0115,U+0115,U+0116->U+0117,U+0117,U+0118->U+0119,\
U+0119,U+011A->U+011B,U+011B,U+011C->U+011D,U+011D,U+011E->U+011F,\
U+011F,U+0130->U+0131,U+0131,U+0132->U+0133,U+0133,U+0134->U+0135,\
U+0135,U+0136->U+0137,U+0137,U+0139->U+013A,U+013A,U+013B->U+013C,\
U+013C,U+013D->U+013E,U+013E,U+013F->U+0140,U+0140,U+0141->U+0142,\
U+0142,U+0143->U+0144,U+0144,U+0145->U+0146,U+0146,U+0147->U+0148,\
U+0148,U+014A->U+014B,U+014B,U+014C->U+014D,U+014D,U+014E->U+014F,\
U+014F,U+0150->U+0151,U+0151,U+0152->U+0153,U+0153,U+0154->U+0155,\
U+0155,U+0156->U+0157,U+0157,U+0158->U+0159,U+0159,U+015A->U+015B,\
U+015B,U+015C->U+015D,U+015D,U+015E->U+015F,U+015F,U+0160->U+0161,\
U+0161,U+0162->U+0163,U+0163,U+0164->U+0165,U+0165,U+0166->U+0167,\
U+0167,U+0168->U+0169,U+0169,U+016A->U+016B,U+016B,U+016C->U+016D,\
U+016D,U+016E->U+016F,U+016F,U+0170->U+0171,U+0171,U+0172->U+0173,\
U+0173,U+0174->U+0175,U+0175,U+0176->U+0177,U+0177,U+0178->U+00FF,\
U+00FF,U+0179->U+017A,U+017A,U+017B->U+017C,U+017C,U+017D->U+017E,\
U+017E,U+0410..U+042F->U+0430..U+044F,U+0430..U+044F,U+05D0..U+05EA,\
U+0531..U+0556->U+0561..U+0586,U+0561..U+0587,U+0621..U+063A,U+01B9,\
U+01BF,U+0640..U+064A,U+0660..U+0669,U+066E,U+066F,U+0671..U+06D3,\
U+06F0..U+06FF,U+0904..U+0939,U+0958..U+095F,U+0960..U+0963,\
U+0966..U+096F,U+097B..U+097F,U+0985..U+09B9,U+09CE,U+09DC..U+09E3,\
U+09E6..U+09EF,U+0A05..U+0A39,U+0A59..U+0A5E,U+0A66..U+0A6F,\
U+0A85..U+0AB9,U+0AE0..U+0AE3,U+0AE6..U+0AEF,U+0B05..U+0B39,\
U+0B5C..U+0B61,U+0B66..U+0B6F,U+0B71,U+0B85..U+0BB9,U+0BE6..U+0BF2,\
U+0C05..U+0C39,U+0C66..U+0C6F,U+0C85..U+0CB9,U+0CDE..U+0CE3,\
U+0CE6..U+0CEF,U+0D05..U+0D39,U+0D60,U+0D61,U+0D66..U+0D6F,\
U+0D85..U+0DC6,U+1900..U+1938,U+1946..U+194F,U+A800..U+A805,\
U+A807..U+A822,U+0386->U+03B1,U+03AC->U+03B1,U+0388->U+03B5,\
U+03AD->U+03B5,U+0389->U+03B7,U+03AE->U+03B7,U+038A->U+03B9,\
U+0390->U+03B9,U+03AA->U+03B9,U+03AF->U+03B9,U+03CA->U+03B9,\
U+038C->U+03BF,U+03CC->U+03BF,U+038E->U+03C5,U+03AB->U+03C5,\
U+03B0->U+03C5,U+03CB->U+03C5,U+03CD->U+03C5,U+038F->U+03C9,\
U+03CE->U+03C9,U+03C2->U+03C3,U+0391..U+03A1->U+03B1..U+03C1,\ 
U+03A3..U+03A9->U+03C3..U+03C9,U+03B1..U+03C1,U+03C3..U+03C9,\
U+0E01..U+0E2E,U+0E30..U+0E3A,U+0E40..U+0E45,U+0E47,U+0E50..U+0E59,\
U+A000..U+A48F,U+4E00..U+9FBF,U+3400..U+4DBF,U+20000..U+2A6DF,\
U+F900..U+FAFF,U+2F800..U+2FA1F,U+2E80..U+2EFF,U+2F00..U+2FDF,\
U+3100..U+312F,U+31A0..U+31BF,U+3040..U+309F,U+30A0..U+30FF,\
U+31F0..U+31FF,U+AC00..U+D7AF,U+1100..U+11FF,U+3130..U+318F,\
U+A000..U+A48F,U+A490..U+A4CF


# 指明分詞法讀取詞典文件的位置,當啟用分詞法時,為必填項。
# 在使用 LibMMSeg 作為分詞庫時,需要確保詞典文件uni.lib在指定的目錄下。
# 再使用 LibMMSeg 分詞外掛時,這個設定值才有效,不然在建立索引時會出錯。
#charset_dictpath = dict


# 忽略的字符列表,預設為空。
#ignore_chars   = U+AD


# 索引的最小前綴長度,預設為0(不索引前綴)。
#min_prefix_len = 3


# 索引的最小中綴長度,預設為0(不索引中綴)。
#min_infix_len  = 3


# 做前綴索引的字段列表,預設為空(所有字段均為前綴索引模式)。
#prefix_fields  = url, domain


# 做中綴索引的字段列表,預設為空(所有字段均為中綴索引模式)。
#infix_fields   = url, domain


# 允許前綴/中綴索引上的星號語法(或稱萬用字符)預設為 0(不使用通配符),這是為了與0.9.7版本的兼容性。設定值有 0 和 1。
enable_star     = 1


#分詞,設定值有 0,1,如果要搜索中文,請指定為 1
ngram_len       = 1


# 分詞字符,中文搜索必要設定。
ngram_chars     = U+4E00..U+9FBF,U+3400..U+4DBF,U+20000..U+2A6DF,\
U+F900..U+FAFF,U+2F800..U+2FA1F,U+2E80..U+2EFF,U+2F00..U+2FDF,\
U+3100..U+312F,U+31A0..U+31BF,U+3040..U+309F,U+30A0..U+30FF,\
U+31F0..U+31FF,U+AC00..U+D7AF,U+1100..U+11FF,U+3130..U+318F,\
U+A000..U+A48F,U+A490..U+A4CF


# 短語邊界字符列表,預設為空。 
#phrase_boundary = ., ?, !, U+2026


# 是否從輸入全文數據中去除 HTML 標記。預設為 0。設定值有 0(禁用),1(啟用)。 
html_strip      = 0


# 去除 HTML 標籤時要索引標籤語言的屬性列表,預設為空(不索引標記語言屬性)。
# 指定被保留並索引的 HTML 標記語言屬性,即使其他 HTML 標記被刪除。
html_index_attrs      = img=alt,title; a=title;


# HTML 標籤列表,不僅這些標籤本身會被刪除,標籤之間的文字內容也會被刪除。預設為空(不刪除任何元素的內容)。 
html_remove_elements  = style, script
}

index dist_tit
{
# 索引類型。預設為空(索引為簡單本地索引)。設定值有空字串或 "distributed"
type            = distributed


# 分佈式索引(distributed index)中的本地索引聲明,可以設定多個,預設為空。
local           = s_tit


# 分佈式索引(distributed index)中的遠程代理和索引聲明,可以設定多個,預設為空。 
agent           = localhost:3313:remote1
agent           = localhost:3314:remote2,remote3


# 遠程代理的最大連接時間,單位為毫秒,預設為 1000(1 sec)。
agent_connect_timeout  = 1000


# 遠程代理的最大查詢時間,單位為毫秒,預設為 3000(3 sec)。
agent_query_timeout    = 3000


# 預先開啟全部索引文件還是每次查詢時再開啟索引。預設為0(不預先開啟)。
#preopen        = 1
}


#------------------------------------------------------------------------
index s_txt
{
source          = s_txt
path            = /var/data/s_txt
docinfo         = extern
mlock           = 0
morphology      = none
min_word_len    = 1 
charset_type    = utf-8
charset_table   = U+FF10..U+FF19->0..9,0..9,U+FF41..U+FF5A->a..z,\
U+FF21..U+FF3A->a..z,A..Z->a..z,a..z,U+0149,U+017F,U+0138,U+00DF,\
U+00FF,U+00C0..U+00D6->U+00E0..U+00F6,U+00E0..U+00F6,\
U+00D8..U+00DE->U+00F8..U+00FE,U+00F8..U+00FE,U+0100->U+0101,\
U+0101,U+0102->U+0103,U+0103,U+0104->U+0105,U+0105,U+0106->U+0107,\
U+0107,U+0108->U+0109,U+0109,U+010A->U+010B,U+010B,U+010C->U+010D,\
U+010D,U+010E->U+010F,U+010F,U+0110->U+0111,U+0111,U+0112->U+0113,\
U+0113,U+0114->U+0115,U+0115,U+0116->U+0117,U+0117,U+0118->U+0119,\
U+0119,U+011A->U+011B,U+011B,U+011C->U+011D,U+011D,U+011E->U+011F,\
U+011F,U+0130->U+0131,U+0131,U+0132->U+0133,U+0133,U+0134->U+0135,\
U+0135,U+0136->U+0137,U+0137,U+0139->U+013A,U+013A,U+013B->U+013C,\
U+013C,U+013D->U+013E,U+013E,U+013F->U+0140,U+0140,U+0141->U+0142,\
U+0142,U+0143->U+0144,U+0144,U+0145->U+0146,U+0146,U+0147->U+0148,\
U+0148,U+014A->U+014B,U+014B,U+014C->U+014D,U+014D,U+014E->U+014F,\
U+014F,U+0150->U+0151,U+0151,U+0152->U+0153,U+0153,U+0154->U+0155,\
U+0155,U+0156->U+0157,U+0157,U+0158->U+0159,U+0159,U+015A->U+015B,\
U+015B,U+015C->U+015D,U+015D,U+015E->U+015F,U+015F,U+0160->U+0161,\
U+0161,U+0162->U+0163,U+0163,U+0164->U+0165,U+0165,U+0166->U+0167,\
U+0167,U+0168->U+0169,U+0169,U+016A->U+016B,U+016B,U+016C->U+016D,\
U+016D,U+016E->U+016F,U+016F,U+0170->U+0171,U+0171,U+0172->U+0173,\
U+0173,U+0174->U+0175,U+0175,U+0176->U+0177,U+0177,U+0178->U+00FF,\
U+00FF,U+0179->U+017A,U+017A,U+017B->U+017C,U+017C,U+017D->U+017E,\
U+017E,U+0410..U+042F->U+0430..U+044F,U+0430..U+044F,U+05D0..U+05EA,\
U+0531..U+0556->U+0561..U+0586,U+0561..U+0587,U+0621..U+063A,U+01B9,\
U+01BF,U+0640..U+064A,U+0660..U+0669,U+066E,U+066F,U+0671..U+06D3,\
U+06F0..U+06FF,U+0904..U+0939,U+0958..U+095F,U+0960..U+0963,\
U+0966..U+096F,U+097B..U+097F,U+0985..U+09B9,U+09CE,U+09DC..U+09E3,\
U+09E6..U+09EF,U+0A05..U+0A39,U+0A59..U+0A5E,U+0A66..U+0A6F,\
U+0A85..U+0AB9,U+0AE0..U+0AE3,U+0AE6..U+0AEF,U+0B05..U+0B39,\
U+0B5C..U+0B61,U+0B66..U+0B6F,U+0B71,U+0B85..U+0BB9,U+0BE6..U+0BF2,\
U+0C05..U+0C39,U+0C66..U+0C6F,U+0C85..U+0CB9,U+0CDE..U+0CE3,\
U+0CE6..U+0CEF,U+0D05..U+0D39,U+0D60,U+0D61,U+0D66..U+0D6F,\
U+0D85..U+0DC6,U+1900..U+1938,U+1946..U+194F,U+A800..U+A805,\
U+A807..U+A822,U+0386->U+03B1,U+03AC->U+03B1,U+0388->U+03B5,\
U+03AD->U+03B5,U+0389->U+03B7,U+03AE->U+03B7,U+038A->U+03B9,\
U+0390->U+03B9,U+03AA->U+03B9,U+03AF->U+03B9,U+03CA->U+03B9,\
U+038C->U+03BF,U+03CC->U+03BF,U+038E->U+03C5,U+03AB->U+03C5,\
U+03B0->U+03C5,U+03CB->U+03C5,U+03CD->U+03C5,U+038F->U+03C9,\
U+03CE->U+03C9,U+03C2->U+03C3,U+0391..U+03A1->U+03B1..U+03C1,\ 
U+03A3..U+03A9->U+03C3..U+03C9,U+03B1..U+03C1,U+03C3..U+03C9,\
U+0E01..U+0E2E,U+0E30..U+0E3A,U+0E40..U+0E45,U+0E47,U+0E50..U+0E59,\
U+A000..U+A48F,U+4E00..U+9FBF,U+3400..U+4DBF,U+20000..U+2A6DF,\
U+F900..U+FAFF,U+2F800..U+2FA1F,U+2E80..U+2EFF,U+2F00..U+2FDF,\
U+3100..U+312F,U+31A0..U+31BF,U+3040..U+309F,U+30A0..U+30FF,\
U+31F0..U+31FF,U+AC00..U+D7AF,U+1100..U+11FF,U+3130..U+318F,\
U+A000..U+A48F,U+A490..U+A4CF

ngram_len       = 1
ngram_chars     = U+4E00..U+9FBF,U+3400..U+4DBF,U+20000..U+2A6DF,\
U+F900..U+FAFF,U+2F800..U+2FA1F,U+2E80..U+2EFF,U+2F00..U+2FDF,\
U+3100..U+312F,U+31A0..U+31BF,U+3040..U+309F,U+30A0..U+30FF,\
U+31F0..U+31FF,U+AC00..U+D7AF,U+1100..U+11FF,U+3130..U+318F,\
U+A000..U+A48F,U+A490..U+A4CF

html_strip      = 0
html_index_attrs    = img=alt,title; a=title;
html_remove_elements  = style, script
}
index dist_txt
{
type            = distributed
local           = s_txt
agent           = localhost:3313:remote1
agent           = localhost:3314:remote2,remote3
agent_connect_timeout  = 1000
agent_query_timeout    = 3000
}



#------------------------------------------------------------------------
index s_comment
{
source          = s_comment
path            = /var/data/s_comment
docinfo         = extern
mlock           = 0
morphology      = none
min_word_len    = 1
charset_type    = utf-8
charset_table   = U+FF10..U+FF19->0..9,0..9,U+FF41..U+FF5A->a..z,\
U+FF21..U+FF3A->a..z,A..Z->a..z,a..z,U+0149,U+017F,U+0138,U+00DF,\
U+00FF,U+00C0..U+00D6->U+00E0..U+00F6,U+00E0..U+00F6,\
U+00D8..U+00DE->U+00F8..U+00FE,U+00F8..U+00FE,U+0100->U+0101,\
U+0101,U+0102->U+0103,U+0103,U+0104->U+0105,U+0105,U+0106->U+0107,\
U+0107,U+0108->U+0109,U+0109,U+010A->U+010B,U+010B,U+010C->U+010D,\
U+010D,U+010E->U+010F,U+010F,U+0110->U+0111,U+0111,U+0112->U+0113,\
U+0113,U+0114->U+0115,U+0115,U+0116->U+0117,U+0117,U+0118->U+0119,\
U+0119,U+011A->U+011B,U+011B,U+011C->U+011D,U+011D,U+011E->U+011F,\
U+011F,U+0130->U+0131,U+0131,U+0132->U+0133,U+0133,U+0134->U+0135,\
U+0135,U+0136->U+0137,U+0137,U+0139->U+013A,U+013A,U+013B->U+013C,\
U+013C,U+013D->U+013E,U+013E,U+013F->U+0140,U+0140,U+0141->U+0142,\
U+0142,U+0143->U+0144,U+0144,U+0145->U+0146,U+0146,U+0147->U+0148,\
U+0148,U+014A->U+014B,U+014B,U+014C->U+014D,U+014D,U+014E->U+014F,\
U+014F,U+0150->U+0151,U+0151,U+0152->U+0153,U+0153,U+0154->U+0155,\
U+0155,U+0156->U+0157,U+0157,U+0158->U+0159,U+0159,U+015A->U+015B,\
U+015B,U+015C->U+015D,U+015D,U+015E->U+015F,U+015F,U+0160->U+0161,\
U+0161,U+0162->U+0163,U+0163,U+0164->U+0165,U+0165,U+0166->U+0167,\
U+0167,U+0168->U+0169,U+0169,U+016A->U+016B,U+016B,U+016C->U+016D,\
U+016D,U+016E->U+016F,U+016F,U+0170->U+0171,U+0171,U+0172->U+0173,\
U+0173,U+0174->U+0175,U+0175,U+0176->U+0177,U+0177,U+0178->U+00FF,\
U+00FF,U+0179->U+017A,U+017A,U+017B->U+017C,U+017C,U+017D->U+017E,\
U+017E,U+0410..U+042F->U+0430..U+044F,U+0430..U+044F,U+05D0..U+05EA,\
U+0531..U+0556->U+0561..U+0586,U+0561..U+0587,U+0621..U+063A,U+01B9,\
U+01BF,U+0640..U+064A,U+0660..U+0669,U+066E,U+066F,U+0671..U+06D3,\
U+06F0..U+06FF,U+0904..U+0939,U+0958..U+095F,U+0960..U+0963,\
U+0966..U+096F,U+097B..U+097F,U+0985..U+09B9,U+09CE,U+09DC..U+09E3,\
U+09E6..U+09EF,U+0A05..U+0A39,U+0A59..U+0A5E,U+0A66..U+0A6F,\
U+0A85..U+0AB9,U+0AE0..U+0AE3,U+0AE6..U+0AEF,U+0B05..U+0B39,\
U+0B5C..U+0B61,U+0B66..U+0B6F,U+0B71,U+0B85..U+0BB9,U+0BE6..U+0BF2,\
U+0C05..U+0C39,U+0C66..U+0C6F,U+0C85..U+0CB9,U+0CDE..U+0CE3,\
U+0CE6..U+0CEF,U+0D05..U+0D39,U+0D60,U+0D61,U+0D66..U+0D6F,\
U+0D85..U+0DC6,U+1900..U+1938,U+1946..U+194F,U+A800..U+A805,\
U+A807..U+A822,U+0386->U+03B1,U+03AC->U+03B1,U+0388->U+03B5,\
U+03AD->U+03B5,U+0389->U+03B7,U+03AE->U+03B7,U+038A->U+03B9,\
U+0390->U+03B9,U+03AA->U+03B9,U+03AF->U+03B9,U+03CA->U+03B9,\
U+038C->U+03BF,U+03CC->U+03BF,U+038E->U+03C5,U+03AB->U+03C5,\
U+03B0->U+03C5,U+03CB->U+03C5,U+03CD->U+03C5,U+038F->U+03C9,\
U+03CE->U+03C9,U+03C2->U+03C3,U+0391..U+03A1->U+03B1..U+03C1,\ 
U+03A3..U+03A9->U+03C3..U+03C9,U+03B1..U+03C1,U+03C3..U+03C9,\
U+0E01..U+0E2E,U+0E30..U+0E3A,U+0E40..U+0E45,U+0E47,U+0E50..U+0E59,\
U+A000..U+A48F,U+4E00..U+9FBF,U+3400..U+4DBF,U+20000..U+2A6DF,\
U+F900..U+FAFF,U+2F800..U+2FA1F,U+2E80..U+2EFF,U+2F00..U+2FDF,\
U+3100..U+312F,U+31A0..U+31BF,U+3040..U+309F,U+30A0..U+30FF,\
U+31F0..U+31FF,U+AC00..U+D7AF,U+1100..U+11FF,U+3130..U+318F,\
U+A000..U+A48F,U+A490..U+A4CF

ngram_len       = 1
ngram_chars     = U+4E00..U+9FBF,U+3400..U+4DBF,U+20000..U+2A6DF,\
U+F900..U+FAFF,U+2F800..U+2FA1F,U+2E80..U+2EFF,U+2F00..U+2FDF,\
U+3100..U+312F,U+31A0..U+31BF,U+3040..U+309F,U+30A0..U+30FF,\
U+31F0..U+31FF,U+AC00..U+D7AF,U+1100..U+11FF,U+3130..U+318F,\
U+A000..U+A48F,U+A490..U+A4CF

html_strip      = 0
html_index_attrs    = img=alt,title; a=title;
html_remove_elements  = style, script
}
index dist_comment
{
type            = distributed
local           = s_comment
agent           = localhost:3313:remote1
agent           = localhost:3314:remote2,remote3
agent_connect_timeout  = 1000
agent_query_timeout    = 3000
}



###############################################################
### indexer settings
###############################################################

indexer
{
# 索引過程中記憶體的使用限制,預設為 32M。
mem_limit       = 64M

# 每秒最大 I/O 操作次數,用於限制 I/O 操作。預設為0(無限制)。
#max_iops       = 40

# 最大單次允許的 I/O 操作大小,以 bytes 為單位,用於I/O節流。預設為0(不限制)。
#max_iosize     = 1048576
}



###############################################################
### searchd settings
###############################################################

searchd
{
# 監聽來源 IP,預設為0.0.0.0(即允許所有 IP 連結)。
#address        = 127.0.0.1


# searchd 的 TCP port。預設為 3312。
port            = 3312


# log 的紀錄文件位址,全部 searchd 運行時事件會被記錄在這個日誌文件中。 
log             = /var/log/sphinx/searchd.log


# 查詢日誌文件名,預設為空(不記錄查詢日誌)。
# 全部搜索查詢會被記錄在此文件中。
query_log       = /var/log/sphinx/query.log


# 最大的查詢請求時間,單位是秒。預設是5秒。
# searchd 將強制關閉在此時間內未能成功發出查詢的客戶端連接。 
read_timeout    = 5


# 並行執行的搜索的數目。預設為0(無限制)。
max_children    = 30


# searchd 進程 ID 文件名。必選項。
pid_file        = /var/log/sphinx/searchd.pid


# 守護進程在記憶體中為每個索引所保持並返回給客戶端的匹配數目的最大值。預設為1000
max_matches     = 1000


# 防止 searchd 輪換在需要預取大量數據的索引時停止響應。預設為1(啟用無縫(seamless)輪換)
seamless_rotate = 1


# 是否在啟動時強制重新打開所有索引文件。預設為0(不重新打開)。
preopen_indexes = 0


# 索引輪換成功之後,是否刪除以.old為擴展名的索引拷貝。預設為1(刪除這些索引拷貝)。
unlink_old      = 1
}

# --eof--



啟動搜尋引擎

開始建立索引及啟動搜尋引擎
# 為所有 sphinx.conf 中設定的資料來源建立索引
/usr/local/bin/indexer --config /usr/local/etc/sphinx.conf --all

# 以 sphinx.conf 中的設定啟動搜尋引擎
/usr/local/bin/searchd --config /usr/local/etc/sphinx.conf


動態更新索引的方法
# 更新所有資料來源
/usr/local/bin/indexer --rotate --config /usr/local/etc/sphinx.conf --all 

# 更新特定的資料來源(s_tit),並且不顯示任何訊息
/usr/local/bin/indexer --quiet --rotate --config /usr/local/etc/sphinx.conf s_tit



主要程式及運作方式

Sphinx 主要有以下部分:
  • indexer: 建立索引庫的程序,在查詢前必須先建立索引庫。
  • search: 提供 console 下的搜尋介面,可用於測試用。
  • searchd: 主要的 service 程序,以 port 為連接介面去對索引庫取得資料。
  • SphinxAPI: 連結 searchd 的客戶端 API,目前支援的 script 語言有(PHP, Python, Perl, Ruby)。
  • SphinxSE: 透過 MySQL 連結 searchd 的資料表引擎。

Sphinx 架構圖


命令列參數說明

indexer
格式:indexer [OPTIONS] [indexname1 [indexname2 [...]]]
  • --config <file> (-c <file>精簡指令) 指定 sphinx.conf 的位址,預設為 /usr/local/etc/sphinx.conf
  • --all 為所有資料來源建立索引,在利用 cron 定期更新索引庫時,可使用此參數更新所有資料來源的索引
  • --rotate 動態更新來源索引,可用在 searchd 仍處於啟動的狀態。
  • --quiet 告訴 indexer 不輸出任何訊息,除非有一個錯誤。在 cron 上使用時非常方便。
  • --noprogress 不顯示進展的細節訊息。而在最後輸出的細節訊息(如文件索引,索引的速度等等)
  • --merge <dst-index> <src-index> 合併 <src-index> 到 <dst-index>,<dst-index>會保存有合併後的結果,<src-index> 不會被修改。
  • --merge-dst-range <attr> <min> <max>合併時過濾 <dst-index> 僅保留 <attr> 值在 <min> 和 <max> (包含)的記錄.


searchd
格式:searchd [OPTIONS]
  • --help (-h 精簡指令) 列出所有的參數說明。
  • --config <file> (-c <file> 精簡指令) 指定 sphinx.conf 的位址,預設為 /usr/local/etc/sphinx.conf
  • --stop 停止 searchd 引擎的運作
  • --console 強制使用 console 介面(windows)
  • --port portnumber (-p 精簡指令) 強制變更 searchd 連結端口(port)的位址
  • --index <index> (-i 精簡指令) 指定唯一的搜尋索引,主要用於測試用。


search
格式:search [OPTIONS] word1 [word2 [word3 [...]]]

一般選項:
  • --config <file>(-c <file> 精簡指令) 讓 search 使用特定的 conf 檔作為配置,就像前面 indexer。
  • --index <index>(-i <index> 精簡指令) 讓 search 限制搜尋的索引檔,預設會嘗試搜尋所有 conf 檔中列出的索引。
  • --stdin tells search 讓 search 接受查詢從標準輸入,而不是命令行。這可用在測試目的上,即可以透過管線輸入或 script。


設置匹配選項:
  • --any (-a 精簡指令) 匹配所有的搜尋關鍵字。
  • --phrase (-p 精簡指令) 短語匹配。
  • --boolean (-b 精簡指令) 布林表達式匹配。
  • --ext (-e 精簡指令) 查詢匹配一個Sphinx內部查詢語言表達式。
  • --ext2 (-e2 精簡指令) 查詢匹配一個Sphinx內部查詢語言表達式。
  • --filter <attr> <v> (-f <attr> <v> 精簡指令) 過慮條件,可用在過慮 conf 中以 attr 標註的欄位,(--filter InsertDate 2009)過慮出建立日期為 2009 的資料


處理結果輸出選項:
  • --limit <count>(-l count 精簡指令) 輸出 row 的數量(預設為20)。
  • --offset <count>(-o <count>精簡指令) 輸出 row 的起始(預設為0)。
  • --group <attr>(-g <attr>精簡指令) specifies that results should be grouped together based on the attribute specified. Like the GROUP BY clause in SQL, it will combine all results where the attribute given matches, and returns a set of results where each returned result is the best from each group. Unless otherwise specified, this will be the best match on relevance.
  • --groupsort <expr>(-gs <expr>精簡指令) instructs that when results are grouped with -group, the expression given in <expr>shall determine the order of the groups. Note, this does not specify which is the best item within the group, only the order in which the groups themselves shall be returned.
  • --sortby <clause>(-s <clause>精簡指令) specifies that results should be sorted in the order listed in <clause>. This allows you to specify the order you wish results to be presented in, ordering by different columns. For example, you could say --sortby "@weight DESC entrytime DESC" to sort entries first by weight (or relevance) and where two or more entries have the same weight, to then sort by the time with the highest time (newest) first. You will usually need to put the items in quotes (--sortby "@weight DESC") or use commas (--sortby @weight,DESC) to avoid the items being treated separately. Additionally, like the regular sorting modes, if --group (grouping) is being used, this will state how to establish the best match within each group.
  • --sortexpr expr (-S expr 精簡指令) specifies that the search results should be presented in an order determined by an arithmetic expression, stated in expr. For example: --sortexpr "@weight + ( user_karma + ln(pageviews) )*0.1" (again noting that this will have to be quoted to avoid the shell dealing with the asterisk). Extended sort mode is discussed in more detail under the SPH_SORT_EXTENDED entry under the Sorting modes chapter of the manual.
  • --sort=date 指定以降幕排序的欄位,必須在 conf 中以 attr 標註的欄位。
  • --rsort=date 指定以升幕排序的欄位,必須在 conf 中以 attr 標註的欄位。



摘要使用

Sphinx 的 API 提供摘要產生的功能
在下載的安裝包中可以找到這份範例
<?php

//
// $Id: test2.php 910 2007-11-16 11:43:46Z shodan $
//

require ( "sphinxapi.php" );

$docs = array
(
"this is my test text to be highlighted, and for the sake ".
"of the testing we need to pump its length somewhat",
"another test text to be highlighted, below limit",
"test number three, without phrase match",
"final test, not only without phrase match, but also above ".
"limit and with swapped phrase text test as well",
);
$words = "test text";
$index = "test1";
$opts = array
(
"before_match"    => "<b>",
"after_match"    => "</b>",
"chunk_separator"  => " ... ",
"limit"        => 60,
"around"      => 3,
);

foreach ( array(0,1) as $exact )
{
$opts["exact_phrase"] = $exact;
print "exact_phrase=$exact\n";

$cl = new SphinxClient ();
$res = $cl->BuildExcerpts ( $docs, $index, $words, $opts );
if ( !$res )
{
die ( "ERROR: " . $cl->GetLastError() . ".\n" );
} else
{
$n = 0;
foreach ( $res as $entry )
{
$n++;
print "n=$n, res=$entry\n";
}
print "\n";
}
}

//
// $Id: test2.php 910 2007-11-16 11:43:46Z shodan $
//

?>



搜尋的匹配模式

匹配模式SPH_MATCH_ALL
匹配所有
SPH_MATCH_ANY
匹配任意
SPH_MATCH_PHRASE
短语匹配
SPH_MATCH_EXTENDED
内部查询
觀察必須包含前後順序,所有 word 都要包含。word 符合前後順序權重高,中文會拆字查詢。必須包前後順序,而且是有序比對(排除標點符號)。無特定前後順序,但有符合前後順序的權重比較高。
權重特性Max match_word lemgth(match_word1 lemgth)^2+
(match_word2 lemgth)^2
match_word lemgthBM25
最高權重值{順序完全批配的word數}{所有 word 數}^2{word數}
沒有較小的權重,此為唯一權重值。
?
word 對結果的影響用所有的 word 進行查詢,只要有一個 word 不符合就不撈出來。符合其中一個word。完全符合前後順序與所有word。符合全數word。
查詢時 word 的先後可以不同。正確的順序權重較高。必要。順序不同不會出現在結果裡。前後順序影響權重。
適合用途適合不會打錯的查詢,word 很少的查詢。多 word 的查詢。完全精準查詢。適合不會打錯的查詢,word 很少的查詢。
特性word 越多,查詢越少。有一個 word 沒有就找不到了。word 越多,資料越多。幾乎等於 SQL like。word 越多,資料越少,權重值會拉開,且權重值是根據所有可能結果之間的差異做權重。



建立 SphinxSE 表

SphinxSE 的使用方式是利用一個虛擬的資料表去與 searchd 作連結
這個表本身不會儲存任何資料,也不能新增資料
利用這個資料表可以作任何 SQL SELECT 的操作(JOIN ...)
-- @ sphinx_interface(Sphinx搜尋連接介面)
CREATE TABLE `sphinx_interface` (
-- 前三個為必要欄位,
-- 欄位屬性順序必須為 INTEGER,INTEGER,VARCHAR
-- 分別標記為(id),匹配權重(weight),查詢(query)
-- 不限定欄位名稱
-- 同時 id 及 query 必須建立索引。
`id` INT NOT NULL                COMMENT '搜尋結果的 Id',
`weight` INT NOT NULL            COMMENT '搜尋結果的權重',
`query` VARCHAR(3072) NOT NULL   COMMENT '搜尋的查詢條件',

-- 額外欄位,需與 sphinx.conf 中 sql_attr 設定的欄位一致,
-- 欄位屬性必須為 INTEGER,VARCHAR 或 TIMESTAMP,
-- 此處的設定可作後續的排序或過濾用。
`insertdate` VARCHAR(3072) NOT NULL  COMMENT '日期', 
`commentnum` INTEGER                 COMMENT '回應總數',
INDEX(id),
INDEX(query)
)ENGINE=SPHINX 
-- CONNECTION 的格式為 sphinx://HOST:PORT/INDEXNAME
-- 建議先不加 INDEXNAME,等在查詢時在決定 index
CONNECTION="sphinx://localhost:3312/" 
COMMENT='Sphinx搜尋連接介面';



SphinxSE 的使用方式

  • query 查詢文本

  • mode 匹配模式.必須是 "all", "any", "phrase", "boolean" 或 "extended",預設為“all”

  • sort 匹配項排序模式必須是“relevance”, “attr_desc”, “attr_asc”, “time_segments”或“extended”之一。除了“relevance”模式,其他模式中還必須在一個冒號後附上屬性名(或“extended”模式中的排序子句)。
    ... WHERE query='test;sort=attr_asc:group_id';
    ... WHERE query='test;sort=extended:@weight desc, group_id asc';

  • offset 結果集中的偏移量,預設是0。

  • limit 從結果集中獲取的匹配項數目,預設為20。

  • index 待搜索的索引:
    ... WHERE query='test;index=test1;';
    ... WHERE query='test;index=test1,test2,test3;';

  • minid , maxid 匹配文檔ID的最小值和最大值

  • weights 逗號分隔的列表,指定Sphinx全文數據字段的權值
    ... WHERE query='test;weights=1,2,3;';

  • filter , !filter 逗號分隔的列表,指定一個屬性名和一系列可匹配的屬性值:
    -- 僅包括群組 1, 5 和 19
    ... WHERE query='test;filter=group_id,1,5,19;';
    
    -- 排除的群組 3 和 11
    ... WHERE query='test;!filter=group_id,3,11;';

  • range , !range 逗號分隔的列表,指定一個屬性名和該屬性可匹配的最小值和最大值:
    -- 僅包括群組 3 至 7 之間 的 group_id
    ... WHERE query='test;range=group_id,3,7;';
    
    -- 排除的群組 5 至 25 之間的 group_id
    ... WHERE query='test;!range=group_id,5,25;';

  • maxmatches 此查詢最大匹配的數量:
    ... WHERE query='test;maxmatches=2000;';

  • groupby 分組(group-by)函數和屬性:
    ... WHERE query='test;groupby=day:published_ts;';
    ... WHERE query='test;groupby=attr:group_id;';

  • groupsort 分組(group-by)排序子句
    ... WHERE query='test;groupsort=@count desc;';

  • indexweights 逗號分隔的列表,指定一系列索引名和搜索時這些索引對應的權值
    ... WHERE query='test;indexweights=idx_exact,2,idx_stemmed,1;';

參考來源

Sphinx 0.9.8.1 reference manual
Coreseek 全文檢索服務器2.0 (Sphinx 0.9.8) 參考手冊
Sphinx速成指南

Sphinx 自由開放原始碼全文搜尋引擎
ubuntu下Mysql+sphinx+中文分词安装配置
Mysql+sphinx+中文分词简介(ubuntu)
用 PHP 构建自定义搜索引擎

7 則留言:

  1. 作者真的很細心~好專業的介紹~謝謝

    回覆刪除
  2. 檢索結果中有很多 ? 問號,某些字符顯示不了,請問怎樣解決?

    [words] => Array
    (
    [奢侈品] => Array
    (
    [docs] => 14
    [hits] => 18
    )

    [主????動力] => Array
    (
    [docs] => 10
    [hits] => 10
    )

    [發展????勢] => Array
    (
    [docs] => 3
    [hits] => 4
    )

    )

    回覆刪除
  3. 看起來像是編碼的問題,確定一下資料庫跟程式都是 UTF-8 的編碼嗎?或者是將 Sphinx 的 charset_type 設定成你所用的格式。

    回覆刪除
  4. 你好,感謝你這麼熱心詳細的解說sphinx的使用
    小弟現在在使用上有個小問題,因為小弟是架設一個論壇phpbb3(不知道跟他有沒有關係)
    小弟設定搜尋引擎是使用sphinx,也可以搜尋到內容了!!
    但現在最大的問題是,分詞問題
    例如文章中如果有"操作系統",當我搜尋"操統"時,因為他自動拆字為"操""統"
    導致該文章會被搜尋到,但這不是我要的關鍵字(不被拆字),我應該如何排除呢???

    回覆刪除
  5. 有網友針對中文重新包裝過的 sphinx,可能不是最新版本,但可以自訂詞庫。

    懶惰一點就是用 ext 或 ext2 的匹配模式,再加權重排序,這樣可以讓最相關的內容放在最前面。

    另外附加一點 sphinx 這個搜尋引擎,並不會完整搜尋內容,所以會有機率出現找不到實際有的狀況。

    回覆刪除
  6. 請問一下,match 宣告要在哪裡做??是在config檔嗎??

    回覆刪除
  7. 這你要研究一下 phpbb3

    回覆刪除

你好!歡迎你在我的 Blog 上留下你寶貴的意見。