一文带你彻底搞懂Elasticsearch中的模糊查询

2021-03-04 14:32:29 浏览数 (1)

写在前面

Elasticsearch(以下简称ES)中的模糊查询官方是建议慎用的,因为的它的性能不是特别好。不过这个性能不好是相对ES自身的其它查询(term,match)而言的,如果跟其它的搜索工具相比ES的模糊查询性能还是不错的。

ES都多种方法可以支持模糊查询,比如wildcard,query_string等,这篇文章可能是全网最全的关于模糊查询的技术博客(哈哈)。

可以支持模糊查询的方案

wildcard

wildcard的用法是这样的,

代码语言:javascript复制
GET kibana_sample_data_flights/_search
{
  "query": {
    "wildcard": {
      "OriginCityName": {
        "value": "Frankfurt*"
      }
    }
  }
}

*或者?也可以放在前面,但是不建议这么做,最好是前缀开始避免太大的性能消耗。查询的字段可以是text类型也可以是keyword类型,两种都支持。

大小写的话默认情况下,是根据字段本身是否对大小写敏感决定的。什么意思呢?比如上面那个查询,OriginCityName字段是keyword类型,我们知道keyword是要求精确匹配,自然就是大小写敏感的。所以如果用下面的这个查询,结果是不一样的:

代码语言:javascript复制
GET kibana_sample_data_flights/_search
{
  "query": {
    "wildcard": {
      "OriginCityName": {
        "value": "frankfurt*"
      }
    }
  }
}

如果查询的字段是text类型,wildcard模糊查询的时候就是大小写不敏感的。

前面说过,模糊查询的性能都不高,wildcard也不例外。不过在ES7.9中引入了一种新的wildcard 字段类型,该字段类型经过优化,可在字符串值中快速查找模式。

代码语言:javascript复制
PUT my-index
{
  "mappings": {
    "properties": {
      "my_wildcard": {
        "type": "wildcard"
      }
    }
  }
}

在引入这个字段类型之前,wildcard要么是在text类型字段查找,要么是keyword类型。而wildcard类型做了特殊的处理,如果某个字段指定了wildcard类型,

  • 与 text 字段不同,它不会将字符串视为由标点符号分隔的单词的集合。
  • 与 keyword 字段不同,它可以快速地搜索许多唯一值,并且没有大小限制。

wildcard字段类型通过两种优化的数据结构提高模糊查询的性能,一种使用n-gram分词器,这个分词器不打算在这里详细讲,只需要知道它会把单词在继续细分存储就行,比如,

代码语言:javascript复制
POST _analyze
{
  "tokenizer": "ngram",
  "text": "Quick Fox"
}

输出的是,

代码语言:javascript复制
[ Q, Qu, u, ui, i, ic, c, ck, k, "k ", " ", " F", F, Fo, o, ox, x ]

相当于把可能用于模糊查询的词项都提前拆分好存储了,这样就减少了查询阶段需要比较的词项。

第二种数据结构是binary doc value,可以自动查询验证由 n-gram 语法匹配产生的匹配候选,关于它的具体介绍可以参考下面这篇文章:

https://www.amazingkoala.com.cn/Lucene/DocValues/2019/0412/49.html

fuzzy

fuzzy也是一种模糊查询,我理解它其实属于比较轻量级别的模糊查询。fuzzy中有个编辑距离的概念,编辑距离是对两个字符串差异长度的量化,及一个字符至少需要处理多少次才能变成另一个字符,比如lucene和lucece只差了一个字符他们的编辑距离是1。

因为可以限制编辑距离,它的性能相对会好一些,毕竟它不是完全的“模糊”。

这样说可能有点抽象,看个例子,

先写入一些测试数据,

代码语言:javascript复制
POST /my_index/_bulk
{ "index": { "_id": 1 }}
{ "text": "Surprise me!"}
{ "index": { "_id": 2 }}
{ "text": "That was surprising."}
{ "index": { "_id": 3 }}
{ "text": "I wasn't surprised."}

