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

    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 scws0.php
      "我"  "是"  "一个"  "中国人"  "我爱"  "我"  "的"  "祖国" 
      这里要说明一下,这个分词程序用起来是大同小异,可以在主页看详细文档,这里做了一个处理,将每个词增加了双引号,这样是为了在sphinxql中调用方便。
      $so->add_dict可以增加其他词典,词典是xdb格式,也可以用文本,只不过文本要慢一些,初期可以用文本,等正式上线再生成xdb文件。下面以文本为例,内容如下

7、做一个简单的demo

     # Pf.php
 

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属性
05-02 22:05