【迅搜11】搜索技巧(一)简单搜索语句构建及高亮折叠效果

搜索技巧(一)简单搜索语句构建及高亮折叠效果

学习完索引管理相关的内容之后,我们就进入到了搜索技巧相关的学习了。其实对应在 XS 中,就是 SDK 中的 XSSearch 对象的相关学习和使用。同样的,在这一部分,我们也会普及很多搜索相关的知识。

其实对于这个 XSSearch 对象,我们并不陌生,之前很多次,很多地方都已经用过了。只不过我们都是只用了它的最简单的一种使用方式。

代码语言:javascript
复制
$xs->search->search("xxxx");

它会返回一个由 XSDocument 对象组合成的数组,想必这部分内容也不用我多解释了。

其实,在这个 XSSearch 的 search() 方法上直接写搜索词,是 XS 为我们提供的一种快捷搜索方法。这个 search() 方法真正的作用是向查询服务端(端口8383)发送查询命令,并通过它继承的 XSServer 中的 respond 来获得返回的结果。关于 XSServer 部分的内容之前都已经学习过了。这里也就不多赘述了。

正式的设置查询语句、查询词的方法其实是 setQuery() 这个方法。

代码语言:javascript
复制
$xs = new XS("./config/zyarticle.ini");
$search = $xs->search; // 后期如果直接写一个 $search ,就是直接表示为 $xs->search 获得的 XSSearch 对象

print_r($search->setQuery('敏捷')->search());

