Sphinx学习笔记2

    因为网站搜索的需要,启动了一个搜索引擎项目,其实也算不上完整的搜索引擎,需求很简单,如下:

    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、环境
     1)CentOS : 6.5和7.0均可
     2)编译环境: yum install gcc gcc-c++
 2、sphinx
     1)sphinx主页在http://sphinxsearch.com/,文档在http://sphinxsearch.com/docs/current.html,比较详细
     2)编译(在测试服务器172.16.8.97上的步骤)
          $ tar xvf sphinx-2.2.10-release.tar.gz
          $ 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没什么可改的,对于我们的系统也够用了。
3、索引生成和查询
    1)indexer --all --rotate
         --all代表重新生成索引, --rotate用于searchd服务已经启动的情况下
    2)searchd 
        启动搜索服务
4、使用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
5、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();
6、使用scws分词
     scws分词主页在http://www.xunsearch.com/scws/,这是一个开源分词程序,可以自定义分词,速度也比较快,也比较简单,所以就用这个了。
     1)配置
     $ tar xvf scws-1.2.2.tar.bz2
     $ 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
<?php
error_reporting(0);
$so = scws_new();
$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_set_multi($so,2);  
$so->set_ignore(true); //忽略标点符号
//$text=$_GET["text"];
$text='我是一个中国人,我爱我的祖国';
$so->send_text($text);
$scws='';
while ($tmp = $so->get_result())   // get_result()要重复调用,直到所有的分词结果都返回为止
{
  for($i=0;$i<count($tmp);$i++){
        $scws=$scws.' "'.$tmp[$i]["word"].'" ';
  }
}
$so->close();
echo substr($scws,1);
?>
      $ php scws0.php
      "我"  "是"  "一个"  "中国人"  "我爱"  "我"  "的"  "祖国" 
      这里要说明一下,这个分词程序用起来是大同小异,可以在主页看详细文档,这里做了一个处理,将每个词增加了双引号,这样是为了在sphinxql中调用方便。
      $so->add_dict可以增加其他词典,词典是xdb格式,也可以用文本,只不过文本要慢一些,初期可以用文本,等正式上线再生成xdb文件。下面以文本为例,内容如下
爱他美  100     100     nt
施华蔻  100.01  100.01  nt
瘦身    100.02  100.02  nt
护眼    100.03  100.03  nt
1段     100.04  100.04  nt
2段     100.04  100.04  nt
3段     100.05  100.05  nt
4段     100.06  100.06  nt
5段     100.07  100.07  nt
结构很简单,第一列是分词,第二列是tf,第三列是idf,第四列是词性,用起来很容易,如果没有这个文件,将上面的程序中的$text值修改为“爱他美”,会输出“爱”“他”“美”,如果增加了这个分词文件,并将scws0.php中的注释去掉,执行结果就变成了“爱他美”。
到此,分词问题就解决了,如果多个服务器使用也很简单,可以将这个程序对外发布。

7、做一个简单的demo

     # Pf.php
<?php
error_reporting(0);
class Pf {
        public static function splitSearch($search){
                $so = scws_new();
                $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_set_multi($so,2);
                $so->set_ignore(true);
                $text=$search;
                //$text='爱他美';
                $so->send_text($text);
                //scws_add_dict($so,'/usr/local/scws/etc/new.txt',SCWS_XDICT_TXT);
                $scws='';
                while ($tmp = $so->get_result())
                {
                        for($i=0;$i<count($tmp);$i++){
                                $scws=$scws.' "'.$tmp[$i]["word"].'" ';
                        }
                }
                $so->close();
                return substr($scws,1);
 
        }
}
#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
        )

)

8、词典生成导出工具

  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很慢,所以自定义分词不要放在标准库里,还是单独做文件吧,然后生成独立的字典
9、正式上线需要做的
   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);
      修改set_dict和add_dict函数的参数
  2)如果要支持英文部分搜索,如搜索deb即可看到deben,使用*匹配,修改Pf.php,如下
        $scws=$scws.' "'.$tmp[$i]["word"].'*" ';
       这需要英文切词支持,indexer中需要有min_infix_len属性
posted @ 2016-04-25 15:17  stone-fly  阅读(417)  评论(0编辑  收藏  举报