近期接到的项目中有一个功能——手机拍照搜索试题。首先进行分析,客户端调用ocr接口识别照片中的文字,服务端拿到文字去题库进行搜索。对后端开发的我来说,问题就变为如何根据关键词搜索匹配度较高的题目。一个全新的问题,之前没有接触过,此时内心有些崩溃,不知道能不能实现,而且要给出开发工期,要怎么办?姑且先给5天吧,如果调研失败,我有最后的保留方案——用mysql全文索引实现(在类似需求中应用过,有局限性,不是根据语义分析而是根据标点拆分文章,看句子是否在题目中出现过)。
调研开始发现百度App有拍照搜题的功能,既然人家能实现,说明这个问题是有解的,进一步增强了自己的信心。
后来发现很多关于simHash的文章,其中一篇:simhash算法原理及实现。用简单的话解释一下simhash:这个算法可以把任何文章都哈希成一个64位的二进制码,语义越相近的文章,获得的二进制码的汉明距离越小。如果感兴趣,大家可以深入研究一下。
基于这个理论,经过大量尝试(其中不乏开源代码,在此非常感谢),终于完成了下面的类。simhash第一步首先要分词,故这个类用到了php的scws这个拓展。
如果想要了解scws怎样安装,点这
<?php
class Glo_Simhash {
/**
* 全角字符转变成半角字符
* @param $str
* @return mixed
*/
function replace_DBC2SBC($str) {
$DBC = Array(
'0' , '1' , '2' , '3' , '4' ,
'5' , '6' , '7' , '8' , '9' ,
'A' , 'B' , 'C' , 'D' , 'E' ,
'F' , 'G' , 'H' , 'I' , 'J' ,
'K' , 'L' , 'M' , 'N' , 'O' ,
'P' , 'Q' , 'R' , 'S' , 'T' ,
'U' , 'V' , 'W' , 'X' , 'Y' ,
'Z' , 'a' , 'b' , 'c' , 'd' ,
'e' , 'f' , 'g' , 'h' , 'i' ,
'j' , 'k' , 'l' , 'm' , 'n' ,
'o' , 'p' , 'q' , 'r' , 's' ,
't' , 'u' , 'v' , 'w' , 'x' ,
'y' , 'z' , '-' , ' ' , ':' ,
'。' , ',' , '/' , '%' , '#' ,
'!' , '@' , '&' , '(' , ')' ,
'<' , '>' , '"' , ''' , '?' ,
'[' , ']' , '{' , '}' , '\' ,
'|' , '+' , '=' , '_' , '^' ,
'¥' , ' ̄' , '`' , '“' , '”',
';' , '·'
);
$SBC = Array(
'0', '1', '2', '3', '4',
'5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y',
'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x',
'y', 'z', '-', ' ', ':',
'.', ',', '/', '%', '#',
'!', '@', '&', '(', ')',
'<', '>', '"', '\'','?',
'[', ']', '{', '}', '\\',
'|', '+', '=', '_', '^',
'$', '~', '`', '"', '"',
';', '.'
);
return str_replace($DBC, $SBC, $str);
}
function hashCode($str) {
if(empty($str)) return '';
$mdv = md5($str);
$mdv1 = substr($mdv,0,16);
$mdv2 = substr($mdv,16,16);
$crc1 = abs(crc32($mdv1));
$crc2 = abs(crc32($mdv2));
$code = decbin(bcmul($crc1,$crc2));
$code = str_repeat('0',64 - strlen($code)).$code;
return $code;
}
function hashCode64($str) {
$len = 8;
$md5 = substr(md5($str), 0, $len);
$seed = 31;
$hash = 0;
for($i = 0; $i < $len; $i++) {
$hash = $hash*$seed+ord($md5{$i});
}
$hash = $hash & 0x7FFFFFFF;
$hash = decbin(bcmul($hash,$hash));
$hash = str_repeat('0',64 - strlen($hash)).$hash;
return $hash;
}
//采用scws分词
function getSimHash($text){
$so = scws_new();
$so->set_charset('utf8'); //编码
$so->set_duality(0); //散字二元
$so->set_ignore(0); //忽略标点符号
$so->set_multi(0);
$str = $this->replace_DBC2SBC($text);
//过滤字符
$filter = [',','.','-','_','`','、','"',"'",":",';','<','>','{','}','(',')'];
$so->send_text($str);
$keyList = array();
while($words = $so->get_result())
{
foreach($words as $word){
$s = $word['word'];
$weight = intval($word['idf'])*20;
if(!in_array($s,$filter) && $weight){
$hash = array();
$hash_code = $this->hashCode64($s);
for($i=0;$i<64;$i++){
$value = intval(substr($hash_code,$i,1));
if($value==1){
$hash[] = $weight;
}else{
$hash[] = -$weight;
}
}
$keyList[] = $hash;
}
}
}
$finCode = '';
for($i=0;$i<64;$i++) {
$code = 0;
if(empty($keyList)) {
break;
}
foreach($keyList as $key) {
$code+=intval($key[$i]);
}
if($code>0) {
$finCode .= '1';
} else {
$finCode .= '0';
}
}
return $finCode;
}
}
创建倒排索引数据表
CREATE TABLE `simhash_syintelligence` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`hashcode16` char(16) NOT NULL DEFAULT '' COMMENT '16位2进制hash',
`dp` text NOT NULL COMMENT '倒排字段 内容:(题目id:hashcode位置;题目id:hashcode位置)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
接下来,如何实现就非常明了了。现将接收到的关键词做一次simHash,然后将值分隔成4段,去表中分4次查询,找到位置对应的题目id即可。
以上是猜想,基于ocr准确识别的情况,但联调时发现,照片识别不是很准确,经常出现类似“问”识别为“间”的情况。simHash是基于语义分析的,我上面的方案必须保证分割的4块中其中一块完全相同,错一字,汉明距离就变大好多,导致可能匹配不到。还有一种情况是,会返回毫不相关的文章,因为这样做返回文章的汉明距离为0-48,于是又改变了策略,调整表的设计:
CREATE TABLE `simhash_syintelligence` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`ques_id` INT(10) NOT NULL DEFAULT '0' COMMENT '题目id',
`hashcode64` CHAR(64) NOT NULL DEFAULT '' COMMENT '64位2进制hash',
PRIMARY KEY (`id`),
KEY `hashcode64` (`hashcode64`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
将题目内容完整hash记录下来,不再做切割。拿到关键词,先做一次simHash,然后轮询表,两两比较汉明距离,如果汉明距离小于某个特定值,则将题目返回回来。
算是一种思路,但是实际应用中发现哈希碰撞的概率挺高的,搜索一段英文,完全没有联系的中文被搜出来的,效果不太理性;
可以换成es全文搜索引擎做