站内搜索初级优化
php1>. 概述:
站内搜索引擎顾名思义即网站内的信息搜索引擎,随着网络的发展,网站已经成为了企业或机构最重要的公共形象门户。每天,大量潜在的客户、合作者、投资人,分析师等会登陆企业的网站,网站带给他们的感受将直接影响到他们对公司的评价。根据IDC的调查显示:当用户登陆一个网站时,在一开始如果不能很快地检索到他所需要的信息,则50%的用户会立刻离开此网站,其中的60%将不再光顾这个网站,这意味着公司将永远失去30%的潜在客户。
当然,我也没去考证过上面的数据准不准,但是可以看出站内的搜索的展示结果质量的准确度对用户的体验是很重要的。
注:以下搜索引擎都特指站内的搜索
php2>. 搜索引擎的自我修养:
一个优秀的站内搜索除了一个醒目美观的搜索框外,最重要的是能快速准确的给出用户所检索的结果,此外还有一些附加功能可以提升用户的体验:
1. 自动提示:不仅能减少错误输入,还能帮助我们推荐产品与产品分类;
2. 自动纠错: 与“无搜索结果”相比,显示点结果总会减少些访客跳出。但这是一把双刃剑,若是推荐的词质量太低,搜索会显得很不专业;
3. 相关搜索:基于同义词的能容推荐,能给访客一些未想到的搜索提示,加大覆盖面,也加能增加用户的点击量。
4. 结果过滤或者在结果中搜索:给用户更精确的搜索体验;
5. 排序方式:如果搜索有多重属性,比如form站的下载量、点击次数或者评分高低,这样能让用户在靠前的位置找到他的关注内容;
6. 高级搜索…..
不在此一一列举
php3>. 搜索引擎的核心技术:
而这其中涉及到的技术有:分词技术(还好我们不做中文网站)、页面抓取分析(全文检索)、建立索引、搜索匹配和排序算法、对搜索关键词的统计、关联、推荐等算法。
php4>. 站内搜索的常用做法:
使用大型商业搜索引擎提供的接口:
比如google、yahoo,国内的baidu 的API
优点:简单省事,申请账户,使用API;
缺点:1. 不能了解具体的搜索排序机制,不能对展示的结果做相应的控制,也不利于进行调整;
2. 免费版本有广告,影响体验。自己实现:
2.1) sql 的 like 查询:
代码实现比较简单,需要完全匹配搜索的字符串,否则搜索不出结果,多关键字的搜索结果展示差;2.2) 基于分词的搜索:
有一些开源的项目:Java里比较有名的Lucene,口碑也很好,也有很多其他基于它的其他项目,可以支撑数据量较大的项目,速度很快,Java项目也可以借鉴,整合到项目里,因为是Java写的,而前期需要嵌入form站,所以只能忍痛割爱;
review的第二个开源项目是Sphinx,C++写的,也比较主流,快,索引较大,搜索精度不如Lecence,试用了一下其编译好的exe文件,速度确实快,听说搜索亿级的数据的时间也在毫秒级,建立索引的时间在小时级,后期可以考虑使用;但是他们都有的问题是,牛逼闪闪但是项目较为庞大,封装出了接口给我们调用,我们要修改内部的算法,可能要track的代码较多,考虑到时间因素,选择了一个轻量级的搜索框架sphider,整个项目的代码量才不到300K, 估计撑死就一万行,而且他是基于mysql和php的,看完之后简直爽high了,这正是我们需要找的东西,跟踪其代码走一遍,整体上能大概了解一个搜索引擎的工作原理,我们下面展示的搜索就是移植和微调了一下其搜索方面的功能:
step 1>
获得源数据:如果要检索网页内容,我们需要建立爬虫爬取需要检索的网页的内容,存数据库,但我们pdf转的html实际上不太规整,而且搜索的关键词绝大部分都是cat或者post的name,所以我们省去了这一步骤,直接取数据库里的字段作为元数据;
step 2>
分词,提取关键词,建立索引, 代码见:SearchindexController.class.php
1)新加数据表:keywords表,keyword_post多张,keyword_cat多张
2)接口:indexallpost(), indexallcat()
注:分词速度较慢,后期跳出thinkphp的框架,用纯SQL写了一个提速版本,索引40万数据,大概需要4-5min,当然与aphinx等比较还有较大的差距,有机会再放出来
代码逻辑:
->indexPost() & indexCat(): 取数据源内容
->unique_word_array(): 每条数据按照多重规则分词(分隔符、忽略词、提取词干)
->计算权重(因为description等都是自动生成的,无意义,所以权重只是对keyword在数据源中出现的次数简单的计算)
->save_post_keywords(): 插入数据库的keywords表,(keyword唯一)
->save_post_keywords(): 然后插入多张关系表(delete_post_keywords_relation(): 事先删除关系表里该post的数据,多表的存在可以缓解单表的压力)
至此,分词完毕!
<?php
namespace Admin\Controller;
use Admin\Controller\CommonController;
/**
* @author chijiaodaxie
*/
class SearchindexController extends CommonController {
//only indexpost and indexcat were public as APIs
private $keywords_array = array();
public function indexAllPost($reindex = 0){
set_time_limit(0);
$this->keywords_array = $this->get_all_keyword();
// dump($this->keywords_array);
$post_db = D('Post');
if($reindex){
echo "post全部重新索引, 马力全开<br/><br/>";
$posts = $post_db->field(array('postid', 'name', 'catid'))->where(array('status'=>4))->select();
}else{
echo "增量索引, 为新增post加索引<br/><br/>";
$posts = $post_db->field(array('postid', 'name', 'catid'))->where(array(/*'status'=>4, */'indexed'=>0))->select();
}
$post_ids = array();
$failed_ids = array();
foreach($posts as $post){
$res = $this->indexPost($post['postid'], $post['name'], $post['catid']);
if($res){
$post_ids[] = $post['postid'];
}else{
$failed_ids[] = $post['postid'];
}
}
$post_id_str = implode(",", $post_ids);
$data['indexed'] = 1;
$post_db->where('postid in ('.$post_id_str.')')->save($data);
if ($failed_ids){
echo count($failed_ids)." posts was not index successly: <br/><br/>友情提示!注意乱码问题<br/><br/>";
echo "Success ids: (".implode(", ", $post_ids).")<br/><br/>";
echo "Failed ids: (".implode(", ", $failed_ids).")";
}else{
echo "Success ids: (".implode(", ", $post_ids).")<br/><br/>";
echo "index success!!";
}
}
private function indexPost($postid, $postname, $catid){
$keywords = $this->unique_word_array($postname);
$this->delete_post_keywords_relation($postid);
$res = $this->save_post_keywords($keywords, $postid, $catid);
if(!$res){
return false;
}
return true;
}
public function indexAllCat($reindex = 0){
set_time_limit(0);
$this->keywords_array = $this->get_all_keyword();
$cat_db = D('Category');
if($reindex){
echo "cat全部重新索引, 马力全开<br/><br/>";
$cats = $cat_db->field(array('catid', 'catname', 'parentid'))->where(array('disabled'=>0, 'ismenu'=>1))->select();
}else{
echo "增量索引, 为新增cat加索引<br/><br/>";
$cats = $cat_db->field(array('catid', 'catname', 'parentid'))->where(array('disabled'=>0, 'ismenu'=>1, 'indexed'=>0))->select();
}
$cat_ids = array();
$failed_ids = array();
foreach($cats as $cat){
$res = $this->indexCat($cat['catid'], $cat['catname'], $cat['parentid']);
if($res){
$cat_ids[] = $cat['catid'];
}else{
$failed_ids[] = $cat['catid'];
}
}
$cat_id_str = implode(",", $cat_ids);
$data['indexed'] = 1;
$cat_db->where('catid in ('.$cat_id_str.')')->save($data);
if ($failed_ids){
echo count($failed_ids)." cats was not index successly: <br/><br/>友情提示!注意乱码问题<br/><br/>";
echo "Success ids: (".implode(", ", $cat_ids).")<br/><br/>";
echo "Failed ids: (".implode(", ", $failed_ids).")";
}else{
echo "index success!!<br/><br/>";
echo "Success ids: (".implode(", ", $cat_ids).")";
}
}
private function indexCat($catid, $catname, $parentid){
$keywords = $this->unique_word_array($catname);
$this->delete_cat_keywords_relation($catid);
$res = $this->save_cat_keywords($keywords, $catid, $parentid);
if(!$res){
return false;
}
return true;
}
private function unique_word_array($str){
if(is_array($str) && !empty($str)){
$str = implode(" ", $str);
}
$str = strtolower($str);
$str = preg_replace("/ /", " ", $str);
$str = preg_replace("/[\*\^\+\?\\\.\[\]\^\$\|\{\)\(\}~!\"\/@#£$%&=`´;><:,]+/", " ", $str);
$str = preg_replace('/\s+/', ' ', $str);
$arr = explode(" ", $str);
$min_word_length = C('MIN_WORD_LENGTH');
$word_upper_bound = C('WORD_UPPER_BOUND');
$index_numbers = C('INDEX_NUMBER');
$stem_words = C('STEM_WORDS');
$common = $this->get_common_word();
if ($stem_words == 1) {
$stem_word = new \Common\Plugin\Stem();
$newarr = array();
foreach ($arr as $val) {
$newarr[] = $stem_word->stem($val);
}
$arr = $newarr;
}
sort($arr);
reset($arr);
$newarr = array();
$i = 0;
$counter = 1;
$element = current($arr);
if ($index_numbers == 1) {
$pattern = "/[a-z0-9]+/";
} else {
$pattern = "/[a-z]+/";
}
$regs = array();
for ($n = 0; $n < sizeof($arr); $n ++) {
//check if word is long enough, contains alphabetic characters and is not a common word
//to eliminate/count multiple instance of words
$next_in_arr = next($arr);
if ($next_in_arr != $element) {
// $element = rtrim($element, ".,");
if (preg_match("/^(-|\\\')(.*)/", $element, $regs))
$element = $regs[2];
if (preg_match("/(.*)(\\\'|-|\'s|\')$/", $element, $regs))
$element = $regs[1];
if (strlen($element) > $min_word_length && preg_match($pattern, $this->remove_accents($element)) && (@ $common[$element] <> 1)) {
$newarr[$i][1] = $element;
$newarr[$i][2] = $counter;
$element = current($arr);
$i ++;
$counter = 1;
} else {
$element = $next_in_arr;
$counter = 1;
}
} else {
if ($counter < $word_upper_bound)
$counter ++;
}
}
// var_dump($newarr);
return $newarr;
}
/*
* save the keywords to post related table
*/
private function save_post_keywords($keywords, $post_id, $cat_id){
// $this->keywords_array;
$table_num = C('POST_KEYWORDS_NUM');
foreach($keywords as $keyword){
$word = $keyword[1];