第07章 改善用户搜索体验
本章内容
如何使用ElasticSearch Suggest API改正用户的拼写错误。
如何使用term suggester给出单词建议。
如何使用phrase suggester提示完整词组。
如何配置建议功能以匹配你的需求。
如何使用complete suggester的自动补全功能。
如何使用ElasticSearch的各种功能改进搜索相关性。
7.1 改正用户拼写错误
suggesters
在我们继续进行查询和分析响应结果之前,先简单交代一下可用的suggester类型。 ElasticSearch目前允许我们使用三种suggesters,即term suggester
, phrase suggester
和autocomplete suggester
。前两种suggester可以用来改正拼写错误,而第三种suggester用来开发出迅捷且自动化的补全功能。从ElasticSearch的0.90.3版本开始,我们可以使用基于前缀匹配的Suggester来便捷地实现自动补全功能。
7.2 改善查询相关性
首先我们将执行一个简单的查询并返回想要的结果,然后修 改这个查询,引人不同的ElasticSearch查询来使结果更好,接着我们会使用过滤器,并降低垃圾文档的得分,然后引入切面计算,用来提供下拉菜单让用户缩小查询结果范围。最后,我们还将探讨如何查看这些查询的改变以及衡量用户搜索体验的变化。
7.2.1 数据
7.2.2 改善相关性的探索之旅
1. 标准查询
ElasticSearch默认把文档内容存人all字段中。既然这样,我们为什么还 要为如何同时查询多个字段伤脑筋呢,仅仅使用一个all字段就可以了,对吗? 按照这个构建查询如下:
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"match:":{
"_all":{
"query":"australian system",
"operator":"OR"
}
}
}
}'
- 结果
{
//...
"hits" : {
"total" : 562264,
"max_score" : 3.3271418,
"hits" : [ {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "3706849",
"_score" : 3.3271418,
"fields" : {
"title" : "List of Australian Awards"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "26663528",
"_score" : 2.9786692,
"fields" : {
"title" : "Australian rating system"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "7239275",
"_score" : 2.9361649,
"fields" : {
"title" : "AANBUS"
}
}
//...
]
}
}
第二个文档比第一个文档更相关,不是吗?让我们来尝试改善一下。
2. 多匹配查询
在这里title字段比存储页面内容的text字段更重要。
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"multi_match":{
"query":"australian system",
"fields":[ "title^100", "text^10", "_all" ]
}
}
}'
- 结果
//...
"hits" : {
"total" : 562264,
"max_score" : 5.3996744,
"hits" : [ {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "7239222",
"_score" : 5.3996744,
"fields" : {
"title" : "Australian Antarctic Building System"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "26663528",
"_score" : 5.3996744,
"fields" : {
"title" : "Australian rating system"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "21697612",
"_score" : 5.3968987,
"fields" : {
"title" : "Australian Integrated Forecast System"
}
}
//...
]
}
第一个文档的标题是“Australian Antarctic Building System ",而 第二个文档的标题是“Australian rating system "。我希望第二个文档的得分更高。
3. 引入短语查询
接下来我们应该想到的是引入短语查询。短语查询可以解决刚才描述的问题。不过, 我们还是希望能在与短语匹配的查询结果后面列出那些没有包含完整短语的结果。
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"bool":{
"must":[
{
"multi_match":{
"query":"australian system",
"fields":[
"title^100",
"text^10",
"_all"
]
}
}
],
"should":[
{
"match_phrase":{
"title":"australian system"
}
},
{
"match_phrase":{
"text":"australian system"
}
}
]
}
}
}'
- 结果
{
//...
"hits" : {
"total" : 562264,
"max_score" : 3.5905828,
"hits" : [ {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "363039",
"_score" : 3.5905828,
"fields" : {
"title" : "Australian honours system"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "7239222",
"_score" : 1.7996382,
"fields" : {
"title" : "Australian Antarctic Building System"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "26663528",
"_score" : 1.7996382,
"fields" : {
"title" : "Australian rating system"
}
}
//...
]
}
}
4. 引入短语查询 - 加入slop
尽管结果有所改善,但和我们的期望还是有点距离,因为没有找到与短语完全匹配的 记录。我们可以考虑引人slop参数。slop参数用来设置单词间的最大间隔,而在最大间隔之内的单词可被认为是和查询中的短语相匹配的。例如,这里使用的“australian system"短语查询,如果设置slop为大于等于1的数,那么就可以和标题为“australian education system”的文档匹配上。
为了加入slop参数,将title、text提取出来作为key,内置query和slop对象:
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"bool":{
"must":[
{
"multi_match":{
"query":"australian system",
"fields":[
"title^100",
"text^10",
"_all"
]
}
}
],
"should":[
{
"match_phrase":{
"title":{
"query":"australian system",
"slop":1
}
}
},
{
"match_phrase":{
"text":{
"query":"australian system",
"slop":1
}
}
}
]
}
}
}'
- 结果
{
//...
"hits" : {
"total" : 562264,
"max_score" : 5.4896,
"hits" : [ {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "7853879",
"_score" : 5.4896,
"fields" : {
"title" : "Australian Honours System"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "363039",
"_score" : 5.4625454,
"fields" : {
"title" : "Australian honours system"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "11268858",
"_score" : 4.7900333,
"fields" : {
"title" : "Wikipedia:Articles for deletion/Australian university system"
}
}
//...
]
}
}
5. 扔掉垃圾信息
现在可以设法移除查询结果中的垃圾信息。因而需要移除重定向页面和特殊文档(如那 些被标记为已删除的文档)。我们在这里引入一个过滤器。过滤器的引入不会干扰其他文档 的排序(因为过滤器不具备评分功能)
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"bool":{
"must":[
{
"multi_match":{
"query":"australian system",
"fields":[
"title^100",
"text^10",
"_all"
]
}
}
],
"should":[
{
"match_phrase":{
"title":{
"query":"australian system",
"slop":1
}
}
},
{
"match_phrase":{
"text":{
"query":"australian system",
"slop":1
}
}
}
]
}
},
"filter":{
"bool":{
"must_not":[
{
"term":{
"redirect":"true"
}
},
{
"term":{
"special":"true"
}
}
]
}
}
}'
- 结果
疑问
结果最后一条“Australian soccer league system ”不符合短语匹配中的slop语法要求,原因?
6. 创建一个可纠正拼写错误的查询系统
回顾一下索引的映射配置,你会发现有一个title
字段被定义为multi_field
类型,而 其中的一个字段需要经过ngram分析器
的分词处理。默认情况下,ngram分析器会产生bigram
,例如,对于单词system,将生成sy ys st teem
等几个bigram。想象一下,我们可 以在查询时忽略其中的一些分词结果,从而使查询系统具备自动纠正拼写错误的能力。接下来用一个简单的查询来演示具体操作:
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"query_string":{
"query":"austrelia",
"default_field":"title",
"minimum_should_match":"100%"
}
}
}'
注意,上述查询故意写错austrelia
,且title字段为multi_field
类型:
其中,keyword_ngram分词定义入下:
- 结果
在这里我们针对title字段发送了一个带有错误拼写的查询命令。由于索引中没有与拼 错词项完全匹配的文档,因而什么也没得到。然后我们转而使用title.ngram
字段,并忽略一些bigram,这样ElasticSearch就可以找到一些匹配的文档了。修改后的查询命令如下:
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d '{
"query":{
"query_string":{
"query":"austrelia",
"default_field":"title.ngram",
"minimum_should_match":"85%"
}
}
}'
引入了minimum_should_match
,把它设置为85%,从而让 ElasticSearch知道,我们并不想要所有的bigram分词结果都匹配上,而是匹配其中一部分, 且具体哪些词项会匹配上我们也不关心。
- 结果
{
//...
"hits" : {
"total" : 67633,
"max_score" : 1.9720218,
"hits" : [ {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "11831449",
"_score" : 1.9720218,
"fields" : {
"title" : "Aurelia (Australia)"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "2568010",
"_score" : 1.8479118,
"fields" : {
"title" : "Australian Kestrel"
}
}, {
"_index" : "wikipedia",
"_type" : "page",
"_id" : "8952722",
"_score" : 1.778588,
"fields" : {
"title" : "Austrlia"
}
}
//...
]
}
}
7. 继续探讨切面计算
以下查询中,字段category.untouched
是不分词的,定义如下:
curl -XGET 'localhost:9200/wikipedia/_search?fields=title&pretty' -d
'{
"query":{
"match_all":{
}
},
"filter":{
"term":{
"category.untouched":"English-language films"
}
},
"facets":{
"category_facet":{
"terms":{
"field":"category.untouched",
"size":10
},
"facet_filter":{
"term":{
"category.untouched":"English-language films"
}
}
}
}
}'
注意一件事:除了标准过滤器,我们还为category_facet引入了一个facet_filter片段,用来缩小切面计算的范围。这样做是正确的。否则,标准过滤器仅仅缩小了搜索结果范围,却不会缩小切面计算的范围,而我们希望能够展示给用户下一级嵌套信息。