文本分析的基础知识

文本数据的分析不同于其它类型的数据分析,比如数字、日期和时间。这一类的分析是一种确定的方式进行。比如,查找某一天的记录,那么就会把符合条件的记录搜索出来。而文本/字符串的分析可能有所不同,根据不同的性质,可用于结构化或非结构化分析。

字符串字段的结构化类型的一些例子如:国家代码、产品代码、非数字序列号等。这些字段的数据类型可能是字符串,都是单一的一种类型。

我们这里首先介绍一下对非结构化文本的分析,也称为全文搜索。所有的文本类型的字段都是由所谓的分析器进行分析,这里我们分为三个方面:

  • Elasticsearch 分析器的介绍
  • 内置分析器
  • 自定义分析器

1. Elasticsearch 分析器的介绍

分析器的主要作用是获取字段的值并将其分解为多个项。也就是说获取文档的中的每个字段,并从中提取术语。

有两种情况下会发生:分析器将输入字符流分解为术语:

  • 在索引时
  • 在搜索的时候

分析器的核心任务是解析文档字段并构建索引

在索引文档之前,需要分析文本类型的每个字段。这个分析过程使文档可以根据搜索时使用的任何术语进行搜索。

可以根据每个字段配置分析程序,也就是说,在同一个文档中有两个文本类型的字段,每个字段可以使用不同的分析程序。

Elasticsearch使用分析程序来分析文本数据。分析器有以下组成部分:

  • Character filters:零或多个
  • Tokenizer:只能一个
  • Token filters:零或多个

上图描述了分析器的组成部分

1.1 character filters

字符过滤器的作用于输入字段中的字符流,每个字符过滤器可以添加、删除或更改输入字段中的字符。可以配置零个或多个字符过滤器。

ES 内置一些字符过滤器,您可以使用这些过滤器组合或创建自己的自定义分析器。

例如:ES 内置的字符过滤器之一:映射字符过滤器。它可以将一个字符或字符序列映射为目标字符。

例如:把表情符号转为文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PUT my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "standard",
"char_filter": [
"my_char_filter"
]
}
},
"char_filter": {
"my_char_filter": {
"type": "mapping",
"mappings": [
":) => _happy_",
":( => _sad_"
]
}
}
}
}
}
1
2
3
4
5
POST my_index/_analyze
{
"analyzer": "my_analyzer",
"text": "I'm delighted about it :("
}

//转换后的结果

