【腾讯云ES】基于NGram分词ES搜索性能优化实践

2022-12-05 14:17:05 浏览数 (1)

背景

在商品搜索场景中,需要根据用户输入关键字严格匹配商品数据,而普通的全文检索方式,诸如:match 或者match_pharse,不一定能达到搜索效果。

例如:使用 match api 时,基于 ik_max_word 分词方式对“白色死神”进行分词后,搜索"白色"、"死神"能搜索到,而根据 "白" 进行搜索时,结果确为空。我们可以看看 ik_max_word 策略的分词效果:

代码语言:txt复制
GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "白色死神"
}

分词结果:

代码语言:txt复制
{
  "tokens" : [
    {
      "token" : "白色",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "死神",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    }
  ]
}

ik_max_word主要是基于词库对文本做最细粒度的拆分,只要你输入的内容能匹配上任何一个分词项,就能将文档返回,虽然召回率很高,但不一定满足严格匹配的场景。

关于严格匹配我们很容易就能想到模糊查询,es本身也是能支持模糊查询的:

方案选择

方案一:模糊查询 wildcard && fuzzy

模糊查询的功能有点类似 mysql 中的 like,可以使用正则表达式的通配符来达到模糊搜索的效果,使用的姿势如下:

代码语言:txt复制

GET my-index/_search

{

"query": {

代码语言:txt复制
"wildcard": {
代码语言:txt复制
  "product_title": {
代码语言:txt复制
    "value": "*白*"
代码语言:txt复制
  }
代码语言:txt复制
}

}

}

代码语言:txt复制
wildcard 能同时支持 text 和 keyword 两种类型的搜索,但是当输入字符串很长或者搜索数据集很大时,搜索性能很低,原因是ES使用的是基于DFA的文本匹配算法,时间复杂度(M N),当索引里面的数据量为K时,时间复杂度为(M N)× K,数据量越大,输入文本越长,模糊搜索的效率就会越低。

###模糊查询性能数据:

对2000万数据进行模糊搜索:**耗时3s **

"took" : 3714,

"timed_out" : false,

"_shards" : {

代码语言:txt复制
"total" : 48,
代码语言:txt复制
"successful" : 48,
代码语言:txt复制
"skipped" : 0,
代码语言:txt复制
"failed" : 0

}

代码语言:txt复制

对2亿数据进行模糊搜索:耗时30s

代码语言:txt复制
 "took" : 32714,
  "timed_out" : false,
  "_shards" : {
    "total" : 48,
    "successful" : 48,
    "skipped" : 0,
    "failed" : 0
  }  事实上,当数据量上千万时,使用模糊查询会存在性能瓶颈,集群会出现大量慢查询,甚至把节点内存打爆。方案二:N-gram 分词生产环境我们可以使用 N-gram 来代替 wildcard 实现模糊搜索功能,N-gram 分词器可以通过指定分词步长来对输入文本进行约束切割,本质上也是一种全文搜索。在使用过程中我们可以通过自定义分析器,在创建索引或者更新字段类型时,对它配置使用N-gram进行分词,简单且高效。我们可以看看分词效果:

POST my-index/_analyze

{

"analyzer":"ngram_analyzer",

"text":"理想小韭菜"

}

代码语言:txt复制

其分词结果为:

代码语言:txt复制
 {
  "tokens" : [
    {
      "token" : "小",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "小韭",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "小韭菜",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "韭",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "韭菜",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "菜",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "word",
      "position" : 5
    }
  ]
}##N-gram配置方式配置方式一:创建索引时候直接指定。

PUT test-ngram-v1

