上回我们已经学习了一些简单的搜索功能,比如设置搜索语句、分页方法、数量查询以及高亮和折叠的查询效果。而今天,我们将更加深入地学习其它搜索相关的内容。最核心的,就是布尔查询,也就是类似于我们在数据库中的 AND 和 OR 之类的语法。不过在这之前,就像是 Explain 可以分析数据库的查询语句一样。XS 也为我们提供了一个可以查看分词结果以及查询条件的方法,我们得先来学会它的使用。
这个其实我们之前就见过,因为在使用 SDK 提供的 Quest.php 工具时,我们一直都会加一个 参数。对应到程序代码中,其实就是 方法。
虽然我们只使用 setQuery() 设置了一个 “数据结构与算法” 这样的搜索短语,但实际上,通过 getQuery() 可以看到,分词结果是 “数据结构”、“与”、“算法”,另外还有一个默认自带的 “数据结构” 的拆分同义词 “数据” 和 “结构” 。为啥说是同义词呢?之前我们已经学过同义词相关的设置了,这个单词 SYNONYM 相信大家也不会陌生了,就是同义词的意思。
在 XS 中,针对分词默认就是 AND ,没错,就和数据库中 WHERE 条件的 AND 是一个意思,表示并且。换句话说,查询出来的文档内容,必须同时包含 “数据结构”、“与”、“算法” 这些关键词。后面讲布尔查询的时候,会再更加详细的说这部分的内容。
“@数字” 这个表示的是词的位置,也就是分词的位置,比如 “数据结构” 后面跟着的是 1 ,“与” 后面跟着的是 2 。
我们也可以使用空格来进行人为的区分单词,这个大家在使用 Baidu 或者 Google 的时候应该会经常使用。
从面上也可以看到另外一个特点,那就是 getQuery() 方法也是一种快捷方法,可以直接像 search() 和 count() 一样给它一个查询语句。然后直接就会返回语句的分析结果,不用像上面一样使用 setQuery() ,这个可以非常方便我们日常的调试。
一个方法叫 setQuery() ,另一个方法叫 getQuery() ,是不是隐约感觉到什么了?没错,这两个方法其实也是对一个属性 query 的封装。
不多解释了,这个原理已经说到烂了。要是第一次看,或者不明白这是什么情况的小伙伴,可以移步 基础对象概览(一)索引项目与字段对象https://mp.weixin.qq.com/s/_xQbRKgk9wf3_DCWl8DAtg 好好复习一下哦。
好了,知道怎么看查询分析结果了之后,后面的学习就轻松一些了。不管怎么写,最终我们都会通过查询分析结果来看看到底是实现了怎样的一种查询效果。先从最基础的,也是最重要的布尔查询开始。
前面已经说过了,默认情况下,分词的结果是以 AND 进行所有的词项连接的。也就是说,默认的查询,是要包含分词后所有词语的内容才是我们要获得的结果。上面的例子中,返回的文档都是必须同时包含 “数据结构”、“算法” 及相关同义词的所有词项的文档。而如果我们想要类似于数据库中的 OR 语法,要怎么做呢?
看到查询结果了吧,AND 变成 OR 了,但是同义词里面的还是 AND 。XS 还为我们提供了一个 setFuzzy() 方法,作用就是 “模糊搜索” ,也即全部条件都变成 OR 。
后面我们再来看它们的查询效果。先来看一个问题,那就是这个连接词,OR ,能用小写吗?
仔细看这里的 or 被当成一个词项了,而所有的布尔连接也还是 AND ,也就是说,在 XS 的查询语法中,只支持大写的 AND 、OR ,以及后面我们要学习到的其它的布尔连接词。
好了,直接上效果,看看咱们的查询结果会有什么不同,在这里我直接以 count() 来返回结果数量,通过数量看一下不同的布尔结果返回的条数区别。如果你想查看具体的文档内容是不是真的符合返回的规范,那么直接把 count() 换成 search() 就行了。
还是比较清晰的吧。默认情况下,也就是全 AND 下返回 77 条数据;使用 OR 关键字,同义词里还是 AND ,返回 140 条数据;使用 setFuzzy() 全部变成 OR ,包括同义词里的条件,返回结果 300 条。
最顶上,我们先设置了一个 setFuzzy(false) 。这个是因为 XS 在查询时,类似 setQuery()、setFuzzy() 这些函数,都会直接发送到 Xapian 服务器,并且返回自身对象。这样的话,后面的查询如果不设置回来,就会一直沿用上一次设置的结果。后面我们还会看到这个问题的出现。总之,setFuzzy(false) 就相当于先取消一下之前设置的 setFuzzy() 效果(它的参数默认值是 true )。
异或语法
前面我们看到了 AND ,也看了 OR ,分别代表的就是 并且 和 或者 。但有的时候,你想要的效果是只要其中一个词,而另一个词不要出现,这就是异或的效果,使用 XOR 标识。
有意思吧,就是只能有“数据结构”或者只能有“算法”这两者之一的文档,这两个词不能同时出现在同一个文档中。
字段检索
接下来我们看看字段检索时的分词效果。关于什么是字段搜索这个我们在很早的时候就说过了,索引是 self 或者 both 类型的,就可以使用字段名进行字段指定的搜索。
可以看到字段搜索时,分词的前面有一个 B 字符,标识这是一个指定字段的分词内容。上面三行测试代码的内容其实没有什么特别之处,只是后面两段代码在使用空格隔开时,后面的词如果不继续加字段名的话,会变回混合区的检索词,这个是需要我们注意的。
如果是使用不同的字段检索,或者说字段检索放在混合区检索的后面,也就是说,第二部分使用字段检索的话,查询分析出来的结果会变成是使用 FILTER 来对关键词进行过滤了。很明显这里和前面相比是有明显的变化的,从字面理解上我们可以看到这应该是在前面检索结果的基础上再通过后面的关键词进行过滤从而获得结果的,但其实根据 Xapian 官方文档的解释,FILTER 操作符的作用是匹配两个子查询的文档,但权重仅取自左侧子查询(在其他方面,它类似于 AND )。也就是说,在上面的查询中,对于权重的计算只有 FILTER 左侧部分会参考,而右侧部分不会参与。关于权重与分数计算,后续内容和文章中会进行更深入的学习。现在我们只需要知道 FILTER 是干嘛的,字段值检索会出现这个操作符就可以了。
否定检索
这个就是取反啦,或者说是希望什么词不出现。
上面的例子中,前两个的语法是一个减号 - 加上词项。注意这里的减号和单词不要分开,查询的结果是包含设计模式但不包含工厂的文档。后面的查询则使用的是 NOT 操作符,注意,这里也是大写得,和上面的 AND、OR、XOR 一样。然后后面跟着的词项是需要有个空格的,这里和减号最大的不同就是这个空格。我推荐还是使用 NOT 写法可以更统一一些。
括号组合复杂查询
查询语句中的括号,也和数据库一样,能够让各种查询条件达到组合的效果。
这一长串下来是不是就有点复杂了。我们一段一段来看。
- 和 形成了一个大的 OR 或 关系,两边哪个成立都行
- 文档的 title 或 content ,也就是索引类型为 both 或 body 类型的字段,需要有“森林”或“迷宫”其一,并且 tags 字段还要有“数据结构”这个词项,这是左边的大条件
- 标题 title 字段要包含“二叉树”,但不能有“遍历”,这是右边部分的大条件
说实话,要是换成 SQL 语句,可能还不算复杂,但是因为搜索引擎有分词这件事,所以如果不用 getQuery() 来看的话,可能有的时候查询的结果会出乎你的意料。这些原理性方面的东西之前我们已经说过很多次了。
最后,再说一件小事,如果我们要查询括号 “()” 或者说是要查找 AND 这类的英文单词的话,要怎么办呢?这其实也是在最早我们就讲过的内容,标点符号会被过滤掉,而括号会起到分组的特殊作用。所以,为了避免注入问题,要做括号组合的话一定要自己拼接,而不是让前端直接传括号过来。而如果是要查询 AND 这类的单词,也直接通过程序代码转换成小写就好了。这个也是我们之前说过的,搜索引擎在建立倒排索引时,会将英文单词做统一的,保存词根和全小写的单词。
追加查询
追加查询的意思,就是在已经设置好 setQuery() 之后,再对搜索词进行追加的效果。比如下面这样使用 addQueryString() 之后的效果。
这里需要注意的是 addQueryString() 方法不是链式调用,无法链式追加。默认情况下,它就是直接通过 AND 将 setQuery() 和 addQueryString() 中的内容进行拼接。我们也可以指定内容连接方式。
这里使用的是 SDK 提供的常量,表示的就是 OR 的意思,可以看到查询分析结果就是 OR 形式的了。 接下来我们再来看看多个 addQueryString() 组合的效果。
注意看,后续的 addQueryString() 追加时,之前的内容会自动通过括号组合分组。这是我们需要注意的地方。这个 XS_CMD_QUERY_OP_AND_NOT 常量,其实就是 NOT 的意思,上面的否定检索的分析结果大家应该也注意到了。
这里还有一个问题,就是 addQueryString() 会针对当前这一次的 setQuery() 不断追加,即使我们现在查询完一次,然后继续再添加的话,那么还是会继续以之前的条件不停追加。比如下面这样。
可以看到最后的查询分析结果还是有之前的内容,这个要怎么办呢?这个其实和上面的 setFuzzy() 中我们讲到的问题是一样的,setFuzzy() 也是需要手动切换一下的,同理,清空之前的 addQueryString() 也需要重新 setQuery() 一下就行了。
不过大部分情况下我们并不需要太关心这个问题,因为 PHP 的特点本来就是运行一次就要全部重新加载一次。而我们平常写代码时,一般也会直接在一次查询中只走一个关键词,很少会有这样复杂的中间还夹杂其它操作的多次查询。因此,咱们只需要知道有这个问题就好了,日常可能真的不会太常见。
关于词项的问题,在 XS 索引管理(一)切换索引库与文档对象https://mp.weixin.qq.com/s/3foDWR-ZBzeANlweE1Gxxg 中我们就学习过,就是那个 addQueryTerm() 方法。这个方法和上面的 addQueryString() 非常像,但是 addQueryString() 是会分词的,而 addQueryTerm() 不会分词,就是将传递进来的参数当成一个 Term 词项来进行查询。这个具体的内容在之前我们就已经说过了,在这里就再看一下 getQuery() 分析的结果吧。
很明显,addQueryTerm() 就是一个完整的词,而且是字段型的一个词项结果,和上面的字段检索的结果一样,前面有个字母。
精确检索和范围检索之前我们都已经用过了,不过这里还是再来说一下。
精确查找
精确查找在我们讲索引配置时就学习过,当字段的 phrase 被设置为 true 时,表示该字段支持精确查找。默认情况下 title 和 body 类型也是打开的。只需要在检索时,给检索词加上引号就行了,这样,检索结果就必须按照检索词的顺序出现。比如说“数据结构与算法”,查询的文档中,必须是依次出现“数据结构”、“与”、“算法”这三个词。而如果出现的顺序是“算法”、“与”、“数据结构”,则该文档不算是当前查找的结果。
上面的例子中,第一个是普通搜索,第二个精确搜索,第三个是把前后顺序换了一下的精确搜索。可以看到查询出来的数量变化还是非常大的。后面两条是针对最后两个精确搜索的 getQuery() 分析。可以看到,在使用了精确搜索之后,查询语句中出现了 PHRASE 关键字。
精确搜索也可以运用于字段检索上。
范围查找
这里的范围查找,就是我们非常常用的大于小于某个区间的那种字段查找。一般来说,这种查找对 date 或 numberic 类型的字段效果更好,因为如果是英文或中文字符的话,会和排序一样使用字典序排序,就会出现“11”比“2”更小的问题了。关于这个问题下节课讲排序的时候我们会细说。
使用 addRange() 方法,第一个参数是字段名称,第二个参数是 from ,也就是从哪里开始,第三个参数是 to ,表示到哪里结束。两个参数分别代表大于等于和小于等于的概念。如果我们想要实现大于和小于的效果,需要手动在代码中加1或减1来实现。在查询分析中的结果,这种范围查找的表示是以中括号来标明的。
addRange() 方法也是叠加效果的,和上面的 addQueryString() 一样,如果要清除上次的条件,就需要重置 setQuery() 参数。如果在一个 addRange() 方法之后再追加了 addRange() ,也是通过 FILTER 关键字来连接多个不同的范围查询条件的。
我们还需要注意的是 addRange() 需要在 setQuery() 之后使用,上面第二条查询的例子中,查询分析结果就没有 id 的范围查询。这个地方是需要大家注意的,但是在之前的例子中,如果是直接使用快捷方法,如 getQuery()、search()、count() 之类的参数来传递查询词,则是可以正常使用的。
距离查询
这个距离查询不是地图那种 Geo 的距离哦,这里的距离查询指的是关键词之间的距离。比如说“数据结构与算法”,其中“数据结构”和“算法”中间隔了一个“与”,这就是表示它们之间的距离是 1 。
不过从实际测试来看,貌似没啥效果。它使用的是 NEAR 和 ADJ 这两个关键字。
嗯,直接变成小写成为分词了。不知道是不是我的用法有问题,希望有用过的小伙伴可以在评论区指点一下哦。
字段加权
字段加权指的是针对某个字段,添加权重索引词。如果指定的字段包含这个词,那么它的结果就会参与权重计算,使结果的相关度评分更高。
上面的例子中,第二行代码我们将 content 中包含“设计”的文档的权重提升,很明显查找出来的第一篇文档的 id 就不同了。内容中包含“设计”关键词的文档的优先级明显被提前了。
关于相关度评价,优先级和排序的内容我们下节课讲,现在你只需要知道通过 addWeight() 这个方法可以做到针对某一类数据提升搜索结果排序位置的效果就可以了。
分页搜索在英文中是 Facets Search ,表示从多个维度对检索数据进行属性聚合。这个功能其实是比较有意思的,就是一种多关键字分级聚合的功能。是 XS 中唯二的聚合查询效果,上一个聚合效果是我们之前学习过的折叠。
测试代码就这么简单,在搜索查询时,使用 setFacets() 指定一个数组,数组元素就是我们要聚合的字段名称。
然后在搜索查询结束之后,使用 getFacets() 获得指定字段的聚合结果数组。这个数组中,key 是聚合分组名称,值是数量 。
对于我写过的这些文章,包含“PHP”关键词的文档中,有282篇来自于 PHP 这个分类,5篇来自于随笔这个分类。下面的 tags 分级出来的结果就更多了,不过这里不精确。“数据结构,算法”这个明显就是有问题的,我的文章中打了这两个标签的内容可达不到 90 篇这么多。这里就要说分面搜索的另一个问题了。
分面搜索是按字段的值来实现的,支持分面的字段要能被索引,而且最好必须是 full ,也就是全值索引。在之前的索引设计中,category_name 我们设计的就是 full 类型,而 tags 则是以逗号分隔的分词索引。因此,针对 tags 的准确率明显就有问题了。
那么如果我们想统计某一天内发布的文章数量,使用 pub_time 字段可以吗?也不行,分面搜索是以 full 类型的字符串为主的,date 和 numberic 直接就会报错,大家可以试一下。
不过,如果我们硬要看的话,上面的 category_name 分面的结果也不准确。如果使用数据库查询的话。
这里可能有分词的影响,也有搜索引擎特殊的条件限制的影响,具体的原理我不清楚,这里只是提醒大家,不要将这个分面搜索的结果当做精确值,做一个参考值就可以。就像很多大数据应用一样,只是一种近似值的效果。
ES 在很老的版本中也有 Facets 功能,但在新版本中已经删除了,现在都是直接使用 Aggregations ,也就是聚合功能来实现分面功能。相对来说,ES 的聚合功能准确性就要高很多了。而且 ES 支持在聚合时分词,也就是 tags 字段如果在 ES 中,也可以按单独的分词结果再进行数量聚合,可以实现词云的效果。反正总的来说,这一块 XS 相比 ES 还是有差距,不过之前也说过了,不是一个量级的东西好不好,要按实际的业务需求来选择最适合自己的嘛。
这个功能一般用在哪里呢?其实大部分就是一个辅助功能,列出可能的分类,比如我们进入B站,随便搜索一个关键词。
看到这一排分类和后面的数字了吧,这就是分面搜索的典型应用场景。
今天我们学习的内容比较杂乱,但都是围绕着查询语句以及一些相关的功能来的。最核心的当然就是布尔搜索相关的语法了,学了这些之后,其实 XS 日常的使用就已经完全没有问题了。精确、范围、加权也都是锦上添花的功能,分面搜索非常方便地能根据指定字段分组但是还是有一些问题,最后就是距离检索没有测出来效果。这里也希望有了解这一方面的小伙伴可以评论留言一起学习哦。
下篇文章,我们将学习到的是排序,并且,划重点了哦,同时我们也将学习到现在搜索引擎最通用的评分算法相关的知识。对于理解搜索引擎的排序规则相当重要,都已经学习到这里了,那么下篇文章不看可是真的要吃亏的。
测试代码:
https://github.com/zhangyue0503/dev-blog/blob/master/xunsearch/source/12.php
参考文档:
http://www.xunsearch.com/doc/php/guide/search.query