PHP搜索引擎完美之路

SJY发表于:2018年06月08日 12:14 • 阅读:

我没有服务器,无法搭建Sphinx搜索引擎。我不会SDK,不会搭建阿里云搜索接口。但是我会点皮毛PHP,搭建一个自己的小型搜索引擎还是可以做到的!

我想要的搜索引擎效果

  • 能够实现全文搜索
  • 能够根据相关度来排序
  • 能够让搜索关键字高亮


接下来我将用最原始的PHP代码来构建搜索引擎,没有类,没有对象,当然更没有封装,原始的PHP更能理解其含义,随着不断发现和解决问题,走向完美之路。

以WordPress数据库作为本例的实验对象,代码如下

<?php
mysql_connect('localhost','root','')or die(mysql_error());
mysql_select_db('wpys');
mysql_query("SET NAMES GBK"); 
if($_GET[key]){
$sql="SELECT * FROM wp_posts WHERE post_title LIKE '%$_GET[key]%' or post_content LIKE '%$_GET[key]%' ORDER BY ((CASE WHEN post_title LIKE '%$_GET[key]%' THEN 2 ELSE 0 END) + (CASE WHEN post_content LIKE '%$_GET[key]%' THEN 1 ELSE 0 END))DESC,post_date DESC";#搜索结果的条件是文章标题或文章内容中出现关键字;排序方式为 如果标题中出现关键字则为2分否则0,如果内容中出现关键字为1分否则0,然后把他们的积分加起来,按照分数从高到低排序,这样标题中包含关键字的数据会排在更前面、其次是内容中包含、最后按发布时间排序。
$result=mysql_query($sql)or die(mysql_error());
while($row=mysql_fetch_array($result)){
$row[post_title]=preg_replace("/($_GET[key])/i","<font color=red><b>\\1</b></font>",$row[post_title]);#把标题中的关键字替换为高亮关键字
$content=strip_tags($row[post_content]);#去除内容中的HTML标签,搜索结果列表中不需要有HTML标签
$content=preg_replace("/($_GET[key])/i","<font color=red><b>\\1</b></font>",$content);#把内容中的关键字替换为高亮关键字
echo '<h2>'.$row[post_title].'</h2>';#输出文章标题
$pos=strpos($content,"$_GET[key]");#找到搜索关键字的位置,为了截取关键字周围的文字
if($pos<100){#如果关键字正好出现在文章的头100个文字中,那么起始位置就从该关键字位置开始
$start=$pos;
}else{#如果关键字的位置在文章的100个文字之后,那么起始位置就截取关键字开始前的100个文字
$start=$pos-100;
}
$rest=mb_strcut("$content",$start,200,'GBK');#设置文章摘要的开始位置,以及摘要的长度,由于是按字节截取的,1个中文是2个字节,所以字符长度必须是双数,如果设为单数很可能会把中文劈成2半,出现乱码。
echo '<p>'.$rest.'</p>';
}
}
?>

<form action="" method="get">
关键字:
<input type="text" name="key">
<input type="submit" name="sub" value="全站检索">
</form>



该代码看似已经实现了基本要求,实则存在一定的问题.如果高亮部分正好被截断呢?完整的高亮部分代码为 <font color=red><b>文字</b></font>
如果前面部分被截断,则不会高亮,源代码可能是这样 red><b>文字</b></font>
如果后面部分被截断,那么会更糟糕,后面所有的内容都会高亮,源代码可能是这样 <font color=red><b>文字


我找到了一种解决方案,或许不是唯一的方案,但自我感觉还是非常不错的!思路如下
不需要在PHP中就进行高亮替换,这样在PHP进行截断的时候就不会出现正好是高亮部分被截断,完全可以在一切做完后,用JS来实现高亮替换。而且用PHP进行替换势必会在HTML代码中加入很多高亮的代码,对搜索引擎来讲未必是一件好事。

<?php
mysql_connect('localhost','root','')or die(mysql_error());
mysql_select_db('wpys');
mysql_query("SET NAMES GBK"); 
?>
<!doctype html>
<html>
<head>
<meta charset="gbk">
<title><?=$_GET[key]?>相关文章</title>
<style>
#searchresult{width:650px;margin:0 auto;}
#searchresult em{color:red;font-weight:bold;}
</style>
</head>
<body>
<div id="searchresult">
<form action="" method="get">
关键字:
<input type="text" name="key">
<input type="submit" name="sub" value="全站检索">
</form>

