Elasticsearch:将精确搜索与词干混合

641 阅读4分钟

在构建搜索应用程序时,词干提取通常是必须的,因为希望 skiing 查询匹配包含 ski 或 skis 的文档。 但是如果用户想专门搜索 skiing 怎么办? 执行此操作的典型方法是使用 multi-field,以便以两种不同的方式索引相同的内容:



1.  PUT my_index
2.  {
3.    "settings": {
4.      "analysis": {
5.        "analyzer": {
6.          "english_exact": {
7.            "tokenizer": "standard",
8.            "filter": [
9.              "lowercase"
10.            ]
11.          }
12.        }
13.      }
14.    },
15.    "mappings": {
16.      "properties": {
17.        "body": {
18.          "type": "text",
19.          "analyzer": "english",
20.          "fields": {
21.            "exact": {
22.              "type": "text",
23.              "analyzer": "english_exact"
24.            }
25.          }
26.        }
27.      }
28.    }
29.  }


在上面,我们通过 multi-filed 来实现对一个字段进行多种分词。如果大家对 englishstandard 分词器不是很熟的话,那么我们可以使用如下的方法来做测试:



1.  GET _analyze
2.  {
3.    "analyzer": "standard",
4.    "text": "Skiing and Skis"
5.  }


在上面, 我们使用 standard 分词器。它的结果为:



1.  {
2.    "tokens" : [
3.      {
4.        "token" : "skiing",
5.        "start_offset" : 0,
6.        "end_offset" : 6,
7.        "type" : "<ALPHANUM>",
8.        "position" : 0
9.      },
10.      {
11.        "token" : "and",
12.        "start_offset" : 7,
13.        "end_offset" : 10,
14.        "type" : "<ALPHANUM>",
15.        "position" : 1
16.      },
17.      {
18.        "token" : "skis",
19.        "start_offset" : 11,
20.        "end_offset" : 15,
21.        "type" : "<ALPHANUM>",
22.        "position" : 2
23.      }
24.    ]
25.  }


也就是说,它不提取词的词干(stem)。这对于 exact 匹配是非常适合的,比如我们想搜索 skiing,那么通过 standard 分词器,我们肯定是可以搜索到的。在下面,我们使用 english 分词器:



1.  GET _analyze
2.  {
3.    "analyzer": "english",
4.    "text": "Skiing and Skis"
5.  }


上面的结果显示:



1.  {
2.    "tokens" : [
3.      {
4.        "token" : "ski",
5.        "start_offset" : 0,
6.        "end_offset" : 6,
7.        "type" : "<ALPHANUM>",
8.        "position" : 0
9.      },
10.      {
11.        "token" : "ski",
12.        "start_offset" : 11,
13.        "end_offset" : 15,
14.        "type" : "<ALPHANUM>",
15.        "position" : 2
16.      }
17.    ]
18.  }


也是就是无论是 skiing 或者是 kiis,它们被分词后的结果都是 ski,也就是它们的词干(stem)。这种对于我们的很多情形的搜索是非常有用的。

基于上面创建的索引 my_index,我们写入如下的文档:



1.  PUT my_index/_doc/1
2.  {
3.    "body": "Ski resort"
4.  }

6.  PUT my_index/_doc/2
7.  {
8.    "body": "A pair of skis"
9.  }

11.  POST my_index/_refresh


我们接着进行如下的搜索:



1.  GET my_index/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "simple_query_string": {
5.        "fields": [ "body" ],
6.        "query": "ski"
7.      }
8.    }
9.  }


由于 body 是采用 english 的分词器,那么很自然两个文档都可以被搜索到:



1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "my_index",
6.          "_id" : "1",
7.          "_score" : 0.18232156,
8.          "_source" : {
9.            "body" : "Ski resort"
10.          }
11.        },
12.        {
13.          "_index" : "my_index",
14.          "_id" : "2",
15.          "_score" : 0.18232156,
16.          "_source" : {
17.            "body" : "A pair of skis"
18.          }
19.        }
20.      ]
21.    }
22.  }


另一方面,在 body.exact 上搜索 ski 只会返回文档 1,因为 body.exact 的分词器不执行词干提取。



1.  GET my_index/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "simple_query_string": {
5.        "fields": [ "body.exact" ],
6.        "query": "ski"
7.      }
8.    }
9.  }


上面搜素返回的结果为:



1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "my_index",
6.          "_id" : "1",
7.          "_score" : 0.8025915,
8.          "_source" : {
9.            "body" : "Ski resort"
10.          }
11.        }
12.      ]
13.    }
14.  }


这不是一件容易暴露给最终用户的事情,因为我们需要有一种方法来确定他们是否正在寻找精确匹配并相应地重定向到适当的字段。 另外,如果只有部分查询需要完全匹配,而其他部分仍应考虑词干,该怎么办?

幸运的是,query_string 和 simple_query_string 查询有一个特性可以解决这个问题:quote_field_suffix。 这告诉 Elasticsearch 出现在引号之间的单词将被重定向到不同的字段,见下文:



1.  GET my_index/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "simple_query_string": {
5.        "fields": [ "body" ],
6.        "quote_field_suffix": ".exact",
7.        "query": "\"ski\""
8.      }
9.    }
10.  }




1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "my_index",
6.          "_id" : "1",
7.          "_score" : 0.8025915,
8.          "_source" : {
9.            "body" : "Ski resort"
10.          }
11.        }
12.      ]
13.    }
14.  }


在上面的例子中,由于 ski 在引号之间,由于quote_field_suffix 参数,它在 body.exact 字段上被搜索,所以只有文档 1 匹配。 这允许用户根据自己的喜好混合精确搜索和词干搜索。

我们也可以尝试如下的搜索:



1.  GET my_index/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "simple_query_string": {
5.        "fields": [ "body" ],
6.        "quote_field_suffix": ".exact",
7.        "query": "\"ski\" pair"
8.      }
9.    }
10.  }


上面返回的结果为:



1.  {
2.    "hits" : {
3.      "hits" : [
4.        {
5.          "_index" : "my_index",
6.          "_id" : "1",
7.          "_score" : 0.8025915,
8.          "_source" : {
9.            "body" : "Ski resort"
10.          }
11.        },
12.        {
13.          "_index" : "my_index",
14.          "_id" : "2",
15.          "_score" : 0.6931471,
16.          "_source" : {
17.            "body" : "A pair of skis"
18.          }
19.        }
20.      ]
21.    }
22.  }


由于 ski 含有引号,那么它在body.exact字段上被搜索,但是由于 pair 没有含有引号,它在 body 字段上进行搜索。

**注意:**如果在 quote_field_suffix 中传递的字段选择不存在,则搜索将回退到使用查询字符串的默认字段。