最近做了一个站搜高亮显示的需求,需要对结果中的搜索内容做高亮显示。看了下我们系统现有架构,用【ES高亮功能】是可以满足这个需求处理的,不需要再引入其他组件。
1. ES 高亮功能
highlight 语句使用,具体使用可看 ES高亮
DSL语句支持指定高亮标签,如下所示,highlight 单独指定字段content和高亮标签
```
GET test/_search
{
"query":{},
"highlight":{
"fields":{
"contnet": {
"pre_tags":[
"<div>"
],
"post_tags":[
"</div>"
]
}
}
}
} ``` 也可以全局设置高亮字段和标签
"highlight": {
"pre_tags": ["<div class='hightLight'>"],
"post_tags": ["</div>"],
"fields": {
"content.search": {}
}
}
因为php 处理空 [] => {} 很麻烦,就没有使用全局的方式。用了第一种方式。
2. 处理关键字
正常highlight 可以返回高亮数据,如下:
"hits" : [
{
"_index" : "testtiku1",
"_type" : "_doc",
"_id" : "170825439135",
"_score" : 42.778034,
"_source" : {
"id" : 170825439135,
"state" : 0,
"content" : "一元二次方程的根为( )x=1x=-1x=2"
},
"highlight" : {
"content.search" : [
"<div class='hightLight'>一</div><div class='hightLight'>元</div><div class='hightLight'>二</div><div class='hightLight'>次</div><div class='hightLight'>方</div><div class='hightLight'>程</div>的根为( )x=1x=-1x=2"
]
}
},
但是我们content字段置存储了文本信息,图片和latex并没有存储,所以这个返回的高亮数据并不能直接返回客户端使用。我利用highlight中的高亮做关键词,然后重新匹配content中的内容,重复做了一次字符串匹配工作。这样项目中省去引入分词组件,利用es的highlight做分词关键字,对content 内容做关键字过滤。
因为content 内容会比较大,为了提高关键字过滤的效率,采用字典树的方式过滤关键字。简单说下字典树,假设关键字有【日本人,本日,日狗】,就是将关键字创建成一个如下图所示的树,然后去匹配需要过滤的 text ,时间复杂度为 O(nlogn):
php代码如下:
<?php
/**
* 指定词的替换,敏感词或高亮处理
*/
class WordFilter
{
private $_tire;//敏感词字典
public function __construct($arrWords)
{
foreach ($arrWords as $strWord) {
$this->addWord(trim($strWord));
}
}
/**
* 分割文本
* @param $str
* @return array[]|false|string[]
*/
protected function splitWord($str)
{
//将字符串分割成组成它的字符
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
/**
* 添加敏感字至节点
* @param $strWord
*/
protected function addWord($strWord)
{
$arrWord = $this->splitWord($strWord);
$curNode = &$this->_tire;
foreach ($arrWord as $char) {
// 判断是否为null
if (!isset($curNode)) {
$curNode[$char] = [];
}
$curNode = &$curNode[$char];
}
//标记到达当前节点完整路径
$curNode['end'] = 1;
}
/**
* 默认情况为敏感词遮挡
* 对字符串进行过滤,比如 str=可耻的日本人,对其中的 '日本' 进行敏感词遮挡或 进行高亮显示,
* 结果为 str= 可耻的**人或 可耻的*日本*人。
*
* @param string $strText 需要操作的字符串
* @param bool $bolReplace 是否替换,false 时,表示包裹字符,默认替换
* @param string $strPre 默认替换字符,包裹时为前置包裹字符 如 dd*A*ee
* @param string $strSuf 后置替换字符
* @return string
*/
public function filter($strText, $bolReplace = true, $strPre = '*', $strSuf = '*')
{
$arrStrText = $this->splitWord($strText);
$arrWrap = []; //包裹字符下标,统一处理
$intLength = count($arrStrText);
$i = 0;
while ($i < $intLength) {
$curChar = $arrStrText[$i];
if (!isset($this->_tire[$curChar])) { //判断当前敏感字是否有存在对应节点
$i++;
continue;
}
$curNode = &$this->_tire[$curChar];
$matchIndex = [$i]; //匹配后续字符串是否match剩余敏感词
if (!isset($curNode['end'])) { // 当前节点是否是单独的关键字
for ($j = $i + 1; $j < $intLength; $j++) {
if (!isset($curNode[$arrStrText[$j]])) { // 当前关键字不存在对应节点
break;
}
//如果匹配到的话,则把对应的字符所在位置存储起来,便于后续敏感词替换
$matchIndex[] = $j;
//继续引用
$curNode = &$curNode[$arrStrText[$j]];
}
}
//判断是否已经到敏感词字典结尾,是的话,进行敏感词替换
if (isset($curNode['end'])) {
foreach ($matchIndex as $index) {
if ($bolReplace) {
$arrStrText[$index] = $strPre;
} else {
//统一处理
// $arrStrText[$index] = $strPre . $arrStrText[$index] . $strSuf;
$arrWrap[] = $index;
}
}
$i = max($matchIndex);
}
$i++;
}
$strRet = '';
if ($bolReplace) {
$strRet = implode('', $arrStrText);
} else {
$strRet = static::reduceTag($arrWrap, $arrStrText, $strPre, $strSuf);
}
return $strRet;
}
/**
* 连续出现的命中字符,统一包裹,减少单个包裹的标签量
* @param $arrWrap
* @param $arrStrText
* @param $strPre
* @param $strSuf
* @return string
*/
public static function reduceTag($arrWrap, $arrStrText, $strPre, $strSuf)
{
$begin = $arrWrap[0] ?? 0;
$end = $arrWrap[0] ?? 0;
for ($i = 1; $i < count($arrWrap); $i++) {
if ($arrWrap[$i] - $end == 1) {
$end++;
} else {
$arrContinue = array_slice($arrStrText, $begin, $end - $begin + 1);
$strTmp = $strPre . implode('', $arrContinue) . $strSuf;
for ($j = $begin; $j <= $end; $j++) {
$arrStrText[$j] = '';
}
$arrStrText[$end] = $strTmp;
$begin = $arrWrap[$i];
$end = $arrWrap[$i];
}
}
if (!empty($arrWrap) && ($end == $arrWrap[count($arrWrap) - 1])) {
$arrContinue = array_slice($arrStrText, $begin, $end - $begin + 1);
$strTmp = $strPre . implode('', $arrContinue) . $strSuf;
for ($j = $begin; $j <= $end; $j++) {
$arrStrText[$j] = '';
}
$arrStrText[$end] = $strTmp;
}
return implode('', $arrStrText);
}
}
$keyword = ['日本人', '本日', '日狗', 'fuck', '日'];
$str = '为日本本日去你的狗日日狗feofohg 日本人fuck狗';
$obj = new WordFilter($keyword);
$r = $obj->filter($str);
var_dump($r);
输出
"为*本**去你的狗**狗feofohg *本人****狗"
其中日狗不会匹配到,因为关键字 日,日狗匹配时,按照最短路径匹配,如果要按照最长路径匹配,去掉 73 行 if (!isset($curNode['end'])) 这个判断即可。
结果为:
为日本**去你的狗***feofohg *******狗
3. 富文本高亮显示
最后用正则对富文本进行添加标签即可。
// 匹配所有标签性质字符串
preg_match_all('/</?[^>]+>/', $strText, $arrMatch);
// 去掉html标签
$arrText = preg_split('/</?[^>]+>/', $strText) ?: [];