<?php
if($_GET[key]){
$sql="SELECT * FROM wp_posts WHERE post_title LIKE '%$_GET[key]%' or post_content LIKE '%$_GET[key]%' ORDER BY ((CASE WHEN post_title LIKE '%$_GET[key]%' THEN 2 ELSE 0 END) + (CASE WHEN post_content LIKE '%$_GET[key]%' THEN 1 ELSE 0 END))DESC,post_date DESC";
$result=mysql_query($sql)or die(mysql_error());
while($row=mysql_fetch_array($result)){
$content=strip_tags($row[post_content]);
echo '<h2>'.$row[post_title].'</h2>';
$pos=strpos($content,"$_GET[key]");
if($pos<100){
$start=$pos;
}else{
$start=$pos-100;
}
$rest=mb_strcut("$content",$start,200,'GBK');
echo '<p>'.$rest.'</p>';
}
}
?>
</div>
<script>
<!--找到id为searchresult的标签,然后把里面所有的查询关键字替换为高亮关键字-->
var txt = document.getElementById('searchresult').innerHTML;
txt = txt.replace(/<?=$_GET[key]?>/g, "<em><?=$_GET[key]?></em>");
document.getElementById('searchresult').innerHTML = txt;
</script>
</body>
</html>

由于去掉了PHP中的查询关键字替换功能,就不用在循环中,每次查询都用正则替换一次。在JS中的替换是整个搜索结果页列出来以后,再把查询的关键字做替换,也就是说只执行了1次替换操作,大大提高了效率。那么如果我要搜索多个关键字,PHP代码又该怎么写?我已经帮你想好了

<?php
mysql_connect('localhost','root','')or die(mysql_error());
mysql_select_db('wp');
mysql_query("SET NAMES GBK"); 
?>
<!doctype html>
<html>
<head>
<meta charset="gbk">
<title><?=$_GET[key]?>相关文章</title>
<style>
#searchresult{width:650px;margin:0 auto;}
#searchresult em{color:red;font-weight:bold;}
</style>
</head>
<body>
<div id="searchresult">
<form action="" method="get">
关键字:
<input type="text" name="key">
<input type="submit" name="sub" value="全站检索">
</form>

<?php
if($_GET[key]){
$k=explode(' ',$_GET[key]);#把关键字中的空格拆分为数组,比如搜索‘网页 设计’就会被拆分成2个关键字,网页、设计
for($i=0;$i<count($k);$i++){
$tarr[]="post_title LIKE '%$k[$i]%'";#把所有搜索的关键字列出来,写成sql语句,并保存为数组
$carr[]="post_content LIKE '%$k[$i]%'";
$jsarr[]="replace(/$k[$i]/g,\"<em>$k[$i]</em>\")";#把所有的搜索关键字列出来,写成javascript语句,并保存为数组
}
$tk=implode(' or ',$tarr);#把数组重新组合成字符串,用 or 来隔开,用于SQL中的条件语句
$ck=implode(' or ',$carr);
$jsk=implode('.',$jsarr);#把数组重新组合成字符串,用 . 来隔开,用于javascript中的替换语句
$sql="SELECT * FROM wp_posts WHERE $tk or $ck ORDER BY ((CASE WHEN $tk THEN 2 ELSE 0 END) + (CASE WHEN $ck THEN 1 ELSE 0 END))DESC,post_date DESC";
$result=mysql_query($sql)or die(mysql_error());
while($row=mysql_fetch_array($result)){
$content=strip_tags($row[post_content]);
for($i=0;$i<count($k);$i++){
$pos=strpos($content,$k[$i]);#检查关键字在内容中的位置
if($pos>0)break;#如果获取到位置则跳出循环,获取不到继续循环直到获取到位置或循环结束为止
}
if(!$pos){#如果pos为0或不存在,位置就从0开始
$start=0;
}elseif($pos<100){#如果pos小于100个字节,位置从pos处开始
$start=$pos;
}else{#如果大于100个字节,位置从pos的前100个位置开始,
$start=$pos-100;
}
$rest=mb_strcut("$content",$start,200,'GBK');#截取200个字节的内容,注意这里是中文,所以用GBK,截取字节数为双数,因为GBK中中文占2个字节
echo '<h2>'.$row[post_title].'</h2>';
echo '<p>'.$rest.'</p>';
}
}
?>
</div>
<script>
<!--找到id为searchresult的标签,然后把里面所有的查询关键字替换为高亮关键字-->
var txt = document.getElementById('searchresult').innerHTML;
txt = txt.<?=$jsk?>;<!--由于要用到多次替换,故在PHP中就已经把要替换的关键字循环列出来,并以javascript的语法写成字符串,在这里调用-->
document.getElementById('searchresult').innerHTML = txt;
</script>
</body>
</html>