1
[ I'm, delighted, about, it, _sad_ ]

因为字符过滤器位于分析器处理链的最开始,所以 Tokenizer 看到的将是替换后的字符。

内置的字符过滤器列表:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html

1.2 Tokenizer

分析器只有一个 tokenizer。tokenizer 的职责是接收字符流并生成tokens流。这些令牌用于构建反向索引。一个token大致相当于一个单词。除了将字符分解为单词或标记外,它还在输出中生成输入流中每个标记的开始和结束偏移量。

Elasticsearch附带许多 tokenizer(分词器),可用于组成自定义分析器;Elasticsearch本身也使用这些分词器来组成其内置的分析程序。

内置的分词器列表:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html

Standard Tokenizer 是比较常用的分词器之一,因为它适用于大多数语言。简单的说,它是通过空格或标点符号来分解字符流。

1
2
3
4
5
POST _analyze
{
"tokenizer": "standard",
"text": "This is Tokenizer Test"
}

上面的命令产生以下输出;注意输出中的start_offset、end_offset和位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"tokens" : [
{
"token" : "This",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "is",
"start_offset" : 5,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "Tokenizer",
"start_offset" : 8,
"end_offset" : 17,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "Test",
"start_offset" : 18,
"end_offset" : 22,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}

这个Token流可以由分析器的Token filters进一步处理。

1.3 Token filters

分析器中可以有零个或多个Token 过滤器。每个Token过滤器都可以在其接收的输入token流中添加、删除或更改令牌。由于在一个分析器中可能有多个令牌过滤器,因此每个令牌过滤器的输出被发送到下一个令牌过滤器,直到所有令牌过滤器走完为止。

Elasticsearch附带了许多令牌过滤器,它们可以用来组合您自己的自定义分析程序。

比如:

  • Lowercase token filter:用小写版本替换输入中的所有 tokens。
  • Stop token filter:删除停止字,即不为上下文添加更多含义的字。例如,在英语句子中,像is、a、an和the这样的单词不会给句子增添额外的意思。对于许多文本搜索问题,删除这样的单词是有好处的,因为它们不会为内容添加任何额外的含义或上下文。

内置 Token filters 列表:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html

2. 使用内置分析器

Elasticsearch内置了许多可以直接使用的分析程序。几乎所有这些分析程序都不需要额外的配置就可以使用,并且提供了一些额外的参数增加灵活性。

常见的分析器有:

  • Standard analyzer:这是ES的默认分析器。如果没有被任何其他字段级、类型级或索引级分析器覆盖,则使用该分析器分析所有字段。

  • Language analyzers:不同的语言有不同的语法规则。在如何将字符流标记为单词或标记方面,一些语言之间存在差异。此外,每种语言都有自己的一组stopwords,可以在配置语言分析器时对其进行配置。

  • Whitespace Analyzer:每当遇到空白字符时,空白分析器就将文本划分为术语。

内置的分析器参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html

2.1 标准分析器

参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html

标准分析器适用于多种语言和场合。它还可以针对底层语言或场景进行定制。标准分析仪由下列部件组成:

(1)Tokenizer

Standard tokenizer:一种用空格字符分割 tokens 的 tokenizer

(2)Token filters
  • Lowercase token filter:使输入中的所有 tokens 都小写。

  • Stop token filter:删除指定的stopwords。默认设置的stopword列表设置为_none_,默认情况下它不会删除任何stopword。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUT index_standard_analyzer
{
"settings": {
"analysis": {
"analyzer": {
"anasta":{
"type": "standard"
}
}
}
},
"mappings": {
"properties": {
"field1": {
"type": "text",
"analyzer": "anasta"
}
}
}
}

在这里,我们创建了索引index_standard_analyzer

  • 在settings中,我们显式定义了一个名为anasta的分析器。它引用标准分析器
  • 在索引中创建了一种名为_doc(默认)的类型,并在惟一的字段field1上显式地设置了字段级别分析器

//测试

1
2
3
4
5
POST index_standard_analyzer/_analyze
{
"field": "field1",
"text": "This is Standard analyzer."
}

//结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"tokens" : [
{
"token" : "this",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "is",
"start_offset" : 5,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "standard",
"start_offset" : 8,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "analyzer",
"start_offset" : 17,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}

这里,它使用的是我们定义的字段级别的分析器,即使不指定,默认也是标准分析器。

如您所见,输出中的所有字符都是小写的。因为 “stopwords” 默认是 _none_

下面我们使用 English stopwords 测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PUT stand_english
{
"settings": {
"analysis": {
"analyzer": {
"std_english": {
"type": "standard",
"stopwords": "_english_"
}
}
}
},
"mappings": {
"properties": {
"my_text": {
"type": "text",
"analyzer": "std_english"
}
}
}
}

新索引使用了"stopwords": "_english_",停用符里面可以也可以写成:stopwords:(a, an, the)_english_ 默认已经包含。

1
2
3
4
5
POST stand_english/_analyze
{
"field": "my_text",
"text": "The old brown cow"
}

//输出如下

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

从上面可以看出,The 已经被删除。

3. 使用自定义分析器实现自动完成

参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html

在有些情况下,您可能想通过组合您所选择的字符过滤器、分词器和分词过滤器来创建自己的自定义分析器。请记住,大多数需求都可以通过内置的分析器来满足。

1
2
3
4
5
GET /_analyze
{
"text": "This is Standard Analyze",
"analyzer": "standard"
}

从上面的输出中,应该可以看出标准分析器是怎样分词的,但有的时候,我们想:输入 St ,它仍然应该匹配 This is Standard Analyze 。让我们组成一个分析器,它可以生成术语,如St, Stan, stand,等等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PUT /custom_analyzer_index
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"custom_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"custom_edge_ngram"
]
}
},
"filter": {
"custom_edge_ngram": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 6
}
}
}
}
},
"mappings": {
"properties": {
"product": {
"type": "text",
"analyzer": "custom_analyzer",
"search_analyzer": "standard"
}
}
}
}

edge_ngram分词过滤器将每个token分解为2个字符、3个字符和4个字符的长度,最多6个字符。

//测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /custom_analyzer_index/_doc
{
"product": "This is Standard Analyze"
}

POST /custom_analyzer_index/_doc
{
"product": "Test study Standard"
}

GET /custom_analyzer_index/_search
{
"query": {
"match": {
"product": "Standa"
}
}
}