然后我们可以这样查询,

代码语言:javascript复制
GET /my_index/_search
{
  "query": {
    "fuzzy": {
      "text": "surprize"
    }
  }
}

查询结果是文档1和文档3会被查询出来,surprise 比较 surprise 和 surprised 都在编辑距离 2 以内。为什么默认值2呢,其实fuzzy有个fuzziness参数,可以赋值为0,1,2和AUTO,默认其实是AUTO。

AUTO的意思是,根据查询的字符串长度决定允许的编辑距离,规则是:

  • 0..2 完全匹配(就是不允许模糊)
  • 3..5 编辑距离是1
  • 大于5 编辑距离是2

其实我们仔细想一下,即使限制了编辑距离,查询的字符串比较长的情况下需要查询的词项也是非常巨大的。所以fuzzy还有一个选项是prefix_length,表示不能被 “模糊化” 的初始字符数,通过限制前缀的字符数量可以显著降低匹配的词项数量。

query string

query string query是ES的一种高级搜索,它支持复杂的搜索方式比如操作符,可以用类似

代码语言:javascript复制
"query": "this AND that"

这样的组合操作语法。

query string支持wildcard,并且查询的字段名和查询字符串都可以使用wildcard,比如:

代码语言:javascript复制
GET /_search
{
  "query": {
    "query_string" : {
      "fields" : ["city.*"],
      "query" : "this AND that OR thus"
    }
  }
}'
代码语言:javascript复制
GET /_search
{
  "query": {
    "query_string" : {
      "query" : "city.\*:(this AND that OR thus)"
    }
  }
}

所以query string对模糊搜索的支持本质上还是wildcard。

prefix 前缀查询

这种只支持前缀查询,属于模糊查询的子集。比如要查找所有以 W1 开始的邮编,可以使用简单的 prefix 查询。

代码语言:javascript复制
GET /my_index/_search
{
    "query": {
        "prefix": {
            "postcode": "W1"
        }
    }
}

prefix的工作原理这里也简单说下。我们知道文档在写入ES时会建立倒排索引,倒排索引都会将包含词的文档 ID 列入 倒排表(postings list),下面是一个示例:

Term

Doc IDs

"SW5 0BE"

5

"W1F 7HW"

3

"W1V 3DG"

1

"W2F 8HW"

2

"WC1N 1LZ"

4

查询的步骤是:

  1. 扫描postings list并查找到第一个以 W1 开始的词。
  2. 搜集关联的文档 ID 。
  3. 移动到下一个词。如果这个词也是以 W1 开头,查询跳回到第二步再重复执行,直到下一个词不以 W1 为止。

可以看到,如果倒排表比较大,满足前缀的词项比较多的情况下,查询的代价也是非常大的。不过对于前缀查询ES提供了一种名叫index_prefixes的机制来提高查询性能。

原理也比较简单,就是字段在mapping中指定index_prefixes,然后ES在索引的时候就会把指定范围的前缀都先存起来,这样查询的时候需要比较的次数就会大大降低。

代码语言:javascript复制
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "body_text": {
        "type": "text",
        "index_prefixes": { }    
      }
    }
  }
}
regexp正则表达式模糊查询

regexp对模糊查询的支持更智能,它能支持更为复杂的匹配模式。比如下面这个示例

代码语言:javascript复制
GET /my_index/_search
{
    "query": {
        "regexp": {
            "postcode": "W[0-9]. " 
        }
    }
}

这个正则表达式要求词必须以 W 开头,紧跟 0 至 9 之间的任何一个数字,然后接一或多个其他字符。

regexp 查询的工作方式与 prefix 查询基本是一样的,需要扫描倒排索引中的词列表才能找到所有匹配的词,然后依次获取每个词相关的文档 ID。


参考:

  • https://www.elastic.co/guide/en/elasticsearch/reference/7.11/index.html
  • https://www.elastic.co/cn/blog/find-strings-within-strings-faster-with-the-new-elasticsearch-wildcard-field

0 人点赞