到这里为止,小型的PHP搜索引擎已经构建完成了,一般用用早就够了。
那么能否再更强大一点呢?
比如 我希望在搜索’网页 设计‘时,在标题中同时包含’网页‘、’设计‘排名要比只包含其中一个词的要高,然后内容中也是一样,同时包含的比只包含一个的要高,有时标题中包含的不一定更相关,所以如果标题中只包含一个,内容中却同时包含了,内容中有关键词的排名要高于标题中有关键词的。希望得到更强大的PHP搜素引擎,那么就继续往下看

<?php
mysql_connect('localhost','root','')or die(mysql_error());
mysql_select_db('wp');
mysql_query("SET NAMES GBK"); 
?>
<!doctype html>
<html>
<head>
<meta charset="gbk">
<title><?=$_GET[key]?>相关文章</title>
<style>
#searchresult{width:650px;margin:0 auto;}
#searchresult em{color:red;font-weight:bold;}
</style>
</head>
<body>
<div id="searchresult">
<form action="" method="get">
关键字:
<input type="text" name="key">
<input type="submit" name="sub" value="全站检索">
</form>

<?php
if($_GET[key]){
$k=explode(' ',$_GET[key]);
for($i=0;$i<count($k);$i++){
$tarr[]="post_title LIKE '%$k[$i]%'";
$carr[]="post_content LIKE '%$k[$i]%'";
$jsarr[]="replace(/$k[$i]/gi,\"<em>$k[$i]</em>\")";
}
$tk=implode(' or ',$tarr);
$ck=implode(' or ',$carr);
$jsk=implode('.',$jsarr);

$sql="SELECT * FROM wp_posts WHERE $tk or $ck";
$result=mysql_query($sql)or die(mysql_error());

while($row=mysql_fetch_array($result)){
$sqlarr[]=$row;
}
if(is_array($sqlarr)){#判断是否有搜索结果,如果没有搜索结果就不是数组,执行下去会出错也没有任何意义,所以只有是数组才能执行下去

/*------------开始进行权值计算--------------*/

foreach($sqlarr as $key=>$value){
$title=strtolower($value[post_title]);#由于substr_count统计字符串出现次数是区分大小写的,所以这里把标题中的英文字母转成小写,方便后面的统计
$content=strtolower($value[post_content]);

for($i=0;$i<count($k);$i++){
$tc+=substr_count($title,$k[$i]);#把每次在标题中查询到的关键字的数量进行累加,比如搜索'功夫 李小龙'出现功夫2次,李小龙3次,那么最终$tc=5
echo $cc+=substr_count($content,$k[$i]);
}

$tc*=10;#标题中的关键词出现的次数,乘以10作为权重分
$cc*=6;#内容中的关键词出现的次数,乘以6作为权重分

$totalw=$tc+$cc;#标题的权重分+内容的权重分=总权重分
$value[weight]=$totalw;#给数组新增加一个值,关键词为weight,值为标题权重分+内容权重分
$weight[$key] = $value[weight];#将关键字 weight 的值循环后赋给数组 weight[$key]

$tc=0;#把$tc清0,否则下次执行 累加操作,会以上次得到的$tc值继续
$cc=0;
//print_r($value);
}
array_multisort($weight,SORT_DESC,$sqlarr);#数组根据weight的值来降序排序
foreach($sqlarr as $key=>$v){
$content=strip_tags($content);#去除内容中的HTML标签,搜索结果列表中不需要有HTML标签
for($i=0;$i<count($k);$i++){
$pos=strpos($content,$k[$i]);#检查关键字在内容中的位置
if($pos>0)break;#如果获取到位置则跳出循环,获取不到继续循环直到获取到位置或循环结束为止,这么做是为了保证截取的内容是搜索的关键词周边文字,实在没有就在循环结束后从位置0开始
}
if(!$pos){#如果pos为0或不存在,位置就从0开始
$start=0;
}elseif($pos<100){#如果pos小于100个字节,位置从pos处开始
$start=$pos;
}else{#如果大于100个字节,位置从pos的前100个位置开始,
$start=$pos-100;
}
$rest=mb_strcut("$content",$start,200,'GBK');#截取200个字节的内容,注意这里是中文,所以用GBK,截取字节数为双数,因为GBK中中文占2个字节

echo '<h2><a href="'.$v[guid].'" target="_blank">'.$v[post_title].'</a></h2>';
echo '<p>'.$rest.'</p>';

}
}else{
        echo '<p>没有搜索到任何相关的内容</p>';
}
}
?>
</div>
<script>
var txt = document.getElementById('searchresult').innerHTML;
txt = txt.<?=$jsk?>;
document.getElementById('searchresult').innerHTML = txt;
</script>
</body>
</html>


