初探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查询机制初步研究完成。