导入示例数据

数据使用的一个电子商务网站的测试数据。

下载地址:http://dbs.uni-leipzig.de/file/Amazon-GoogleProducts.zip (可选)

//创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PUT /amazon
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": {
"analyzer": {}
}
},
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"title": {
"type": "text"
},
"description": {
"type": "text"
},
"manufacturer": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
  • id:使用 keyword 类型,
  • title/description:使用 text 类型,执行文本分析,这些字段启用全文查询。
  • manufacturer:类型是 text , 又有一个 keyword ,所以它以两种方式存储,一种是文本,另一种是manufacturer.raw 存储为关键字。关键字类型的所有字段都在内部使用keyword分析器。关键字分析器仅由keyword tokenizer组成,它是一个noop 分析器,简单地将整个输入作为一个token返回。记住,在分析器中,字符过滤器和 token 过滤器是可选的。因此,通过在字段上使用关键字类型,我们选择了一个noop分析器,从而跳过了该字段上的整个分析过程。
  • price:类型为 scaled_float

//导入数据

1
2
3
$ git clone https://github.com/aishangwei/elk.git
$ cp elk/elk-study/example1/* ./
$ logstash -f logstash_amazon.conf

//验证

1
2
3
4
5
6
GET /amazon/_search
{
"query": {
"match_all": {}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1363,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
...

结构化搜索

在某些情况下,我们可能想指定是否包含给定的文档,这是一个简单的二元答案。另一方面,还有其它类型的查询是基于相关性的。这种查询还会对每个文档返回一个评分,以判断该文档是否适合该查询。大多数结构化查询不需要基于相关性的评分,因为是要么不包含,要么包含。这些结构化搜索查询也称为术语查询。

上图的查询是一种简单的基础查询。

1. Range query

范围查询可以应用于具有自然排序的数据类型的字段。比如,整数和日期都有一个自然排序。在查询一个值时,使用 大于,小于,等,没有歧义。这种定义良好的数据类型排序,可以应用范围查询。

  • 数字类型的范围查询
  • 带分数的范围查询
  • 日期范围查询

(1)数字类型的查询

比如我们想得到一个范围价格内的产品。

1
2
3
4
5
6
7
8
9
10
11
GET /amazon/_search
{
"query": {
"range": {
"price": {
"gte": 10,
"lte": 15
}
}
}
}

请注意以下事项:

  • hits.total.value:匹配的结果总数
  • hits.max_score:显示查询的最佳匹配文档的得分。由于范围查询是结构化查询,没有任何重要性或相关性,所以它作为过滤器执行。它不能得分。所有文档的得分都是1。
  • hits.hits:数组列出了所有实际结果。默认情况下,Elasticsearch不会一次返回全部匹配的结果。它只返回前10条记录。
  • price:在所有搜索中请求的范围,即10:<= price <= 15。

(2)带分数的范围查询

默认情况下,范围查询为每个匹配的文档分配1分。如果您将范围查询与其他查询结合使用,并且希望在结果文档满足某些条件的情况下为其分配更高的分数,该怎么办?我们将研究复合查询,比如bool查询,您可以在其中组合多种类型的查询。范围查询允许您提供一个boost参数,以提高其相对于其他查询的得分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /amazon_products/_search
{
"from": 0,
"size": 3,
"query": {
"range": {
"price": {
"gte": 10,
"lte": 20,
"boost": 2.8
}
}
}
}

在这个查询中,所有通过筛选的文档的得分都将是2.8,而不是1。

(3)日期范围查询(该实验可后续收集完日志测试)

范围查询也可以应用于日期字段,因为日期本身也是有序的。您可以在查询日期范围时指定日期格式:

1
2
GET /orders/_search
{"query":{"range":{"orderDate":{"gte":"01/09/2017","lte":"30/09/2017","format":"dd/MM/yyyy"}}}}

Elasticsearch 还允许我们以其它方式的格式进行日期范围查询,比如最近 7 天的数据,精度为毫秒。

1
2
GET /orders/_search
{"query":{"range":{"orderDate":{"gte":"now-7d","lte":"now"}}}}

另外的格式:

1
2
3
4
5
6
7
- 1y     //年
- 1M //月
- 1w //周
- 1d //天
- 1h //小时
- 1m //分钟
- 1s //秒

范围查询默认情况下在过滤器上下文中运行。它不计算任何分数,对于所有匹配的文档,分数总是设置为1。

2. Exists query

有时,只获取某个字段中具有非空值的记录是有用的。比如,获取所有定义了描述字段的产品。

1
2
3
4
5
6
7
8
GET /amazon/_search
{
"query": {
"exists": {
"field": "description"
}
}
}

exists查询将转换为过滤器;换句话说,它在过滤器上下文中运行。这类似于range查询,其中得分无关紧要。

3. Term query

有的时候我们要找到特定制造商生产的所有产品,我们前面已经知道,manufacturer 的字段是字符类型的,这是一个全文查询。而我们想要精确的搜索,这时就需要使用术语查询了。

1
2
3
4
5
6
7
8
GET /amazon/_search
{
"query": {
"term": {
"manufacturer.raw": "knowledge adventure"
}
}
}

术语查询是一种低级查询,因为它不对术语进行任何分析。此外,它直接运行在从上述字段构造的反向索引上;默认情况下,术语查询在查询上下文中运行,因此计算分数,对 manufacturer.raw 字段是不利的。

返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 12,
"relation" : "eq"
},
"max_score" : 4.692448,
"hits" : [
{
"_index" : "amazon",
"_type" : "_doc",
"_id" : "71dniGwBKl1dQYvhGUiW",
"_score" : 4.692448,
"_source" : {
...

从上面看出,每个文档都进行了评分。要想提高速度不进行评分,就需要在过滤器上下文中运行,将其封装在 constant_score过滤器中

1
2
3
4
5
6
7
8
9
10
11
12
GET /amazon/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"manufacturer.raw": "knowledge adventure"
}
}
}
}
}

该查询现在将返回所有匹配文档,它的分数都是固定的1分。您可以把它想象为将计分查询转换为非计分查询

全文搜索

全文查询可以在非结构化文本字段上工作。这些查询知道分析过程。在执行实际搜索操作之前,全文查询对搜索项应用分析器。它首先检查是否定义了字段级search_analyzer,然后检查是否定义了索引级分析器,从而确定要应用的正确分析器。如果没有定义字段级别的分析程序,它将尝试使用在索引级别定义的分析器。

因此,全文查询能够感知基础字段上的分析过程,并在形成实际搜索查询之前应用正确的分析过程。这些支持分析的查询也称为高级查询。看下图

1. Match query

匹配查询是大多数全文搜索需求的默认查询。

例如,当您在关键字字段上使用匹配查询时,它知道底层字段是关键字字段,因此在查询时不分析搜索项:

1
2
3
4
5
6
7
8
GET /amazon/_search
{
"query": {
"match": {
"manufacturer.raw": "global software publishing"
}
}
}

从输入结果中,应该可以看出,它没有将搜索词global software publishing 分析为独立的术语进行查询,这是因为我们使用的是关键字字段 manufacturer.raw 。事实上,在这个特殊的例子中,match查询被转换成一个term查询,如下所示:

1
2
3
4
5
6
7
8
GET /amazon/_search
{
"query": {
"term": {
"manufacturer.raw": "global software publishing"
}
}
}

术语查询返回与本例中的匹配查询相同的分数,因为它们都是针对关键字字段执行的。

让我们看看如果对文本字段执行匹配查询会发生什么,这是全文查询的实际用例:

1
2
3
4
5
6
7
8
GET /amazon/_search
{
"query": {
"match": {
"manufacturer": "global software publishing"
}
}
}

当我们执行 match 查询时,它执行的过程和上图一样的。

  • 在所有文档中的 manufacturer 字段内进行搜索术语 global、software、publishing 。
  • 按照分数降序进行匹配显示

有的时候,我们想指定一些特定的选项,来达到我们的需求,一些常见的选项分类如下:

  • 操作符
  • 最少匹配
  • 模糊值(容差)

(1)操作符

查询时,分析器会把我们的查询值分解为单个术语,默认情况下,match 查询对单个术语的组合操作符是 or(或),也就是说,只要文档的字段中包含其中一个单个术语就符合查询。

在有的时候,我们可以改变这种方式:

1
2
3
4
5
6
7
8
9
10
11
GET /amazon/_search
{
"query": {
"match": {
"manufacturer": {
"query": "global software publishing",
"operator": "and"
}
}
}
}

(2)最少匹配

我们也可以使用 or(默认) 操作符配置其它选项,实现更加细粒度的控制。

1
2
3
4
5
6
7
8
9
10
11
GET /amazon/_search
{
"query": {
"match": {
"manufacturer": {
"query": "global software publishing",
"minimum_should_match": 2
}
}
}
}

上面示例中,三个术语最低匹配两个。

(3)Fuzziness(模糊度/容差)

这种模糊度查询,允许我们对搜索的值,进行有限的拼写错误。模糊参数的取值可以是:0(精确匹配)、1、2 或 AUTO(根据原始项自动确定)

AUTO:

0-2个字符:必须全部匹配

3-5个字符:只允许一个字符模糊

>5 个字符:只允许两个字符模糊

1
2
3
4
5
6
7
8
9
10
11
GET /amazon/_search
{
"query": {
"match": {
"manufacturer.raw": {
"query": "globel software publishing",
"fuzziness": 1
}
}
}
}

我们把 global 写成 globel ,也可以搜索到我们需要的东西 。

模糊性是有代价的,因为Elasticsearch必须生成额外的术语来匹配。

参考链接:https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness

2. 匹配短语查询

当您希望匹配一个短语,而不是单独的术语时,match_phrase 查询比较有用。

例如:我们想要搜索一个描述的短语:

1
with windows xp professional edition

这个时候就不能使用 match 查询了,因为它是无序的。

1
2
3
4
5
6
7
8
9
10
GET /amazon/_search
{
"query": {
"match_phrase": {
"description": {
"query": "with windows xp professional edition"
}
}
}
}

match_phrase查询还支持slop参数,该参数允许您指定一个整数:0、1、2、3,等等。slop放宽了查询时可以跳过的单词/术语的数量。

1
2
3
4
5
6
7
8
9
10
11
GET /amazon/_search
{
"query": {
"match_phrase": {
"description": {
"query": "with windows professional ",
"slop": 1
}
}
}
}

slop值为1时,用户可以使用 with windows professional 进行查询,仍然能够返回包含with windows xp professional edition 的结果。slop 的默认值为0

3. Multi match query(多匹配查询)

multi_match查询是match查询的扩展。multi_match查询允许我们跨多个字段运行匹配查询,还允许使用许多选项来计算文档的总体得分。

multi_match查询可以与不同的选项一起使用。我们看一下以下选项:

(1)使用默认值查询多个字段

当在 web 上进行搜索产品时,我们系统同时搜索 title 和 description 字段。这就可以使用 multi_match 查询来完成。

1
2
3
4
5
6
7
8
9
GET /amazon/_search
{
"query": {
"multi_match": {
"query": "monitor aquarium",
"fields": ["title", "description"]
}
}
}

这个查询对两个字段的重要性相同。让我们看看如何增强一个或多个字段。

(2)增强一个或多个字段

继续上面的示例,如果我们想让 title 字段的匹配比 description 字段的匹配更重要,怎么操作呢?

1
2
3
4
5
6
7
8
9
GET /amazon/_search
{
"query": {
"multi_match": {
"query": "monitor aquarium",
"fields": ["title^3", "description"]
}
}
}

复合搜索

复合查询其实就是把一个或多个查询组合起来,比如一些复合查询将计分查询转换为非计分查询,并将多个计分查询和非计分查询组合在一起。

  • 常数分数查询
  • Bool查询

1. Constant score(常数分数)查询

Elasticsearch 支持结构化查询和全文查询。全文查询需要评分机制来找到最佳匹配的文档,但是结构化查询不需要评分。Constant score 查询允许我们在查询上下文中运行的评分查询转换非评分查询。

例如:术语查询通常在查询上下文中运行。这意味着当Elasticsearch执行一个term查询时,它不仅过滤文档,而且对所有文档进行评分:

1
2
3
4
5
6
7
8
GET /amazon/_search
{
"query": {
"term": {
"manufacturer.raw": "global software publishing"
}
}
}

从输出中,应该可以看出,每个文档都执行了计分

如果我们只是打算过滤文档,不想计分(这会消耗性能)。可以使用 constant_score 。

1
2
3
4
5
6
7
8
9
10
11
12
GET /amazon/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"manufacturer.raw": "global software publishing"
}
}
}
}
}

输出所见,评分都变成了默认的 1.0 ,当然我们也可以手工指定一个 bootst 参数,来改变评分。

1
2
3
4
5
6
7
8
9
10
11
12
GET /amazon/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"manufacturer.raw": "global software publishing"
}
},"boost": 1.2
}
}
}

在单独的这样查询当中,提高分数,是没有什么意义的,当此查询与其他查询相结合时,使用诸如bool查询之类的查询,提高的分数就变得非常重要。与其他查询组合的文档相比,通过此筛选器的所有文档得分都更高。

2. Bool query

布尔查询是最常用的组合查询,不仅将多个查询条件组合在一起,并且将查询的结果和结果的评分组合在一起。当查询条件是多个表达式的组合时,布尔查询非常有用,实际上,布尔查询把多个子查询组合(combine)成一个布尔表达式,所有子查询之间的逻辑关系是与(and);只有当一个文档满足布尔查询中的所有子查询条件时,ElasticSearch引擎才认为该文档满足查询条件。

bool 查询有以下几个部分:

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"must": [...],
"should": [...],
"filter": {},
"must_not": [...]
}
}
}
  • must:子句(查询)必须出现在匹配的文档中。对在查询上下文中执行的查询进行评分,除非整个 bool 查询包含在过滤器上下文中。
  • should:文档应该匹配 should 子句查询中的一个或多个。对在查询上下文中执行的查询进行评分,除非整个 bool 查询包含在过滤器上下文中。
  • filter:子句(查询)必须出现在匹配的文档中。在过滤器上下文中执行非计分查询。
  • must_not: 子句(查询)不能出现在匹配的文档中,在过滤器上下文中执行非计分查询。

[success]一般情况下,should 子句是数组字段,包含多个 should 子查询,默认情况下,匹配的文档必须满足其中一个子查询条件。也可以使用参数minimum_should_match 改变默认行为。bool查询的四个子句,都可以是数组字段,因此,支持嵌套逻辑操作的查询。

布尔查询的各个子句之间是 and 关系,也就是说,一个文档只有同时满足所有的查询子句时,才会作为匹配结果返回。

在布尔查询中,对查询结果的过滤,一般可使用 filtermust_not 子句,因为这两个子句属于过滤上下文,经常使用filter子句,使得ElasticSearch引擎自动缓存数据,当再次搜索已经被缓存的数据时,能够提高查询性能;由于过滤上下文不影响查询的评分,而评分计算让搜索变得复杂,消耗更多CPU资源,因此,filter和must_not查询能减轻搜索的工作负载。

让我们看看如何形成一个只执行结构化搜索的非计分查询。我们将了解如何使用bool查询来构造以下类型的结构化搜索查询:

  • or 条件
  • and 和 or 组合条件
  • Not 条件

(1)or 条件

查找价格范围在 6 到 12 的所有产品,或生产商是 global software publishing 的所有产品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GET /amazon/_search
GET /amazon/_search
{
"query" : {
"constant_score": {
"filter": {
"bool": {
"should":[
{
"range": {
"price": {
"gte": 6,
"lte": 12
}
}
},
{
"term": {
"manufacturer.raw": {
"value": "global software publishing"
}
}
}
]
}
}
}
}
}

should 下的条件默认是为 or,所以我们不需要单独指定,另外,由于放在了 constant_score 下,所以不计分。

(2)and 和 or 组合

查找价格范围在 10 到 30 的所有产品,并且生产商是 global software publishing 或 pinnacle 的所有产品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
GET /amazon/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"must": [
{
"range": {
"price": {
"gte": 10,
"lte": 30
}
}
}
],
"should": [
{
"term": {
"manufacturer.raw": {
"value": "global software publishing"
}
}
},
{
"term": {
"manufacturer.raw": {
"value": "pinnacle"
}
}
}
],
"minimum_should_match" : 1
}
}
}
}
}

[danger]按照正常理解,”minimum_should_match” : 1 应该是一个默认值,但是测试发现,没显示指定该值,should 不起作用。待后续测试

(3)添加 NOT 条件

查找产品价格10-20之间的产品,并且不是 global software publishing 生产的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET /amazon/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"price": {
"gte": 10,
"lte": 20
}
}
}
],
"must_not": [
{
"term": {
"manufacturer.raw": "global software publishing"
}
}
]
}
}
}

[success]注意,当只使用must_not来否定查询时,我们不需要将查询包装在constant_score查询中。must_not查询总是在过滤器上下文中执行。

另外 Elasticsearch 还支持更多的复合查询:

  • Dis_max query
  • Function Score query
  • Boosting query
  • Indices query

官方参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html