现在我可以做到搜索多个关键字,还能够以相关度来排序了。
比如说搜索‘中国 功夫’,那么有以下几种情况

  • 标题中同时包含‘中国’和‘功夫’要比只包含其中一个词的排名来的高;
  • 内容中同时包含‘中国’和‘功夫’要比标题中只包含其中一个词的排名来的高;
  • 其他条件相等,标题中包含要比内容中包含关键字排名要高。

但还是有缺点,标题中包含3个中国的权值分要比标题中同时包含一个中国和一个功夫的权值分要高,显然后者更相关,在站内搜索中这种重复出现同一个关键词的几率并不高,但追求完美嘛,所以还得继续下去!

下面我希望做到相关度更高,且使用户输入关键字后自动分词,首先得去下载用于PHP的SCWS 中文分词,下载 PSCWS23 就够用了。
 

<?php
mysql_connect('localhost','root','')or die(mysql_error());
mysql_select_db('wp');
mysql_query("SET NAMES GBK"); 
?>
<!doctype html>
<html>
<head>
<meta charset="gbk">
<title><?=$_GET[key]?>相关文章</title>
<style>
#searchresult{width:650px;margin:0 auto;}
#searchresult em{color:red;font-weight:bold;}
</style>
</head>
<body>
<div id="searchresult">
<form action="" method="get">
关键字:
<input type="text" name="key">
<input type="submit" name="sub" value="全站检索">
</form>
<h1>搜索 <?=$_GET[key]?> 的结果</h1>

<?php
$t1 = microtime(true);
// ... 时间计算,执行代码 ...

if($_GET[key]){
/*-----分词开始------*/
require 'fenci/pscws3.class.php';
$pscws = new PSCWS3('fenci/dict/dict.xdb');
$pscws->set_autodis(true);#识别人名
$pscws->set_ignore_mark(true);#忽略标点符号
$k = $pscws->segment($_GET[key]);

$kc=count($k);#先统计好数组的数量,如果放在循环条件中,由于循环过程中会删除一部分数组元素,会导致循环提前结束
for($i=0;$i<$kc;$i++){
if(strlen($k[$i])<3){unset($k[$i]);}#如果字符长度小于3,就删除这个元素。一个中文长度为2,搜索一个字是没有意义的
}
sort($k);#通过unset(),数组的键名已不再连续,所以需要用sort函数重排键名,从0开始连续增加
print_r($k);#打印分词后的数组

/*-----分词结束------*/
for($i=0;$i<count($k);$i++){
$tarr[]="post_title LIKE '%$k[$i]%'";
$carr[]="post_content LIKE '%$k[$i]%'";
$jsarr[]="replace(/$k[$i]/gi,\"<em>$k[$i]</em>\")";
}
$tk=implode(' or ',$tarr);
$ck=implode(' or ',$carr);
$jsk=implode('.',$jsarr);

$sql="SELECT * FROM wp_posts WHERE $tk or $ck";
$result=mysql_query($sql)or die(mysql_error());

while($row=mysql_fetch_array($result)){
$sqlarr[]=$row;
}
if(is_array($sqlarr)){

/*------------开始进行权值计算--------------*/

        foreach($sqlarr as $key=>$value){
        for($i=0;$i<count($k);$i++){
        $tkpos=stripos($value[post_title],$k[$i]);#查询关键词在标题中的位置,由原来的strpos修改为stripos,因为搜索英语时会出现大小写,stripos忽略大小写
        if($tkpos!==false){$tc+=10;}#如果标题中包含关键字则累加10分,当搜索中国 功夫,标题中同时包含这2个词,$tc就会累加到20。修改了原先的代码,不再比关键字出现次数,而是比是否包含了我所搜的所有关键字,包含的关键字越多,则越相关
        //$ckpos=stripos($value[post_content],$k[$i]);
        //if($ckpos!==false){$cc+=6;}#内容中不能用标题中的方法,因为内容太长了,很可能头部出现中国,尾部出现功夫,也就是说这篇文章可能跟中国功夫根本就不相关,所以本行及上一行代码注释,需要另想办法

        $ke=stripos($value[post_content],$k[$i]);
        $kb=@stripos($value[post_content],$k[$i-1]);#由于第一次循环,$k[$i-1]是不存在的,会报错,但之后的循环必须用上他,所以用@来隐藏错误
        if($ke!==false&&$kb!==false){#被减数和减数必须有值,也就是说两个关键字必须都存在与内容中才能去算他们的位置距离
        $kv=abs($ke-$kb);
        if($kv<30){$cc+=12;}#两个存在的关键字之间距离小于30,累加12分,这样比只有一个关键字在标题中高2分
        }

        }
        //标题中包含2个关键字是20分,包含3个关键字是30分
        //内容中包含2个关键字是12分,包含3个关键字是24分
        //也就是说标题和内容包含的关键字相等时,标题权重高;内容包含的关键字个数大于标题中时,内容权重高

        $value[weight]=$tc+$cc;
        $weight[$key] = $value[weight];
        $tc=0;
        $cc=0;
        $reorder[]=$value;#把增加新字段weight后的数组赋值给$reorder[]
        }
/*------------权值计算结束--------------*/
array_multisort($weight,SORT_DESC,$reorder);
foreach($reorder as $key=>$v){
$content=strip_tags($v[post_content]);
for($i=0;$i<count($k);$i++){
$pos=stripos($content,$k[$i]);
if($pos>0)break;
}
if(!$pos){
$start=0;
}elseif($pos<100){
$start=$pos;
}else{
$start=$pos-100;
}
$rest=mb_strcut("$content",$start,200,'GBK');
//print_r($v);

echo '<h2><i>权重'.$v[weight].'</i>  <a href="'.$v[guid].'" target="_blank">'.$v[post_title].'</a></h2>';
echo '<p>'.$rest.'</p>';

}
}else{
        echo '<p>没有搜索到任何相关的内容</p>';
}
}