其实它的效果和 search-&gt;search(&#34;敏捷&#34;);</code> 是一样的。那么如果我们同时使用 setQuery() ,并且 search() 中也有搜索词,而且两个词不一样会出现什么情况呢?</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">search->setQuery('敏捷')->search('算法');

大家可以自己打印一下结果试试,我这里则是使用返回查询数量一个属性来测试的。

代码语言:javascript
复制
$search->search('敏捷');
print_r($search->lastCount); // 37

$search->search('算法');
print_r($search->lastCount); // 63

$search->setQuery('敏捷')->search('算法');
print_r($search->lastCount); // 63

print_r($search->setQuery('敏捷')->setLimit(1000)->search('算法')); // 打印的是算法相关的数据

可以看到,最后返回的结果是 63 条,也就是说,最终查询词是以 search() 的参数为准的。大家可以运行最后一行的代码来查看返回的结果是不是 63 条。

这个地方通过源码分析的话,setQuery() 是直接将参数通过 XS_CMD_QUERY_PARSE 这个命令常量发送到服务端了。这个查询参数会直接保存在服务端。而 search() 如果有参数,就会以 search() 的参数,通过 XS_CMD_SEARCH_GET_RESULT 这个查询命令标识直接进行查询。比如我们再这样测试:

代码语言:javascript
复制
print_r($search->setLimit(1000)->search()); // 打印的是敏捷相关的数据

没有 setQuery() ,同时 search() 也没有参数,返回的结果是上一次 setQuery() 设置的查询内容,也就是 “敏捷” 相关的 37 条数据。

在这两段代码中,我们使用了一个 setLimit() 方法,它就是 XS 中的分页方法。接下来,我们就看一下这个分页的效果。

分页

默认情况下,我们不加 setLimit() 方法,那么最终的 search() 会默认返回从第 0 条数据开始的 10 条数据。也就是默认第一页的十条数据。这个和 MySQL 中的 limit 没啥太大区别,第一个参数是返回数量,第二个参数是 offset 偏移量。

代码语言:javascript
复制
print_r($search->setLimit(2)->search('')); // 返回两条数据
print_r($search->setLimit(1,1)->search('')); // 返回跳过第一条数据之后的一条数据

没啥太多好解释的吧。但是,这里要多一嘴。包括 ES 在内的大部分搜索引擎对于深分页的支持都不怎么样。什么叫深分页?就比如每页显示 10 条数据,然后显示到第 1000 页、第 10000 页以后的内容。默认情况下,ES 的分页只支持 10000 条数据,也就是说,如果每页十条数据,在 ES 中,最多也就直接分 1000 页。当然也有别的方式可以继续向下翻页,但是却无法支持跳页了(直接指定页码)。

这一块的原因其实就是在于搜索引擎会对查询结果进行分析、打分、计算。所以在分页时往往会将数据全部拿回来进行这些计算操作。如果数据量太大,即使是 ES 也抗不住,毕竟它可以把数据分片存储,但是最后分页进行打分、排序时还是要把所有分片上的数据一起拿过来进行总体计算的。基于这样的原因,它就硬性规定了最多只能处理 10000 条数据。

虽说 XS 的文档上没写,但是基于对于大部分搜索引擎(包括百度和 Google )的理解,搜索引擎对于深分页的支持都不太友好。就像百度,最多也只是到 100 页左右,大家可以试试直接访问百度超过 100 页之后的内容是什么样子的。

代码语言:javascript
复制
https://www.baidu.com/s?wd=PHP
https://www.baidu.com/s?wd=PHP&pn=750 # 我测试时可以搜索到 76 页
https://www.baidu.com/s?wd=PHP&pn=760 # 试试看访问第 77 页是什么效果

但其实,即使只是中文网页,我相信关键词包含 “PHP” 的文章甚至是网站,远不止 700 多条搜索结果。也就是说,搜索引擎其实并不需要全面,而且有的时候也并不需要完全的精准,真正的搜索引擎,需要的是找到符合用户需要的内容。因此,千万不要以为百度、Google 养得成百上千的工程师是混饭吃的。真正商业化的全网搜索引擎的技术要求,可比我们学习的这些 ES、XS 之类的工具要复杂的多。

不过 XS 没提到过这个问题,那么咱们就来测试一下,就用默认的 demo 吧。

代码语言:javascript
复制
// php
$xs = new XS("demo");
$xs->index->openBuffer();
for(i=100000;i>=1;$i--){
$xs->index->update(new XSDocument([
'pid'=>'pid'.$i,
'subject'=>'sub'.$i,
'message'=>'msg'.$i,
]));
}
$xs->index->closeBuffer();

> php vendor/hightman/xunsearch/util/Quest.php --show-query --limit=20000,10 demo ""

通过 PHP 代码向索引中添加十万条数据,然后通过 SDK 提供的查询工具,使用 --limit 参数来进行分页。可以看到最终的效果是能够顺利返回 20000 到 20010 条数据的列表。看来 XS 对这个深分页的支持还好,并且响应速度也还可以。当然,ES 有它自己的原因和道理,这里我也只是多嘴说一句,并不代表说 XS 比它强或者怎么样,只是通过测试证明,XS 是可以对超过 10000 条以上的数据进行深分页的。

快捷数量查询

数量查询,其实也就是类似于 MySQL 中的 count() 的效果。在 XS 中,我们已经在前面看到了 lastCount 属性的应用。它实际上就是返回最近一次查询结果的数量,这是个属性,因此对应的也有一个 getLastCount() 方法。但是这个属性没有 set 相关的方法,因此,这个变量属性是一个只读变量。是不是有体会到面向对象中封装的好处了?

除了这种返回最后一次查询结果数量的属性及对应的方法之外,就像上面的 search() 方法一样,XS 也为我们提供了一个快捷获得指定查询条件数量的方法,就叫 count() 。

代码语言:javascript
复制
print_r($search->count()); // 37
print_r($search->count('算法')); // 63
print_r($search->count()); // 37

不传任何参数时,count() 返回的结果也是上回查询关键词的结果数量。但是它也可以指定一个查询参数,比如第二行,但是大家会发现,第三行又变回之前的查询结果数量了。其实呀,这个查询对于查询参数的处理和 search() 是一样的,如果给了参数,按参数的来,如果没给参数,就按上一次 setQuery() 方法指定的查询条件来。

前面我们已经说过,setQuery() 是直接将查询参数传递到服务端了,而 search() 和 count() 的参数都是现拼的。如果它们有参数,就以最新的这个查询参数来执行。这个直接执行的查询参数在服务端是不会保留的,服务端只会保留通过 setQuery() 设置的,命令常量为 XS_CMD_QUERY_PARSE 的数据。同样的,直接给 count() 的参数也是针对这一次请求的,和 search() 的效果一模一样。

另外还需要注意的一点是,这个 count() 方法返回的数量是一个估算值,不是精确值。同样地,lastCount 属性及对应方法返回的数量值也是估算值,不是精确值。这又是一个什么概念呢?

如果有做过大数据量的日志统计、流量统计或者类似统计系统,或者深分页达到100页以上的同学一定会知道。有的时候,为了性能,我们的汇总数据值是可以不精确的。比如说千万条日志中统计出来的实时日活数量,误差在一定范围内都是可以授受的。包括之前我们学习过的 Redis 中的 HyperLogLog 就明确说了不精确,有多少误差,但是速度飞快,存储空间小。同样的,对于大部分搜索结果及其分页来说,本身分词就是有着不确定性以及异步索引操作的问题,数量统计也会因此产生不准确的问题。

ES 中的 count 效果一般是通过聚合 aggs 实现的,相对来说要精确一些,但是它是以更多的计算和资源消耗量为代价换来的。但它也不是完全的精确值,特别是如果采用了多分片分布式的情况下,一样是有误差率的。但它可以通过一些参数设置来调节精确度。还是那句话,看业务需求进行取舍。

索引项目总数量

最后还有一个索引项目内的文档总数量的属性。

代码语言:javascript
复制
print_r($search->dbTotal); // 339

这个没啥多说,它也有一个对应的 getDbTotal() ,没有 set 相关方法,是一个只读属性。这个数量值是不是精确的文档没说,咱也不清楚。

链式调用

通过前面的学习,其实大家已经发现了,XS 的 SDK 中的各种操作都是可以进行链式调用的。关于这种调用方式,之前在建造者模式、Laravel数据库相关的学习中我们都已经说过了。这里就简单的说一下 XS 中的应用。

在 XS 中,XSSearch 对象除了 search() 和 count() 之外,与查询有关的其它方法都是可以进行链式调用的。其实 XSIndex 也都可以,之前我们就看过,add()、update() 这些方法都是返回的 XSIndex 自身,所以完全可以这么写。

代码语言:javascript
复制
$xs->index->add(xxx)->add(xxx)->add(xxx);

不过相对来说,在操作增、删、改时,不管是数据库内核,还是代码表现形式上,我们都会追求尽量的原子化,也就是一行一行的写。逻辑更清晰,也更容易看明白,同时也符合日常的认知。

而对于搜索来说,这样链式的写就完全不违和了。

代码语言:javascript
复制
$xs->search->setQuery('敏捷')->setLimit(1000)->search('算法');

这样写是不是要比下面这样的写法清晰很多。

代码语言:javascript
复制
search = xs->search;
$search->setQuery('敏捷');
$search->setLimit(1000);
$search->search('算法');

高亮与折叠效果

大家在使用百度或者 Google 以及很多网站的搜索功能时,会发现在返回的结果中会把我们搜索的关键字标红。这个功能在 XS 中的实现非常简单。其实自己去实现也不复杂,简单来说原理就是将分词后的查询关键字,一一替换加上一个特殊的 HTML 标签。然后在前端通过给这个标签设置特定的 CSS 样式实现变红、加粗、改字体等等功能就可以了。

在 XS 中,直接使用 XSSearch 提供的 highlight() 方法就可以了。

代码语言:javascript
复制
doc = search->setLimit(1)->search('数据结构与算法')[0];
echo search-&gt;highlight(doc->title);
// PHP<em><em>数据</em><em>结构</em></em><em>与</em><em>算法</em>1】在学<em><em>数据</em><em>结构</em></em>和<em>算法</em>的时候我们究竟学的是啥?
echo PHP_EOL;
echo search-&gt;highlight(doc->content);
echo PHP_EOL; //………………

看出来效果了吧,“数据结构与算法” 通过默认分词实际上是分成了 “数据结构”、“数据”、“结构”、“与”、“算法” 这几个词。然后调用 highlight ,将一个字符串(通常就是我们的 title 和 body 指定的字段)传递给它,它就会根据分词的结果为指定字符添加上 <em> 标签。然后在前台展示时,我们就可以通过 CSS 来控制标签展示样式了。大家可以自己去看下源码,替换过程真的不复杂,只是获取分词和组合替换规则的部分要麻烦一点,原理还是我上面说过那个原理。甚至最终使用的函数就是 PHP 原生的 preg_replace()、strtr()、str_replace() 这三个之一。

它还有第二个参数,是一个布尔值,表示是否使用 strtr() 函数来进行替换处理。

代码语言:javascript
复制
echo search-&gt;highlight(doc->title,true);
// PHP<em>数据结构</em><em>与</em><em>算法</em>1】在学<em>数据结构</em>和<em>算法</em>的时候我们究竟学的是啥?
echo PHP_EOL;
echo search-&gt;highlight(doc->content,true);
echo PHP_EOL; // ………………

能看出来不同在哪里了吧,上面的 “数据结构” 套了两层 <em> ,还有一层是分开 “数据” 和 “结构” 的。而下面的只有一层完整的长词 “数据结构” 。关于 strtr() 和 str_replace() 的区别以及使用方法,大家可以自己查阅相关的资料。

另外,search() 方法的第二个参数,是表示是否保存本次分词结果到高亮变量中用于后续的高亮操作的。

代码语言:javascript
复制
doc = search->setLimit(1)->search('敏捷', false)[0];
echo search-&gt;highlight(doc->title);
// 【敏捷1.4】敏捷开发环境:领导<em>与</em>团队
echo PHP_EOL;

比如上面这个例子,我们在最后调用 search() ,将第二个参数设置为 false ,就表示本次分词内容,也就是 “敏捷” 这个词不用于后续的高亮操作,高亮缓存中的分词内容还是上一次的内容。因此,在下方调用高亮效果时,正好就只对标题中出现的 “与” 字进行了高亮操作了。

那么要删除之前的高亮缓存中的分词内容要怎么弄呢?直接用空字符串搜索一次就好啦。

代码语言:javascript
复制
$search->setLimit(1)->search('');
echo search-&gt;highlight(doc->title);
// 【敏捷1.4】敏捷开发环境:领导与团队
echo PHP_EOL;

折叠

折叠是啥意思?其实呀,它就是类似于数据库操作中的 GROUP 的效果。折叠搜索称为归并搜索,就像 Google 上通常搜索结果中对于某一个网站只会显示 2 条最匹配的结果, 其余的归并折叠起来。从而避免一个网站权重太大,连续多好页显示的都是同一个网站的内容。

在 XS 中,可以通过 XSSearch 的 setCollapse() 去指定根据某一个字段的值进行折叠归并。它的第一个参数是指定的字段名称,第二个参数是默认的数量值,也就是折叠归并,或者说分组后,这一组内有多少文档,这个数量值是通过返回结果中 XSDocument 对象的 ccount 属性来获得的。

说了半天,直接看例子吧,一看你就明白。

代码语言:javascript
复制
docs = search->setCollapse('category_name')->search('');
foreach (docs as doc)
{
echo '分类:'.doc-&gt;category_name.&#39; 下有 &#39; . (doc->ccount() + 1) . ' 条匹配结果。',PHP_EOL;
}
// 分类:PHP 下有 287 条匹配结果。
// 分类: 下有 1 条匹配结果。
// 分类: 下有 1 条匹配结果。
// 分类:随笔 下有 2 条匹配结果。
// 分类:项目产品 下有 48 条匹配结果。

看出效果了吧?咱们再用数据对应的 SQL 语句来试下。

代码语言:javascript
复制
select count(),category_name from zy_articles_xs_test group by category_name
-- 287 PHP
-- 2
-- 2 随笔
-- 49 项目产品

结果正好对应上了吧。不过这里有两个问题,一是分类为空的内容,在 XS 折叠时是分成两个空的数据统计出来的。二是官方文档是是用得 ccount() - 1 ,表示当前分类下除了显示出来的这篇文档还有多少篇。但我的测试是不需要减 1,本身就是排除当前这篇文档之外的文档数量,因此在我的结果(我统计的是该分类下总共的数量 )中还需要加 1 。

折叠搜索时,还可以组合其它搜索条件的,大家可以试一下,这里就不演示了。

对于这种聚合运算功能,还有一种就是后面要学习的分面搜索,其它就没有了。如果想要更复杂的聚合功能,不用考虑别的了,直接上 ES 吧。

典型搜索步骤

在 XS 中的搜索过程,其实也是可以分不同的步骤的,就好像 MySQL 中,我们可以直接不加任何语句的一行 SELECT ,也可以加 WHERE 、加 ORDER BY 、加 LIMIT 、加 GROUP BY 、加 HAVING 等等。这一系列步骤中,也有先后顺序,比如说 GROUP BY 的要求就比较多。

而在 XS 中,类似的过程也是有的:

  • 通过 setQuery() 设置搜过条件语句,语句内部也可以设定布尔规则
  • 添加附加条件,比如 setWeight() 设置排名权重、addRange() 搜索区间范围、setFuzzy() 开启模糊查找等
  • 搜索结果限定,通过 setLimit() 设置分页效果,setSort() 指定排序等

这些内容就是我们后面要继续深入学习的具体内容了。

总结

进入搜索部分的第一篇文章,内容还是比较简单的吧。我们设置查询条件、分页、查询数量、高亮、折叠这些功能方法的使用。也体会到了链式调用的好处与效果。最后,还说了一下典型的一个搜索步骤应该是什么样的。这也为我们直接引出了下一篇将要学习到的内容。

好了,话不多说,赶紧练一练,之后就准备进入到更深入的搜索技巧学习吧。

测试代码:

https://github.com/zhangyue0503/dev-blog/blob/master/xunsearch/source/11.php

参考文档:

http://www.xunsearch.com/doc/php/guide/search.overview

http://www.xunsearch.com/doc/php/guide/search.query

http://www.xunsearch.com/doc/php/guide/search.search

http://www.xunsearch.com/doc/php/guide/search.count

http://www.xunsearch.com/doc/php/api/XSSearch