由於網站搜索的須要,啓動了一個搜索引擎項目,其實也算不上完整的搜索引擎,需求很簡單,以下:php
1)搜索產品名、類別名、品牌名、副標題、關鍵字等字段
2)數據量目前爲13000左右,將來可能在5萬左右,超出10萬的可能性不大
3)搜索必須精確
4)搜索結果須要按照必定的規則排序
5)搜索結果能夠按條件過濾
可選的產品主要有3種,sphinx、solr、ElasticSearch,其中sphinx是基於C++的,體積小,運行速度快,分佈式查詢較困難,查詢接口支持多種語言。solr和ElasticSearch基於Lucene,開發語言是Java,提供http訪問支持。簡單分析一下區別:
1)sphinx創建索引較快,通常單機使用
2)solr和es都是基於lucene的,有一些細微的區別,其中solr更正宗,由於solr和lucene如今已經合併了,可是es的查詢效果更快、更穩定,尤爲是solr的集羣須要zookeeper,而es不須要,它自己設計就考慮了分佈式。因此solr和es更適用於大項目,數據量較大的狀況。好比較流行的elk日誌分析系統,基於ElasticSearch、Logstash、Kiabana,理論上能夠支持上百萬臺機器的日誌分析、PB級別的數據分析。
相對來講,咱們的項目比較小,只須要用sphinx就能夠了,另外考慮到項目時間較緊,就沒有過多的測試es和solr。
在這裏,要區分兩個概念:查詢分詞和索引分詞
1)查詢分詞指的是將輸入查詢的詞進行分詞,如輸入「我是中國人」會分紅「我」「是」「中國人」「中國」 「人」
2)索引分詞指的是在原始數據進行索引時對原始數據進行分詞,即如原始數據是「我是中國人」,會和上面同樣的進行分詞,並存儲爲索引。
3)sphinx的原始版本只能對中文進行單字切割,因此須要在查詢時進行分詞,而後全詞匹配,不然會出現不少莫名其妙的結果,好在這種狀況下速度也挺快的。若是數據量很是大,單字切割速度就會變慢,更好的辦法是索引分詞,有一個corseek支持中文分詞,只惋惜很久不更新了
由於原始的sphinx版本只支持單字分詞,因此須要使用查詢分詞,選擇的是scws分詞,配置起來較爲容易。步驟以下:
一、環境
1)CentOS : 6.5和7.0都可
2)編譯環境: yum install gcc gcc-c++
二、sphinx
2)編譯(在測試服務器172.16.8.97上的步驟)
$ cd sphinx-2.2.10
$ ./configure
$ make
$ make install
這種安裝的配置文件在/usr/local/etc下,默認有三個文件example.sql,是用來建立test索引的數據庫,sphinx.conf.dist是比較詳細的配置文件,sphinx-min.conf.dist是最小的配置文件,能夠將sphinx-min.conf.dist更名或複製爲sphinx.conf,這是默認的配置文件。
sphinx有兩個命令比較重要,indexer和searchd,前者是用來建索引的,後者是查詢的守護進程,提供查詢服務。
3)配置sphinx.conf
sphinx.conf也很容易懂,大體分爲source, index, indexer, searchd這幾部分,source表明數據來源,支持mysql、pgsql、mssql、odbc、xmlpipe、xmlpipe2,咱們用的是mysql,基本配置以下
#source配置
source goods
{
type = mysql
sql_host = localhost
sql_user = root
sql_pass =
sql_db = xigang
sql_port = 3306 # optional, default is 3306
sql_query_pre = SET NAMES utf8
sql_query = \
SELECT a.goods_id,goods_sn,a.goods_name,a.goods_brief,b.cat_name,c.brand_name,a.keywords goods_keywords,a.specification, a.goods_spec,e.region_name goods_country,c.alias brand_alias,a.brand_country,c.country brand_country1, b.alias category_alias, a.last_update,b.cat_id, c.brand_id,b.keywords category_keywords,to_pinyin(a.goods_name) goods_name_pinyin,to_fpinyin(a.goods_name) goods_name_fpinyin,market_price,shop_price,origin_price,promote_price, IF(promote_price>0,1,0) promote_flag, goods_number, IF(goods_number>0,1,0) goods_num_flag,sales_volume,if(sales_volume>0,1,0) volume_flag,is_new,a.is_delete goods_delete,a.sort_order,a.is_delete goods_delete,b.is_delete cat_delete,is_on_sale FROM ecs_goods a LEFT JOIN ecs_category b ON a.cat_id=b.cat_id LEFT JOIN ecs_brand c ON a.brand_id=c.brand_id LEFT JOIN ecs_region e ON a.goods_country=e.region_id
sql_attr_uint = cat_id
sql_attr_uint = brand_id
sql_attr_float = market_price
sql_attr_float = origin_price
sql_attr_float = promote_price
sql_attr_uint = promote_flag
sql_attr_uint = sales_volume
sql_attr_uint = goods_number
sql_attr_uint = goods_num_flag
sql_attr_uint = is_new
sql_attr_uint = goods_delete
sql_attr_float = sort_order
sql_attr_uint = volume_flag
sql_attr_uint = is_on_sale
sql_attr_uint = cat_delete
sql_attr_timestamp = last_update
sql_field_string = goods_name
sql_field_string = goods_sn
sql_field_string = shop_price
sql_field_string = goods_brief
sql_field_string = cat_name
sql_field_string = brand_name
sql_ranged_throttle = 0
}
配置簡很是易懂,須要注意如下內容
1)sql_query_pre = SET NAMES utf8 是必須的,不然索引創建了,卻搜索不出來,這在拷貝sphinx-min.conf.dist做爲默認配置文件要特別注意,由於該文件中沒有這一條,在sphinx.conf.dist中存在,若是有註釋,去掉就能夠了。
2)sql_attr_*,這些字段都包含在搜索結果中,能夠用來過濾、排序、分組等,須要注意的是sql_attr_string字段,這個字段也能夠達到過濾、排序和分組的效果,可是這個字段不會爲全文索引,因此須要用sql_field_string字段代替,它兼有過濾、分組、排序,還有索引的功能。sql_fied_string能夠用在列表顯示的時候,這樣能夠減小對mysql的查詢,直接顯示全部數據
3)其餘問題看手冊爲準
#index配置
index goods
{
source = goods
path = /var/data/sphinx/goods
docinfo = extern
dict = keywords
mlock = 0
min_stemming_len = 1
min_word_len = 1
min_infix_len = 2
ngram_len = 1
ngram_chars = U+4E00..U+9FBB, U+3400..U+4DB5, U+20000..U+2A6D6, U+FA0E, U+FA0F, U+FA11, U+FA13, U+FA14, U+FA1F, U+FA21, U+FA23, U+FA24, U+FA27, U+FA28, U+FA29, U+3105..U+312C, U+31A0..U+31B7, U+3041, U+3043, U+3045, U+3047, U+3049, U+304B, U+304D, U+304F, U+3051, U+3053, U+3055, U+3057, U+3059, U+305B, U+305D, U+305F, U+3061, U+3063, U+3066, U+3068, U+306A..U+306F, U+3072, U+3075, U+3078, U+307B, U+307E..U+3083, U+3085, U+3087, U+3089..U+308E, U+3090..U+3093, U+30A1, U+30A3, U+30A5, U+30A7, U+30A9, U+30AD, U+30AF, U+30B3, U+30B5, U+30BB, U+30BD, U+30BF, U+30C1, U+30C3, U+30C4, U+30C6, U+30CA, U+30CB, U+30CD, U+30CE, U+30DE, U+30DF, U+30E1, U+30E2, U+30E3, U+30E5, U+30E7, U+30EE, U+30F0..U+30F3, U+30F5, U+30F6, U+31F0, U+31F1, U+31F2, U+31F3, U+31F4, U+31F5, U+31F6, U+31F7, U+31F8, U+31F9, U+31FA, U+31FB, U+31FC, U+31FD, U+31FE, U+31FF, U+AC00..U+D7A3, U+1100..U+1159, U+1161..U+11A2, U+11A8..U+11F9, U+A000..U+A48C, U+A492..U+A4C6
html_strip = 0
}
內容也很易懂,須要注意如下內容
1)source是上面創建的source名字,不能寫錯了
2)path是索引文件的位置,須要注意一下這個目錄是否存在,是否有權限
3)min_infix_len,主要用於單詞內部搜索,由於默認狀況下,單詞按照空格或符號分割,若是隻搜索一部分,就搜索不出來,增長這個屬性,就能夠了,可是會增長索引的大小,由於產生了不少小詞。
4)ngram_*是用於cjk字符的,即中文、日文和朝鮮文的分詞,其中ngram_len說明分詞寬度爲1,ngram_chars指的那些詞會被看成cjk,這是文檔上的標準寫法,拷貝就能夠了。
5)其餘問題,看文檔。
indexer和searchd沒什麼可改的,對於咱們的系統也夠用了。
三、索引生成和查詢
1)indexer --all --rotate
--all表明從新生成索引, --rotate用於searchd服務已經啓動的狀況下
2)searchd
啓動搜索服務
四、使用sphinx
1)sphinx提供了兩套機制來訪問sphinx索引服務,一個是sphinxapi,一個是sphinxql,前者比較通用,資料較多,但性能差一些,使用的端口是9312,sphinxql其實是一種相似sql的查詢語言,協議用的是mysql客戶端,速度要快一些。二者在功能上是等價的,區別在於sphinxql支持實時索引,使用的接口是9306。我比較喜歡sphinxql,由於能夠直接打印出來,在mysql客戶端工具裏執行,看到效果。
2)匹配模式
SPH_MATCH_ALL 匹配全部查詢詞(sphinxapi默認值,可是效果很差,好比‘日本’,會搜索出‘今日買本子去了’)
SPH_MATCH_ANY 匹配任何詞
SPH_MATCH_PHRASE 查詢詞整個匹配,返回最佳結果(適合精確搜索)
SPH_MATCH_BOOLEAN 將查詢詞看成布爾值搜索(不知道怎麼用)
SPH_MATCH_EXTENED 查詢詞能夠做爲內部查詢語言,便可以使用異或、正則之類的功能(默認值)。
SPH_MATCH_EXTENED2 和SPH_MATCH_EXTENED相似
4)SphinxQL
$mysql -h0 -P 9306 #鏈接
myql> select * from goods where match('奶粉'); #全部匹配的字段匹配"奶粉"這兩個字
mysql>select * from goods where match('"奶粉"); #全部匹配的字段匹配「奶粉」這個詞
mysql>select * from goods where match('@goods_name 奶粉'); #商品名中匹配「奶粉」這兩個字
mysql>select * from goods where match('@goods_name "奶粉"'); #商品名中匹配「奶粉」這個詞
mysql>select * from goods where match('@(goods_name,goods_brief) "喜寶" "配方奶粉"');
#這裏是goods_name,goods_biref這兩個字段包括"喜寶" "配方奶粉"這兩個詞,若是沒有雙引號,就是全部的字
mysql>select id,weight() from goods where match('@(goods_name,goods_brief) "喜寶" "配方奶粉"') order by weight() desc option ranker=proximity,field_weights=(goods_name=100,goods_brief=10);
#這裏增長權重,goods_name=100,goods_brief=10,若是二者都匹配,weight()應該是相似110,不過因爲算法問題,可能會是220,440之類的。能夠經過權重知道匹配狀況,及時處理一些不合適的搜索問題
mysql> select max(id),count(*),archival_sn, weight() wt from goods where is_on_sale=1 and goods_delete=0 and pr_area='01' group by archival_sn having count(*)>1 order by goods_num_flag desc ,promote_flag desc ,sort_order desc ,weight() desc ,id desc limit 0,20 option ranker=proximity ,field_weights=(goods_name=10000000,goods_sn=1000000,goods_keywords=100000,cat_name=10000,brand_name=1000,goods_brief=100,specification=10,goods_spec=1);
#這個比較複雜,含義是按照必定的條件篩選數據,同一備案號的商品選擇id最大的,只顯示一個,而後去數據庫中抓取相應的數據,固然也能夠在sphinx中抓取數據,簡單字段都保存在sphinx中了。
#另外sql_attr_*和sql_field_string至關於索引goods的數據列,select cat_id,cat_name from goods
五、PHP訪問sphinx
PHP訪問sphinx有兩種方法,SphinxAPI和SphinxQL,前者是調用sphinx提供的接口函數,這些函數保存在sphinx源代碼包的api目錄下,包括php、python、java、ruby的接口。php調用SphinxAPI文檔不少,就很少說了。使用SphinxQL也很是簡單,用mysql的操做函數就能夠了,區別在於端口是9306,用戶名密碼都是空,如
$pdo = new PDO("mysql:host=localhost;port=9306;charset=utf-8','','');
$query1="select * from goods where match('奶粉'); ";
$sth1 = $pdo->prepare($query1);
$sth1->execute();
$result1 = $sth1->fetchAll();
六、使用scws分詞
1)配置
$ cd scws-1.2.2
$ ./configure --prefix=/usr/local/scws
$ make
$ make install
默認程序是安裝到/usr/local/scws下,能夠去這個目錄看看是否安裝成功。
2)分詞詞典
$ cd /usr/local/scws/etc
$ tar xvjf scws-dict-chs-gbk.tar.bz2
$ tar xvjf scws-dict-chs-utf8.tar.bz2
在/usr/local/scws/etc下會產生兩個文件dict.xdb和dict.utf8.xdb,前者是gbk編碼的,後者是utf8編碼的字典
3)PHP擴展
scws是一個C語言程序,能夠用C語言直接調用,不過它提供了php接口,安裝也很簡單,以下
$ cd phpext #scws源程序根目錄
$ phpize #須要安裝php開發包,yum install php-devel
$ ./configure
$ make
$ make install
$ vim /etc/php.ini #也有可能在php-fpm目錄下,看你的服務器狀況
增長以下內容
[scws]
extension = /usr/lib64/php/modules/scws.so
scws.default.charset = utf8
scws.default.fpath = /usr/local/scws/etc
編寫簡單的demo,以下
//scws0.php
$so->set_charset('utf8');
$so->set_dict('/usr/local/scws/etc/dict.utf8.xdb');
$so->set_rule('/usr/local/scws/etc/rules.utf8.ini');
//$so->add_dict('/usr/local/scws/etc/new.txt',SCWS_XDICT_TXT);
$so->set_ignore(true); //忽略標點符號
while ($tmp = $so->get_result()) // get_result()要重複調用,直到全部的分詞結果都返回爲止
for($i=0;$i<count($tmp);$i++){
$scws=$scws.' "'.$tmp[$i]["word"].'" ';
"我" "是" "一個" "中國人" "我愛" "我" "的" "祖國"
這裏要說明一下,這個分詞程序用起來是大同小異,能夠在主頁看詳細文檔,這裏作了一個處理,將每一個詞增長了雙引號,這樣是爲了在sphinxql中調用方便。
$so->add_dict能夠增長其餘詞典,詞典是xdb格式,也能夠用文本,只不過文本要慢一些,初期能夠用文本,等正式上線再生成xdb文件。下面以文本爲例,內容以下
結構很簡單,第一列是分詞,第二列是tf,第三列是idf,第四列是詞性,用起來很容易,若是沒有這個文件,將上面的程序中的$text值修改成「愛他美」,會輸出「愛」「他」「美」,若是增長了這個分詞文件,並將scws0.php中的註釋去掉,執行結果就變成了「愛他美」。
到此,分詞問題就解決了,若是多個服務器使用也很簡單,能夠將這個程序對外發布。
七、作一個簡單的demohtml
# Pf.php
public static function splitSearch($search){
$so->set_charset('utf8');
$so->set_dict('/usr/local/scws/etc/dict.utf8.xdb');
$so->set_rule('/usr/local/scws/etc/rules.utf8.ini');
$so->add_dict('/usr/local/scws/etc/new.txt',SCWS_XDICT_TXT);
//scws_add_dict($so,'/usr/local/scws/etc/new.txt',SCWS_XDICT_TXT);
while ($tmp = $so->get_result())
for($i=0;$i<count($tmp);$i++){
$scws=$scws.' "'.$tmp[$i]["word"].'" ';
#test.php
<?php
require('./Pf.php');
$query = "愛他美";
$search =Pf::splitSearch($query);
$host = "127.0.0.1";
$port = 9306;
$pdo = new PDO("mysql:host=".$host.';port='.$port.';charset=utf-8','','');
$sql = "select id,goods_name from goods where match('".$search."') limit 1,2";
$sth=$pdo->prepare($sql);
$sth->execute();
$result = $sth->fetchAll();
echo "<pre>";
print_r($result);
echo "</pre>";
輸出結果爲
Array
(
[0] => Array
(
[id] => 2927
[0] => 2927
[goods_name] => 德國原裝 Aptamil愛他美 嬰兒配方奶粉2段800g
[1] => 德國原裝 Aptamil愛他美 嬰兒配方奶粉2段800g
)
[1] => Array
(
[id] => 3223
[0] => 3223
[goods_name] => 德國原裝 Aptamil愛他美 嬰兒配方奶粉2+段600g
[1] => 德國原裝 Aptamil愛他美 嬰兒配方奶粉2+段600g
)
)
八、詞典生成導出工具java
3)解壓縮有四個文件readme.txt xdb.class.php make_xdb_file.php dump_xdb_file.php,其中make_xdb_file.php是從文件生成xdb的,dump_xdb_file.php是生成文本文件的,執行過程以下
php make_xdb_file.php 字典文件 文本文件
php dump_xdb_file.php 字典文件 文本文件
dump比較快,make很慢,因此自定義分詞不要放在標準庫裏,仍是單獨作文件吧,而後生成獨立的字典
九、正式上線須要作的
1)要使用字典文件,而且加載到內存裏,這樣能夠提升一下分詞速度,以下
$ php make_xdb_file.php new.xdb new.txt
$ cp new.xdb /usr/local/scws/etc
$ vim Pf.php
$so->set_dict('/usr/local/scws/etc/dict.utf8.xdb',SCWS_XDICT_MEM);
$so->add_dict('/usr/local/scws/etc/new.xdb',SCWS_XDICT_MEM);
2)若是要支持英文部分搜索,如搜索deb便可看到deben,使用*匹配,修改Pf.php,以下
$scws=$scws.' "'.$tmp[$i]["word"].'*" ';
這須要英文切詞支持,indexer中須要有min_infix_len屬性