//时间计算结束
$t2 = microtime(true);
echo '耗时'.round($t2-$t1,5).'秒';
?>
</div>
<script>
var txt = document.getElementById('searchresult').innerHTML;
txt = txt.<?=$jsk?>;
document.getElementById('searchresult').innerHTML = txt;
</script>
</body>
</html>

自动分词,相关度排序,都已经搞定了。为了精确相关度,我把分词后的单个中文去掉了,因为只有一个字并不容易显现出其意义,即使是英文,搜索2个字母也没什么意义。

我在本地测试执行效率还是很高的,基本上1秒内就搜索到结果。当然如果你对PHP理解的够深,就把代码该精简的精简,该封装的封装,完成后别忘了分享出来,非常欢迎你以留言的方式把代码分享出来!
补充:
最近发现一个问题,当搜索的关键字包含英文,并且正好URL中出现这个关键字时,js把我的URL也给替换了,导致无法正确打开页面。比如搜索 phpcms教程 我的URL里也正好有 phpcms ,于是就把他替换成了<em>phpcms</em>
我重新写了一个js,只替换标题标签h2下的a下的内容,以及描述标签p下的内容,问题就解决了,代码如下

<script>
var krt=document.getElementById('searchresult').getElementsByTagName('h2');//找到id为searchresult下的h2标签
var krc=document.getElementById('searchresult').getElementsByTagName('p');//找到id为searchresult下的p标签
var arrt = [];//定义数组,在循环里把标题替换部分赋值到这个数组
var arrc = [];//在循环时,把内容替换部分赋值到这个数组
for(var i=0;i<krt.length;i++){//因为在id为searchresult下的h2标签有很多,所以需要通过循环,对所有的h2标签里的内容进行替换,在这里h2标签的数量跟p标签的数量相同,所以不用重复写循环
arrt[0] = krt[i].getElementsByTagName("a")[0].innerHTML.<?=$jsk?>;//获取h2标签下的第一个a标签里的HTML内容,并进行替换操作,替换后赋值给arrt[0];这里之所以要获取a标签下的内容,是由于一旦关键字出现在URL中,会把URL也进行替换导致无法访问,所以要替换的内容是a标签里的HTML内容,而非直接是h2下的内容
krt[i].getElementsByTagName("a")[0].innerHTML=arrt[0]; //把a标签下的内容重写为已经替换好的内容
arrc[0] = krc[i].innerHTML.<?=$jsk?>; //由于p标签下并不存在关键字出现在URL的情况,所以可以直接获取内容并进行替换
krc[i].innerHTML=arrc[0]; //重写为替换后的内容
}
</script>

注:var arrt = []; 也可以不定义为数组,定义一个普通变量也一样,var abc;
 

欢迎转载,但请保留原文地址 http://www.sjyhome.com/php/1392.html

标签: 搜索引擎

回复(0)