以前在IBM做后端开发时,也接触过关于缓存技术,当时给了n多文档来学习,后面由于其他紧急的项目,一直没有着手去仔细研究这个技术,即时后来做Commerce的时候,后台用了n多缓存技术,需要build index,甚至在category里面都用了缓存,其实一直不明白,为什么那么简单的数据都需要做缓存技术,在技术角度来说,肯定是能实现的,当category比较稳定的状态,确实用起来会比较快吧,其实,当数据小的时候,或者当没有大数据类型 text的时候,未必哪种技术就更优秀呢。
记录一下,之前我接触过的里面,是会用到kafka去做index的更新。
先说用搜索引擎的原因:笔者做在公司 网站www.baicaio.com后台记录20万条以上,且有不断增长的趋势,而由于历史原因,之前编辑的文章,对标题,标签,文章内容(text) 没有做很好的规范,且用户真正想搜索的东西,可能会在文章内容里面。搜索时会比较慢,给用户体验不好,为了让体验好起来,我们之前只搜索标题,标签,虽然如此,但针对 keyword1_keyword2 这种,最后都会转换成 column1 like "%keyword1%keyword2%" or column1 like "%keyword2%keyword1%" or column2 like "%keyword1%keyword2%" or column2 like "%keyword2%keyword1%", 这个sql语句的效率可想而知。
解决方案:创建自己的搜索引擎
实践过程:我们准备试试xunsearch,因为它免费,服务器linux,前端php,于我们而言,很完美的适配,这可能就是为什么说php是世界上最好的语言的原因了吧。:)
1.download and unzip
wget http://www.xunsearch.com/download/xunsearch-full-latest.tar.bz2 tar -xjf xunsearch-full-latest.tar.bz2
2.install
cd xunsearch-full-1.3.0/ sh setup.sh
//run setup.sh的时候出现乱码,解决方案,把setup里面的中文echo去掉,再运行就好了.
3.启动服务
bin/xs-ctl.sh -b local start // 监听在本地回环地址 127.0.0.1 上 bin/xs-ctl.sh -b inet start // 监听在所有本地 IP 地址上 bin/xs-ctl.sh -b a.b.c.d start // 监听在指定 IP 上 bin/xs-ctl.sh -b unix start // 分别监听在 tmp/indexd.sock 和 tmp/searchd.sock
4.创建项目配置文件
$prefix/sdk/php/app/baicai.ini
这里要引用官方的文档,里面说得最详细,摘出来给不喜欢看文档的朋友们,当然,如果你只是想做下试验,这些细节确实没必要看:
-
服务器连接参数
服务端连接参数的格式包含 3 种格式:
- 端口号(数字),连接
localhost
的该端口号 (例:8383
) - 地址:端口号,冒号连接地址(域名、IP地址)和端口 (例:
127.0.0.1:8383
) - 文件路径,本机的
unix socket
连接路径 (例:/tmp/index.sock
)
; 索引服务端配置,默认值为 8383 server.index = 8383 ; 搜索服务端配置,默认值为 8384 server.search = 8384
Note: 自
1.4.7
起,服务端地址可以使用;
分隔指定多个。 索引更新将同步到所有服务端,而搜索则随机从中挑选一个可用的服务端以达到均横效果。 - 端口号(数字),连接
3. 项目字段设计
每个搜索项目均可以简单地理解为单表检索,凡是涉及关联表的, 请将关联记录转换为搜索项目的新字段并设置对应的分词规则。字段设计很重要, 请仔细根据需求创建,字段名建议和您的实际数据库字段一致。
-
定义字段
每个字段用一个区段配置来表示,中括号内的名字即为字段名。每个项目包含若干个字段, 具体由项目搜索需求决定,并不需要一味的与实际源数据库 (如
MYSQL
) 一致, 而只需要设计搜索功能所涉及的字段即可。[field_name]
-
字段选项
每个字段根据实际情况指定字段选项,所有选项均有默认值,所以即便不指定任何选项而只有中括号定义的字段, 那也是一个合法的字段,字段选项包括以下几种:
type 字段类型
- string 字符型,适用多数情况,也是默认值
- numeric 数值型,包含整型和浮点数,仅当字段需用于以排序或区间检索时才设为该类型,否则请使用 string 即可
- date 日期型,形式为 YYYYmmdd 这样固定的 8 字节,如果没有区间检索或排序需求不建议使用
- id 主键型,确保每条数据具备唯一值,是索引更新和删除的凭据,每个搜索项目必须有且仅有一个 id 字段,该字段的值不区分大小写
- title 标题型,标题或名称字段,至多有一个该类型的字段
- body 内容型,主内容字段, 即本搜索项目中内容最长的字段,至多只有一个该类型字段,本字段不支持字段检索
type = string
index 索引方式
xunsearch 的索引有 2 种模式:其一是不标明字段的检索,称之为“混合区检索”;其二是标明特定字段的“字段检索”。 例如:搜索
XXX YYY
表示在混合区检索,返回的结果可能是 title 也有可能是 body 字段符合匹配; 而搜索title:XXX
则表示仅检索 title 匹配 XXX 的数据。每个字段可以指定的索引方式的值如下:- none 不做索引,所有的搜索匹配均与本字段无关,这个字段只用于排序或搜索结果展示用到。
- self 字段索引,可以在搜索时用 field:XXX 来检索本字段
- mixed 混合区索引,不标明字段的默认搜索也可以检索本字段
- both 相当于 self + mixed,两种情况均索引
通常情况默认值为 none ,但 id 型字段默认是 self ,title 型字段是 both ,body 型字段则固定为 mixed 。
index = none
tokenizer 分词器
默认为 default 采用内置的功能强大的 scws 分词,适合绝大多数字符串字段。也可以指定自定义分词器, 格式为 name 或 name(arg) 两种形式,其中 name 是分词器名称,arg 则是传递给分词器构造函数的参数。 自定义分词器需要在 lib/ 目录下编写名为 XSTokenizerName 的分词类并实现接口 XSTokenizer, 内置支持的分词器有以下几种:
- full 表示本字段的值整体作为一个检索词,像各种 ID 都适合这种情况
- none 表示本字段没有任何词汇用于索引
- split([ ]) 表示根据参数分割内容,默认参数为空格,若参数以 / 开头并以 / 结尾则 内部调用
preg_split(arg, ..)
来分割取词,以支持正则或其它特殊字符分割 - xlen([2]) 表示根据指定参数长度分段取词,如 ABCDEF => AB + CD + EF
- xstep([2]) 表示根据指定参数步长逐段取词,如 ABCDEF => AB + ABCD + ABCDEF
- scws([3]) 表示采用指定参数为复合等级的 scws 分词,(若无特殊复合需求,无需指定)
tokenizer = default
Note: 小括号内的值表示参数,中括号表示省略后的默认值,实际编写请勿照抄中括号!!!
cutlen 搜索结果摘要截取长度
默认值为 0 表示不截取。主要是针对某些内容特别长的字段在返回结果时自动剪取包含关键词的一小段文字。 典型的是 body 型字段默认为 300 。长度单位是字节,通常 UTF-8 编码的一个汉字为 3 个字节。
cutlen = 0
weight 混合区检索时的概率权重
在混合检索时,可以对标题和内容等不同字段进行权重计算,如果你不想该字段参与计算权重可设为 0 。 通常默认值为 1 ,但 title 型默认为 5 而 body 型则固定为 1 。
weight = 1
phrase 是否支持精确检索
即当给搜索语句加上引号时,则要求匹配的结果必须严格按照搜索词的顺序匹配,此外还支持用 NEAR 之类的语法来做精确检索,具体参见:搜索技巧 。通常默认值为 no 但是 title 和 body 型字段默认则为 yes 。值得注意的是该功能仅支持默认分词器,如非必要请勿开启此项, 因为这会增加索引数据的大小。
phrase = no
non_bool 强制指定是否为布尔索引
布尔索引不参与权重排名计算,默认情况下所有自定义分词器的字段均为布尔索引。因此, 当您使用自定义分词器却又想让本字段参与权重计算的话,请将本项设为 yes。
non_bool = yes
4. 配置示例文件
下面是 discuz 搜索项目的配置示范文件,包含 12 个字段。其中可以看到 tid
和 fid
虽然从内容上讲它们都是数字型,但没有排序需求所以仍应为 string
类型,而 dateline
由于有排序需求,所以必须指定为 numeric
类型。
project.name = sample project.default_charset = GBK ;server.index = 8383 ;server.search = 8384 [pid] type = id [subject] type = title [message] type = body [dateline] type = numeric [author] index = both [authorid] [tid] index = self tokenizer = full [fid] index = self tokenizer = full [flag]
上面的规则很重要,但是对于做试验来说,不重要。如果当你真正要设计一个搜索时,你会回过头来仔细研究这个文档,来满足你的搜索需求,比如我需要对某些字段进行搜索,我会考虑多加一些self字段进来。
然后就是根据这个ini来创建自己的索引,这个过程可能需要几十秒到1分钟。
/usr/local/xunsearch/sdk/php/util/Indexer.php --rebuild --source=mysql://dbuser:dbpwd@dbhost/db_name/ --sql="select xxx from xxx where xxx" --project=baicai
在我build的时候,httpd会down掉,目前不知道原因,需要重启
做完这些之后,就相当于你的搜索库已经创建好了,你可以在/usr/local/xunsearch/data/baicai 下面看到属于你的文件:
然后我们在搜索时,需要加入这段代码:
require LIB_PATH . 'Pinlib/php/lib/XS.php';
$xs = new XS('baicai');
$search = $xs->search; // 获取搜索对象
$search->setLimit(20,20*($p-1));
$search->setSort('add_time',false);
$search->setQuery($q);
$docs = $search->search();
记得注意的是docs不能直接拿来我们的php用,我们需要再转换一次.
$item_list = Array();
foreach ($docs as $doc) {
$item['title'] = $doc->title;
$item['img'] = $doc->img;
$item['id'] = $doc->id;
$item['price'] = $doc->price;
$item['add_time'] = $doc->add_time;
$item['zan'] = $doc->zan;
$item['hits'] = $doc->hits;
$item['comments_cache'] = $doc->comments_cache;
$item['orig_id'] = $doc->orig_id;
$item['go_link'] = $doc->go_link;
array_push($item_list,$item);
}
需要把方法变成属性。
然后试试你的搜索吧,是不是快了很多呢?但是还没完。
当你对你的数据编辑之后,你会发现搜索结果没有变,这是为什么呢?
1.我们的搜索是针对自己用xunsearch build出来的文件进行搜索,数据库变了,这些文件并没有变,所以需要对文件索引进行更新,当加入一条数据,要add一个索引,当remove,需要del一个索引,当update,需要update一个索引。
if($data['status'] == '1' ){
$doc = new XSDocument;
$xu_data = M("item")->where("id=$data[id]")->find();//查询数据库
$doc->setFields($xu_data);
//更新到索引数据库中
$index->update($doc);
}
else{
$index->del($data['id']);
}
我们只需要把status=1的数据收录其中,所以会这样去写,大家可以根据自己的项目需求去灵活处理这一段逻辑。
这个索引只是针对我们的xunsearch的数据表,并不会提交到数据库中,所以大家还是要保留自己的数据库update的部分。