ElasticSearch工程实践中的总结

ES in Action

Posted by LiuShuo on September 2, 2020

本文主要总结在平时工作中使用到的ElasticSearch知识点。

问题总结

关于keyword和long的选择问题

  • keyword更适合用作term查询和聚合,long则适合用于range查询。并不是说value是数字就一定用long。 比如一些固定的值完全可以用keyword,如ISBN。
  • long可能并不支持highlight,如果想高亮,需要用keyword
  • 待续。。

DisMaxQuery的使用

DisMaxQuery是一个接受多个查询的查询,并返回与任何查询子句匹配的任何文档。 虽然bool查询组合了所有匹配查询的分数,但dis_max查询使用单个最佳匹配查询子句的分数。

一个查询,它生成由其子查询生成的文档的并集,并为每个文档评分由任何子查询生成的该文档的最大分数, 以及任何其他匹配子查询的平局增量。

当在具有不同增强因子(boost)的多个字段中搜索单词时,这非常有用(因此不能将字段等效地组合到单个搜索字段中)。 我们希望主要分数是与最高提升相关联的分数,而不是字段分数的总和(如BoolQuery布尔查询所给出的)。 如果查询是“albino elephant”,则这确保匹配一个字段的“albino”和匹配另一个的“elephant”获得比匹配两个字段 的“albino”更高的分数。要获得此结果,请同时使用Boolean Query和DisjunctionMax Query: 对于每个术语,DisjunctionMaxQuery在每个字段中搜索它,而将这些DisjunctionMaxQuery的集合组合成BooleanQuery。 

上面是来自官方的翻译,比较晦涩,这里给一个例子,在单字符串的多段搜索时会非常有用。

举一个例子,来自 极客ES12单字符串多字段的查询和DisMaXQuery

在本例中,文档2出现了 brown fox ,而文档1 只出现了brown,所有理论上文档2的相关度应该更高。然而结果正好相反

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
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.90425634,
    "hits" : [
      {
        "_index" : "blogs",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.90425634,
        "_source" : {
          "title" : "Quick brown rabbits",
          "body" : "Brown rabbits are commonly seen."
        }
      },
      {
        "_index" : "blogs",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.77041256,
        "_source" : {
          "title" : "Keeping pets healthy",
          "body" : "Granted quick brown fox eats rabbits on a regular basis."
        }
      }
    ]
  }
}

这与Bool查询的算分过程有关:

  • 1.查询should语句中的两个查询
  • 2.加和两个查询的平分
  • 3.乘以匹配语句的总数
  • 4.除以所有语句的总数

所以,在2个字段都都含有 brown 的文档一比在一个字段中含有 brown fox的文档二的得分高,这显然是不合理的。

DisJunction Max Query查询

上例中,title和body互相竞争,不应该简单分数叠加,而是应该找到单个最佳匹配的字段的评分 这次2的结果就是最高的了。

最佳字段查询调优

通过tie breaker调优 有时,我们也需要其他字段来影响最终的得分,使用 tie_breaker参数来做到这一点

文档1和文档2虽然最佳匹配字段都是title且得分相同,但是文档1拥有更高的得分,因为文档1的body字段含有quick而文档2不包含,这个字段上的得分会乘以 tie_breaker并累计到最终得分上去。

因为DisMaxQuery本身不支持Filter,但是可以用一种组合BoolQuery的方式, how-to-use-dismax-query-with-filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
    "query": {
        "bool": {
            "must": {
                "dis_max": {
                    "queries": [{
                         "multi_match": {}
                     },
                     { 
                         "multi_match": {}
                     }]
                }
            }
            "filter": {
                //Term filter
            }           
        }
    }
}

keyword不支持分词有什么办法吗

使用了keyword之后, 只能用作聚合、排序,不能用来做全文匹配。

为了能够既支持聚合、排序,也支持全文搜索, 那么可以通过使用fields属性来补充支持可以被全文检索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"mappings": {
    "my_type": {
        "properties": {
            "title": {
                "type": "keyword",
                "fields": {
                    "make_title_searchable": {
                        "type": "text"
                    }
                }
            }
        }
    }
}

nested类型

nested嵌套类型是object中的一个特例,可以让array类型的Object独立索引和查询。 使用Object类型有时会出现问题,比如文档 my_index/my_type/1的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT my_index/my_type/1
{
  "group" : "fans",
  "user" : [ 
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]

user字段会被动态添加为Object类型。 最后会被转换为以下平整的形式:

keyword和text

Elasticsearch 5.0.0 版本之后 将string拆分成两个新的类型: text和keyword。

text默认结合standard analyzer(标准解析器)对文本进行分词、倒排索引,默认结合标准分析器进行词命中、词频相关度打分。 。而keyword直接将完整的文本保存到倒排索引中,用于筛选数据(例如: select * from x where status=’open’)、排序、聚合(统计)。

should的特殊地方

返回的文档可能满足should子句的条件。在一个Bool查询中,如果没有must或者filter,有一个或者多个should子句,那么只要满足一个就可以返回。minimum_should_match参数定义了至少满足几个子句。

property中fields属性的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"nickname": {
    "type": "text",
    "fields": {
      "ik": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "ngram": {
        "type": "text",
        "analyzer": "nickname_ngram_analyzer",
        "search_analyzer": "keyword_lowercase_analyzer"
      }
    }
  }

这里注意对nickName进行了fields设置,分别设置了名为ik和ngram的两个field,并分别指定了对应的索引和搜索时的分析器:analyzer和search_analyzer属性。 所以可以在索引的时候同时对该字段进行两个fields的独立分词索引,搜索的时候也可以同时对两个field进行查询,只要符合一个就可以,有更好的保证准确性。 再来看一下setting片段:

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
"analysis": {
  "analyzer": {
    "keyword_lowercase_analyzer": {
      "filter": [
        "lowercase"
      ],
      "type": "custom",
      "tokenizer": "keyword"
    },
    "nickname_ngram_analyzer": {
      "filter": [
        "lowercase"
      ],
      "tokenizer": "nickname_ngram_tokenizer"
    },
    "uid_analyzer": {
      "tokenizer": "uid_tokenizer"
    }
  },
  "tokenizer": {
    "uid_tokenizer": {
      "type": "ngram",
      "min_gram": "3",
      "max_gram": "13"
    },
    "nickname_ngram_tokenizer": {
      "type": "ngram",
      "min_gram": "1",
      "max_gram": "13"
    }
  }
}

Reference

本文首次发布于 LiuShuo’s Blog, 转载请保留原文链接.