学习完索引管理相关的内容之后,我们就进入到了搜索技巧相关的学习了。其实对应在 XS 中,就是 SDK 中的 XSSearch 对象的相关学习和使用。同样的,在这一部分,我们也会普及很多搜索相关的知识。
其实对于这个 XSSearch 对象,我们并不陌生,之前很多次,很多地方都已经用过了。只不过我们都是只用了它的最简单的一种使用方式。
它会返回一个由 XSDocument 对象组合成的数组,想必这部分内容也不用我多解释了。
其实,在这个 XSSearch 的 search() 方法上直接写搜索词,是 XS 为我们提供的一种快捷搜索方法。这个 search() 方法真正的作用是向查询服务端(端口8383)发送查询命令,并通过它继承的 XSServer 中的 respond 来获得返回的结果。关于 XSServer 部分的内容之前都已经学习过了。这里也就不多赘述了。
正式的设置查询语句、查询词的方法其实是 setQuery() 这个方法。
其实它的效果和 是一样的。那么如果我们同时使用 setQuery() ,并且 search() 中也有搜索词,而且两个词不一样会出现什么情况呢?
大家可以自己打印一下结果试试,我这里则是使用返回查询数量一个属性来测试的。
可以看到,最后返回的结果是 63 条,也就是说,最终查询词是以 search() 的参数为准的。大家可以运行最后一行的代码来查看返回的结果是不是 63 条。
这个地方通过源码分析的话,setQuery() 是直接将参数通过 这个命令常量发送到服务端了。这个查询参数会直接保存在服务端。而 search() 如果有参数,就会以 search() 的参数,通过 这个查询命令标识直接进行查询。比如我们再这样测试:
没有 setQuery() ,同时 search() 也没有参数,返回的结果是上一次 setQuery() 设置的查询内容,也就是 “敏捷” 相关的 37 条数据。
在这两段代码中,我们使用了一个 setLimit() 方法,它就是 XS 中的分页方法。接下来,我们就看一下这个分页的效果。
分页
默认情况下,我们不加 setLimit() 方法,那么最终的 search() 会默认返回从第 0 条数据开始的 10 条数据。也就是默认第一页的十条数据。这个和 MySQL 中的 limit 没啥太大区别,第一个参数是返回数量,第二个参数是 offset 偏移量。
没啥太多好解释的吧。但是,这里要多一嘴。包括 ES 在内的大部分搜索引擎对于深分页的支持都不怎么样。什么叫深分页?就比如每页显示 10 条数据,然后显示到第 1000 页、第 10000 页以后的内容。默认情况下,ES 的分页只支持 10000 条数据,也就是说,如果每页十条数据,在 ES 中,最多也就直接分 1000 页。当然也有别的方式可以继续向下翻页,但是却无法支持跳页了(直接指定页码)。
这一块的原因其实就是在于搜索引擎会对查询结果进行分析、打分、计算。所以在分页时往往会将数据全部拿回来进行这些计算操作。如果数据量太大,即使是 ES 也抗不住,毕竟它可以把数据分片存储,但是最后分页进行打分、排序时还是要把所有分片上的数据一起拿过来进行总体计算的。基于这样的原因,它就硬性规定了最多只能处理 10000 条数据。
虽说 XS 的文档上没写,但是基于对于大部分搜索引擎(包括百度和 Google )的理解,搜索引擎对于深分页的支持都不太友好。就像百度,最多也只是到 100 页左右,大家可以试试直接访问百度超过 100 页之后的内容是什么样子的。
但其实,即使只是中文网页,我相信关键词包含 “PHP” 的文章甚至是网站,远不止 700 多条搜索结果。也就是说,搜索引擎其实并不需要全面,而且有的时候也并不需要完全的精准,真正的搜索引擎,需要的是找到符合用户需要的内容。因此,千万不要以为百度、Google 养得成百上千的工程师是混饭吃的。真正商业化的全网搜索引擎的技术要求,可比我们学习的这些 ES、XS 之类的工具要复杂的多。
不过 XS 没提到过这个问题,那么咱们就来测试一下,就用默认的 demo 吧。
通过 PHP 代码向索引中添加十万条数据,然后通过 SDK 提供的查询工具,使用 参数来进行分页。可以看到最终的效果是能够顺利返回 20000 到 20010 条数据的列表。看来 XS 对这个深分页的支持还好,并且响应速度也还可以。当然,ES 有它自己的原因和道理,这里我也只是多嘴说一句,并不代表说 XS 比它强或者怎么样,只是通过测试证明,XS 是可以对超过 10000 条以上的数据进行深分页的。
快捷数量查询
数量查询,其实也就是类似于 MySQL 中的 count(*) 的效果。在 XS 中,我们已经在前面看到了 lastCount 属性的应用。它实际上就是返回最近一次查询结果的数量,这是个属性,因此对应的也有一个 getLastCount() 方法。但是这个属性没有 set 相关的方法,因此,这个变量属性是一个只读变量。是不是有体会到面向对象中封装的好处了?
除了这种返回最后一次查询结果数量的属性及对应的方法之外,就像上面的 search() 方法一样,XS 也为我们提供了一个快捷获得指定查询条件数量的方法,就叫 count() 。
不传任何参数时,count() 返回的结果也是上回查询关键词的结果数量。但是它也可以指定一个查询参数,比如第二行,但是大家会发现,第三行又变回之前的查询结果数量了。其实呀,这个查询对于查询参数的处理和 search() 是一样的,如果给了参数,按参数的来,如果没给参数,就按上一次 setQuery() 方法指定的查询条件来。
前面我们已经说过,setQuery() 是直接将查询参数传递到服务端了,而 search() 和 count() 的参数都是现拼的。如果它们有参数,就以最新的这个查询参数来执行。这个直接执行的查询参数在服务端是不会保留的,服务端只会保留通过 setQuery() 设置的,命令常量为 的数据。同样的,直接给 count() 的参数也是针对这一次请求的,和 search() 的效果一模一样。
另外还需要注意的一点是,这个 count() 方法返回的数量是一个估算值,不是精确值。同样地,lastCount 属性及对应方法返回的数量值也是估算值,不是精确值。这又是一个什么概念呢?
如果有做过大数据量的日志统计、流量统计或者类似统计系统,或者深分页达到100页以上的同学一定会知道。有的时候,为了性能,我们的汇总数据值是可以不精确的。比如说千万条日志中统计出来的实时日活数量,误差在一定范围内都是可以授受的。包括之前我们学习过的 Redis 中的 HyperLogLog 就明确说了不精确,有多少误差,但是速度飞快,存储空间小。同样的,对于大部分搜索结果及其分页来说,本身分词就是有着不确定性以及异步索引操作的问题,数量统计也会因此产生不准确的问题。
ES 中的 count 效果一般是通过聚合 aggs 实现的,相对来说要精确一些,但是它是以更多的计算和资源消耗量为代价换来的。但它也不是完全的精确值,特别是如果采用了多分片分布式的情况下,一样是有误差率的。但它可以通过一些参数设置来调节精确度。还是那句话,看业务需求进行取舍。
索引项目总数量
最后还有一个索引项目内的文档总数量的属性。
这个没啥多说,它也有一个对应的 getDbTotal() ,没有 set 相关方法,是一个只读属性。这个数量值是不是精确的文档没说,咱也不清楚。
通过前面的学习,其实大家已经发现了,XS 的 SDK 中的各种操作都是可以进行链式调用的。关于这种调用方式,之前在建造者模式、Laravel数据库相关的学习中我们都已经说过了。这里就简单的说一下 XS 中的应用。
在 XS 中,XSSearch 对象除了 search() 和 count() 之外,与查询有关的其它方法都是可以进行链式调用的。其实 XSIndex 也都可以,之前我们就看过,add()、update() 这些方法都是返回的 XSIndex 自身,所以完全可以这么写。
不过相对来说,在操作增、删、改时,不管是数据库内核,还是代码表现形式上,我们都会追求尽量的原子化,也就是一行一行的写。逻辑更清晰,也更容易看明白,同时也符合日常的认知。
而对于搜索来说,这样链式的写就完全不违和了。
这样写是不是要比下面这样的写法清晰很多。
大家在使用百度或者 Google 以及很多网站的搜索功能时,会发现在返回的结果中会把我们搜索的关键字标红。这个功能在 XS 中的实现非常简单。其实自己去实现也不复杂,简单来说原理就是将分词后的查询关键字,一一替换加上一个特殊的 HTML 标签。然后在前端通过给这个标签设置特定的 CSS 样式实现变红、加粗、改字体等等功能就可以了。
在 XS 中,直接使用 XSSearch 提供的 highlight() 方法就可以了。
看出来效果了吧,“数据结构与算法” 通过默认分词实际上是分成了 “数据结构”、“数据”、“结构”、“与”、“算法” 这几个词。然后调用 highlight ,将一个字符串(通常就是我们的 title 和 body 指定的字段)传递给它,它就会根据分词的结果为指定字符添加上 标签。然后在前台展示时,我们就可以通过 CSS 来控制标签展示样式了。大家可以自己去看下源码,替换过程真的不复杂,只是获取分词和组合替换规则的部分要麻烦一点,原理还是我上面说过那个原理。甚至最终使用的函数就是 PHP 原生的 preg_replace()、strtr()、str_replace() 这三个之一。
它还有第二个参数,是一个布尔值,表示是否使用 strtr() 函数来进行替换处理。
能看出来不同在哪里了吧,上面的 “数据结构” 套了两层 ,还有一层是分开 “数据” 和 “结构” 的。而下面的只有一层完整的长词 “数据结构” 。关于 strtr() 和 str_replace() 的区别以及使用方法,大家可以自己查阅相关的资料。
另外,search() 方法的第二个参数,是表示是否保存本次分词结果到高亮变量中用于后续的高亮操作的。
比如上面这个例子,我们在最后调用 search() ,将第二个参数设置为 false ,就表示本次分词内容,也就是 “敏捷” 这个词不用于后续的高亮操作,高亮缓存中的分词内容还是上一次的内容。因此,在下方调用高亮效果时,正好就只对标题中出现的 “与” 字进行了高亮操作了。
那么要删除之前的高亮缓存中的分词内容要怎么弄呢?直接用空字符串搜索一次就好啦。
折叠
折叠是啥意思?其实呀,它就是类似于数据库操作中的 GROUP 的效果。折叠搜索称为归并搜索,就像 Google 上通常搜索结果中对于某一个网站只会显示 2 条最匹配的结果, 其余的归并折叠起来。从而避免一个网站权重太大,连续多好页显示的都是同一个网站的内容。
在 XS 中,可以通过 XSSearch 的 setCollapse() 去指定根据某一个字段的值进行折叠归并。它的第一个参数是指定的字段名称,第二个参数是默认的数量值,也就是折叠归并,或者说分组后,这一组内有多少文档,这个数量值是通过返回结果中 XSDocument 对象的 ccount 属性来获得的。
说了半天,直接看例子吧,一看你就明白。
看出效果了吧?咱们再用数据对应的 SQL 语句来试下。
结果正好对应上了吧。不过这里有两个问题,一是分类为空的内容,在 XS 折叠时是分成两个空的数据统计出来的。二是官方文档是是用得 ,表示当前分类下除了显示出来的这篇文档还有多少篇。但我的测试是不需要减 1,本身就是排除当前这篇文档之外的文档数量,因此在我的结果(我统计的是该分类下总共的数量 )中还需要加 1 。
折叠搜索时,还可以组合其它搜索条件的,大家可以试一下,这里就不演示了。
对于这种聚合运算功能,还有一种就是后面要学习的分面搜索,其它就没有了。如果想要更复杂的聚合功能,不用考虑别的了,直接上 ES 吧。
在 XS 中的搜索过程,其实也是可以分不同的步骤的,就好像 MySQL 中,我们可以直接不加任何语句的一行 SELECT ,也可以加 WHERE 、加 ORDER BY 、加 LIMIT 、加 GROUP BY 、加 HAVING 等等。这一系列步骤中,也有先后顺序,比如说 GROUP BY 的要求就比较多。
而在 XS 中,类似的过程也是有的:
这些内容就是我们后面要继续深入学习的具体内容了。
进入搜索部分的第一篇文章,内容还是比较简单的吧。我们设置查询条件、分页、查询数量、高亮、折叠这些功能方法的使用。也体会到了链式调用的好处与效果。最后,还说了一下典型的一个搜索步骤应该是什么样的。这也为我们直接引出了下一篇将要学习到的内容。
好了,话不多说,赶紧练一练,之后就准备进入到更深入的搜索技巧学习吧。
测试代码:
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