初探dedecms的搜索原理(二)
dedecms

接上文,arc.searchview.class.php类
从开头看,dede的类已经很陈旧了
var $dsql; var $dtp; var $dtp2; var $TypeID; var $TypeLink; var $PageNo; var $TotalPage; var $TotalResult; var $PageSize; var $ChannelType; var $TempInfos; var $Fields; var $PartView; var $StartTime; var $Keywords; var $OrderBy; var $SearchType; var $mid; var $KType; var $Keyword; var $SearchMax; var $SearchMaxRc; var $SearchTime; var $AddSql; var $RsFields; var $Sphinx;
var已经被弃用了,php4时代的东西
我们看看这个类参数传递
$sp = new SearchView($typeid,$keyword,$orderby,$channeltype,$searchtype,$starttime,$pagesize,$kwtype,$mid);
这个是类的构造定义
function __construct($typeid,$keyword,$orderby,$achanneltype="all",$searchtype='',$starttime=0,$upagesize=20,$kwtype=1,$mid=0)
往下看
global $cfg_search_max,$cfg_search_maxrc,$cfg_search_time,$cfg_sphinx_article; if(empty($upagesize)) { $upagesize = 10; }
global调用的是/data/config.cache.inc.php,对应的值分别为
$cfg_search_max = 50000; $cfg_search_maxrc = 300; $cfg_search_time = 3; $cfg_sphinx_article = 'N';
并初始化每页显示最大数为10
继续往下看
$this->TypeID = $typeid; $this->Keyword = $keyword; $this->OrderBy = $orderby; $this->KType = $kwtype; $this->PageSize = (int)$upagesize; $this->StartTime = $starttime; $this->ChannelType = $achanneltype; $this->SearchMax = $cfg_search_max; $this->SearchMaxRc = $cfg_search_maxrc; $this->SearchTime = $cfg_search_time; $this->mid = $mid; $this->RsFields = ''; $this->SearchType = $searchtype=='' ? 'titlekeyword' : $searchtype; $this->dsql = $GLOBALS['dsql']; $this->dtp = new DedeTagParse(); $this->dtp->SetRefObj($this); $this->dtp->SetNameSpace("dede","{","}"); $this->dtp2 = new DedeTagParse(); $this->dtp2->SetNameSpace("field","[","]"); $this->TypeLink = new TypeLink($typeid);
前几行没什么好说的,类参数的传值。而$this->dsqp=$GLOBALS['dsql'],可见dede也是有全局变量的,打印一下,$GLOBALS五百来行。
大致都是一些配置信息。
$this->dtp = new DedeTagParse(); $this->dtp->SetRefObj($this); $this->dtp->SetNameSpace("dede","{","}"); $this->dtp2 = new DedeTagParse(); $this->dtp2->SetNameSpace("field","[","]");
构造类,并声明了两个命名空间
接下来
// 通过分词获取关键词 $this->Keywords = $this->GetKeywords($keyword);
这个是值得好好的研究一下的,这是dede的分词。
实际测试
$keyword = 'PHP的命名空间(namespace)是php5.3之后才有的。这个概念在C#中已经很早就有了,php中的namespace其实和c#的概念是一样的。'; echo $this->Keywords = $this->GetKeywords($keyword);
得到的结果为 :PHP的命名空间(namespace)是 namespace 命名 空间
可见它是把字符串截取,然后再重新分词,经过多次测试,观察到它的分词还算理想。可见其后台的关键词自动获取也是通过些原理弄出来的。
看一下其分词方法
function GetKeywords($keyword) { global $cfg_soft_lang; $keyword = cn_substr($keyword, 50); $row = $this->dsql->GetOne("SELECT spwords FROM `#@__search_keywords` WHERE keyword='".addslashes($keyword)."'; "); if(!is_array($row)) { if(strlen($keyword)>7) { $sp = new SplitWord($cfg_soft_lang, $cfg_soft_lang); $sp->SetSource($keyword, $cfg_soft_lang, $cfg_soft_lang); $sp->SetResultType(2); $sp->StartAnalysis(TRUE); $keywords = $sp->GetFinallyResult(); $idx_keywords = $sp->GetFinallyIndex(); ksort($idx_keywords); $keywords = $keyword.' '; foreach ($idx_keywords as $key => $value) { if (strlen($key) <= 3) { continue; } $keywords .= ' '.$key; } $keywords = preg_replace("/[ ]{1,}/", " ", $keywords); //var_dump($idx_keywords);exit(); unset($sp); } else { $keywords = $keyword; } $inquery = "INSERT INTO `#@__search_keywords`(`keyword`,`spwords`,`count`,`result`,`lasttime`) VALUES ('".addslashes($keyword)."', '".addslashes($keywords)."', '1', '0', '".time()."'); "; $this->dsql->ExecuteNoneQuery($inquery); } else { $this->dsql->ExecuteNoneQuery("UPDATE `#@__search_keywords` SET count=count+1,lasttime='".time()."' WHERE keyword='".addslashes($keyword)."'; "); $keywords = $row['spwords']; } return $keywords; }
开头是定义字符编码,然后截取字符串.
接着通过关键词查询数据库中的search_keywords表中有无数据,$keyword进行了简单的过滤处理。
然后,如果keyword字符长度大于7,注utf8中一个中文3长度,也就是说超过两个中文,进行分词处理。此时调用了分词类,SplitWord,此类早已经开源放出,网上许多人在用。
$sp = new SplitWord($cfg_soft_lang, $cfg_soft_lang); $sp->SetSource($keyword, $cfg_soft_lang, $cfg_soft_lang); $sp->SetResultType(2); //1 为全部, 2去除特殊符号 $sp->StartAnalysis(TRUE); //是否对结果进行优化 $idx_keywords = $sp->GetFinallyIndex();
得到结果后,去除单个字的情况, unset($sp);然后销毁类,这个学习了,以前用完类后没有进行过销毁处理,资源该节约的还是要节约的。
接着把关键词,和分后的词入库.(该关键词不存在,就插入入库,存在的话,搜索量加1入库)
这个设计还是很赞的,这个表search_keywords中存有,搜索过的关键词(唯一索引),分词处理过后的数据,查询量,以及查询结果数。
但是我发现,如果随便一个词,不管能不能搜到结果,它都是入库的。我认为最好是查询到数据以后才能入库,这样的话比较合理一些。
接下来
if ($cfg_sphinx_article == 'Y') { // 初始化sphinx $this->sphinx = new SphinxClient; $mode = SPH_MATCH_EXTENDED2; //匹配模式 $ranker = SPH_RANK_PROXIMITY_BM25; //统计相关度计算模式,仅使用BM25评分计算 $this->sphinx->SetServer($GLOBALS['cfg_sphinx_host'], $GLOBALS['cfg_sphinx_port']); $this->sphinx->SetArrayResult(true); $this->sphinx->SetMatchMode($mode); $this->sphinx->SetRankingMode($ranker); $this->CountRecordSphinx(); } else { $this->CountRecord(); }
它在这里是有关于sphinx的调用的,不过默认$cfg_sphinx_article是为N的,而且sphinx是要工作里在于安装和配置,已经自身索引的搭建,所以这个并没有多大的作用的,所以技术虽然是好技术,但是有多少人有那个能力,服务器有这个条件能搭起来sphinx环境呢,所以只能走$this->CountRecord()了,我们来看看此方法
function CountRecord() { $this->TotalResult = -1; if(isset($GLOBALS['TotalResult'])) { $this->TotalResult = $GLOBALS['TotalResult']; $this->TotalResult = is_numeric($this->TotalResult)? $this->TotalResult : ""; } if(isset($GLOBALS['PageNo'])) { $this->PageNo = intval($GLOBALS['PageNo']); } else { $this->PageNo = 1; } $ksql = $this->GetKeywordSql(); $ksqls = array(); if($this->StartTime > 0) { $ksqls[] = " arc.senddate>'".$this->StartTime."' "; } if($this->TypeID > 0) { $ksqls[] = " typeid IN (".GetSonIds($this->TypeID).") "; } if($this->ChannelType > 0) { $ksqls[] = " arc.channel='".$this->ChannelType."'"; } if($this->mid > 0) { $ksqls[] = " arc.mid = '".$this->mid."'"; } $ksqls[] = " arc.arcrank > -1 "; $this->AddSql = ($ksql=='' ? join(' AND ',$ksqls) : join(' AND ',$ksqls)." AND ($ksql)" ); if($this->ChannelType < 0 || $this->ChannelTypeid< 0){ if($this->ChannelType=="0") $id=$this->ChannelTypeid; else $id=$this->ChannelType; $row = $this->dsql->GetOne("SELECT addtable FROM `#@__channeltype` WHERE id=$id"); $addtable = trim($row['addtable']); $this->AddTable=$addtable; }else{ $this->AddTable="#@__archives"; } $cquery = "SELECT * FROM `{$this->AddTable}` arc WHERE ".$this->AddSql; //var_dump($cquery); $hascode = md5($cquery); $row = $this->dsql->GetOne("SELECT * FROM `#@__arccache` WHERE `md5hash`='".$hascode."' "); $uptime = time(); if(is_array($row) && time()-$row['uptime'] < 3600 * 24) { $aids = explode(',', $row['cachedata']); $this->TotalResult = count($aids)-1; $this->RsFields = $row['cachedata']; } else { if($this->TotalResult==-1) { $this->dsql->SetQuery($cquery); $this->dsql->execute(); $aidarr = array(); $aidarr[] = 0; while($row = $this->dsql->getarray()) { if($this->ChannelType< 0 ||$this->ChannelTypeid< 0) $aidarr[] = $row['aid']; else $aidarr[] = $row['id']; } $nums = count($aidarr)-1; $aids = implode(',', $aidarr); $delete = "DELETE FROM `#@__arccache` WHERE uptime<".(time() - 3600 * 24); $this->dsql->SetQuery($delete); $this->dsql->executenonequery(); $insert = "INSERT INTO `#@__arccache` (`md5hash`, `uptime`, `cachedata`) VALUES('$hascode', '$uptime', '$aids')"; $this->dsql->SetQuery($insert); $this->dsql->executenonequery(); $this->TotalResult = $nums; } } }
前几行,isset($GLOBALS['TotalResult'],isset($GLOBALS['PageNo']没有找到在哪定义的,所以说$this->TotalResult只能为-1了, $this->PageNo也只能为1了...以后找到了,在补吧
看这一句
$ksql = $this->GetKeywordSql();具体方法
function GetKeywordSql() { $ks = explode(' ',$this->Keywords); $kwsql = ''; $kwsqls = array(); foreach($ks as $k) { $k = trim($k); if(strlen($k)<1) { continue; } if(ord($k[0])>0x80 && strlen($k)<2) { continue; } $k = addslashes($k); if($this->ChannelType < 0 || $this->ChannelTypeid < 0){ $kwsqls[] = " arc.title LIKE '%$k%' "; }else{ if($this->SearchType=="title"){ $kwsqls[] = " arc.title LIKE '%$k%' "; }else{ $kwsqls[] = " CONCAT(arc.title,' ',arc.writer,' ',arc.keywords) LIKE '%$k%' "; } } } if(!isset($kwsqls[0])) { return ''; } else { if($this->KType==1) { $kwsql = join(' OR ',$kwsqls); } else { $kwsql = join(' And ',$kwsqls); } return $kwsql; } }
这个方法,值的说的
if(ord($k[0])>0x80 && strlen($k)<2)
{
continue;
}
在前面的话,已经有很多条件限制分词以后的字数了,这个判断感觉不是很有意义阿。还有这个:
if($this->KType==1) { $kwsql = join(' OR ',$kwsqls); } else { $kwsql = join(' And ',$kwsqls); }
我现在不是很理解,刚开始构造函数的时候,$typeid根本就是为0的,程序走到这里,肯定执行的是else,也就是说是用and连接的,这样的话,问题就来了,如果我搜索后,分成两个词加上原keywords,最后where条件应该类似于这样的样式:arc.title LIKE '%xxxxx%' And arc.title LIKE '%xxx%' And arc.title LIKE '%xx%',这样模糊查询还有什么意义?这个条件理论上终止于arc.title LIKE '%xxxxx%',只要他满足,下面的必定满足,它不满足,也就查询不到,真是让人费解。所以说我认为构造的时候,一定要让$typeid为1,这样的话,查询才有意义。
接下来,就没什么好说的了,无法是构造完整的sql语句,查询出结果,然后把sql语句md5加密,该更新的更新,更入库的入库,至此dedecms查询机制初步研究完成。