{

代码语言:txt复制
    "settings": {
代码语言:txt复制
    "index.max_ngram_diff": 10, //核心参数:ngram最大步长,可以手动配置,默认为1。
代码语言:txt复制
    "analysis": {
代码语言:txt复制
      "analyzer": {
代码语言:txt复制
        "ngram_analyzer" : {
代码语言:txt复制
          "tokenizer" : "ngram_tokenizer" // 配置ngram分词器。
代码语言:txt复制
        }
代码语言:txt复制
      },
代码语言:txt复制
      "tokenizer": {
代码语言:txt复制
      "ngram_tokenizer" : {
代码语言:txt复制
          "token_chars" : [ //指定生成的token应该包含哪些字符.对没有包含进的字符进行分割,默认为[],即保留所有字符。
代码语言:txt复制
            "letter", // 
代码语言:txt复制
            "digit"  // 
代码语言:txt复制
          ],
代码语言:txt复制
          "min_gram" : "1", // 指定最小步长,按需配置。
代码语言:txt复制
          "type" : "ngram", 
代码语言:txt复制
          "max_gram" : "10" // 指定最大步长 ,按需配置,不能超过"index.max_ngram_diff"。
代码语言:txt复制
        }
代码语言:txt复制
      }
代码语言:txt复制
    }
代码语言:txt复制
},
代码语言:txt复制
"mappings" : {
代码语言:txt复制
    "properties" : {
代码语言:txt复制
      "messsage" : {
代码语言:txt复制
        "type" : "text",
代码语言:txt复制
         "norms" : false,
代码语言:txt复制
        "fields" : {
代码语言:txt复制
          "ngram" : {
代码语言:txt复制
            "type" : "text",
代码语言:txt复制
            "analyzer" : "ngram_analyzer" // 配置类型
代码语言:txt复制
          }
代码语言:txt复制
        }
代码语言:txt复制
      },
代码语言:txt复制
       "level" : {
代码语言:txt复制
      "type" : "keyword"
代码语言:txt复制
    }
代码语言:txt复制
    }
代码语言:txt复制
}

}

代码语言:txt复制

配置方式二:通过索引模版指定。

通过索引模版可以对指定的字段配置ngram分词器,通过 template 中的"match"来指定需要配置的字段,能支持字段类型、字段名、路径、正则等多种匹配条件,也可以配置filter来对分词后token进行后置处理。具体使用方式可以参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/dynamic-templates.html

代码语言:txt复制
PUT _index_template/analyzer-template
{
  "priority": 1,
  "template": {
    "settings": {
      "index": {
        "max_result_window": "200000",
        "codec": "zstandard",
        "max_ngram_diff": "10",
        "analysis": {
          "analyzer": {
            "my_analyzer": {
              "tokenizer": "my_tokenizer"
            }
          },
          "tokenizer": {
            "my_tokenizer": {
              "token_chars": [
                "letter",
                "digit"
              ],
              "min_gram": "1", // 按需修改
              "type": "ngram",
              "max_gram": "5" //按需修改
            }
          }
        }
      }
    },
    "mappings": {
      "dynamic_templates": [
        {
          "strings": {
            "mapping": {
              "type": "keyword"
            },
            "match_mapping_type": "string"
          }
        },
        {
          "ngram_analyzer": {
            "mapping": {
              "type": "text",
              "fields": {
                "ngram": {
                  "analyzer": "ngram_analyzer",
                  "type": "text"
                }
              }
            },
            "match": [
              "message" // 按需配置
            ]
          }
        }
      ]
    }
  },
  "index_patterns": [
    "*"
  ]
}

##NGram查询性能:

2亿数据查询性能:P99 < 200ms

代码语言:txt复制

"took" : 98,

"timed_out" : false,

"_shards" : {

代码语言:txt复制
"total" : 48,
代码语言:txt复制
"successful" : 48,
代码语言:txt复制
"skipped" : 0,
代码语言:txt复制
"failed" : 0

},

代码语言:txt复制

#总结 :

1. 使用 wildcard 不需要做分词,不需要额外占用磁盘,但数据量大时搜索性能很差,小规模业务可以使用。

2. Ngram搜索性能要远远高于 wildcard,但会额外消耗10%左右磁盘(并不明显),可以配合一些数据压缩策略使用。

3. Ngram 能够同时支持 match 与 term 查询,重建索引后,客户端无需变动。

4. 直接使用 Ngram 分词,单个关键字命中即返回,召回错误率太高,可以搭配使用 match_phrase,通过设定slot偏移量,可以减少智能分词结果差异导致的召回率低的问题,提升搜索准确率。

0 人点赞