基于elasticsearch最新版本7.x的ngram分词场景分析

业务场景:输入任意字符查询到结果

1 车牌的搜索   沪A3SD42
2 名字的搜索   张三、李四、王五
3 证件号码的搜索 110234294234234234.....
4 介绍一下常用的两种分词器区别:ik_max_word、ik_smart
 ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。使用场景
两种分词器使用的最佳实践是:索引时用ik_max_word,在搜索时用ik_smart。
即:索引时最大化的将文章内容分词,搜索时更精确的搜索到想要的结果。

那么基于平时正常的一些分词来分析:

    ik_max_word 分词结果:
    沪A3SD42                                =>  ["沪","a3sd42","3","sd","42]
    李四                                     =>  ["李四","四"]
    1102342942342342341                     => ["110234294234234234","0"]

当然ik_smart也是类似分词


介绍完常用的分词情况,那么针对上述场景搜索分析,实际场景下会输入任意字符数字进行查询到结果,
当按照上述分词,进行搜索,发现部分字符就算单一的match匹配搜索竟然搜索不到

POST my_test/_search
{
  "query": {
    "match": {
      "license_plate": "A"
    }
  }
}

搜索结果为空,同理搜索其它字段类似结果,why?
实际上在文档写入的时候就按照分词规则把文档切割成一个一个的词语进行建立了索引进行保存.
但是词语切割是按照语义来切分.例如车牌的切分:[“沪”,“a3sd42”,“3”,“sd”,“42”]
可以看到切割的值很乱,大写变小写,切割的词组也不是很理想,这个东西已经初始化到内存中.所以当我们搜索的时候会根据我们传入的词语进行切割去匹配,当满足其中一个就会返回,上述查询条件传入A,实际上查询就是"a"这个字符串,因为"a"并不在原来的词组当中,所以查询不到结果.

尝试一下存在词组的结果搜索

POST my_test/_search
{
  "query": {
    "match": {
      "license_plate": "沪"
    }
  }
}

现在的搜索就可以得到我们想要的结果.

可是业务场景是任意字符都要拿到结果.那么现有的分词并不满足我们想要的结果,所以按照搜索原理来分析。在写入数据的分词的就已经不满足我们的需求了,按照我们的场景希望是把每个字分成一个词语来进行索引存储.采用match_phrase 进行任何字符输入都可以拿到这个结果就是我们想要的场景

那么从ES索引建立考虑大的范围来说,这是一个很细粒度的切分,会占用大量的内存消耗,需要从性能 搜索场景 以及效率来分析 那么我们简单来分析一下这个车牌所占用的内存

沪A3SD42 分解成 沪 A 3 S D 4 2 7位 (汉字+字符+数字 构成)
按照es倒排索引原理来说同一个词会放在一起,看一下车牌总数量

沪:(代表省份,全国差不多23个省)
字母:(这里索引初始化,统一小写,所以就只有26个字母)
数字:(0-9)
根据上述总结分析,实际上分词的个数也就23+26+10=不到70个字符索引

ES自带的ngram分词可以做到单个词语的解析:


1 什么是ngram

参考:ngram官方地址

例如英语单词 quick,5种长度下的ngramngram length=1,q u i c k

ngram length=2,qu ui ic ck
ngram length=3,qui uic ick
ngram length=4,quic uick
ngram length=5,quick

2、什么是edge ngramquick这个词,抛锚首字母后进行ngram

q
qu
qui
quic
quick

使用edge ngram将每个单词都进行进一步的分词和切分,用切分后的ngram来实现前缀搜索推荐功能

hello world
hello we
h
he
hel
hell
hello    doc1,doc2

w         doc1,doc2
wo
wor
worl
world
e       doc2

比如搜索hello w
doc1和doc2都匹配hello和w,而且position也匹配,所以doc1和doc2被返回。搜索的时候,不用在根据一个前缀,然后扫描整个倒排索引了;简单的拿前缀去倒排索引中匹配即可,如果匹配上了,那么就完事了。

3、最大最小参数

min ngram = 1
max ngram = 3

最小几位最大几位。(这里是最小1位最大3位)
比如有helloworld单词那么就是如下

h
he
hel

最大三位就停止了。

4、试验一下ngramPUT /my_index

{
  "settings": {
    "analysis": {
      "filter": {
        "autocomplete_filter" : {
          "type" : "edge_ngram",
          "min_gram" : 1,
          "max_gram" : 20
        }
      },
      "analyzer": {
        "autocomplete" : {
          "type" : "custom",
          "tokenizer" : "standard",
          "filter" : [
            "lowercase",
            "autocomplete_filter"
          ]
        }
      }
    }
  }}
  PUT /my_index/_mapping/my_type
{
  "properties": {
      "title": {
          "type":     "string",
          "analyzer": "autocomplete",
          "search_analyzer": "standard"
      }
  }}

注意这里search_analyzer为什么是standard而不是autocomplete?因为搜索的时候没必要在进行每个字母都拆分,比如搜索hello w。
直接拆分成hello和w去搜索就好了,没必要弄成如下这样:

h
he
hel
hell
hello   

w

弄成这样的话效率反而更低了。插入4条数据

PUT /my_index/my_type/1{
  "title" : "hello world"}

PUT /my_index/my_type/2{
  "title" : "hello we"}

PUT /my_index/my_type/3{
  "title" : "hello win"}

PUT /my_index/my_type/4{
  "title" : "hello dog"}

执行搜索
GET /my_index/my_type/_search

{
  "query": {
    "match_phrase": {
      "title": "hello w"
    }
  }}

结果

{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 1.1983768,
    "hits": [
      {
        "_index": "my_index",
        "_type": "my_type",
        "_id": "2",
        "_score": 1.1983768,
        "_source": {
          "title": "hello we"
        }
      },
      {
        "_index": "my_index",
        "_type": "my_type",
        "_id": "1",
        "_score": 0.8271048,
        "_source": {
          "title": "hello world"
        }
      },
      {
        "_index": "my_index",
        "_type": "my_type",
        "_id": "3",
        "_score": 0.797104,
        "_source": {
          "title": "hello win"
        }
      }
    ]
  }}

match_phrase会按照分词的结果进行顺序验证,当一一满足则返回我们想要的结果,现在
ngram分词我们明白了那么尝试之前的业务场景.
针对我们的场景对ngram分词做出分析场景:
1 我们只需要单个分词,所以粒度控制在1即可 当大于1针对类似场景会产生大量的索引(数学中的组合情况) 我们采用match_phrase每个认知即可
2 车牌是大写默认转小写,所以搜索的时候一定要统一化(搜索,写入都是统一默认小写,看场景)

5 建立索引 Mapping:

PUT my_test
{
  "settings": {
    "number_of_shards": 5,
    "analysis": {
      "tokenizer": {
        "ngram_tokenizer": {
          "type": "nGram",
          "min_gram": 1,
          "max_gram": 1,
          "token_chars": [
            "letter",
            "digit"
          ]
        }
      },
      "analyzer": {
        "license_plate_analyzer": {
          "tokenizer": "ngram_tokenizer",
          "filter": [
          <u>#转大写操作===调试记得删除此行</u>
            "uppercase"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "license_plate": {
        "type": "text",
        "analyzer": "license_plate_analyzer",
        "search_analyzer": "license_plate_analyzer"
      },
      "name": {
        "type": "text",
        "analyzer": "license_plate_analyzer",
        "search_analyzer": "license_plate_analyzer"
      },
      "certificate": {
        "type": "text",
        "analyzer": "license_plate_analyzer",
        "search_analyzer": "license_plate_analyzer"
      }
    }
  }
}

写入文档:

POST my_test/_doc/1
{
  "license_plate": "沪A3SD42",
  "name": "李四",
  "certificate": "110234294234234234"
}

搜索:

POST my_test/_search
{
  "query": {
    "match_phrase": {
      "license_plate": "4"
    }
  }
}

结果:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "my_test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "license_plate" : "沪A3SD42",
          "name" : "李四",
          "certificate" : "110234294234234234"
        }
      }
    ]
  }
}

换条件搜索:

POST my_test/_search
{
  "query": {
    "match_phrase": {
      "license_plate": "A3"
    }
  }
}


结果:
      {
        "_index" : "my_test",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5753642,
        "_source" : {
          "license_plate" : "沪A3SD42",
          "name" : "李四",
          "certificate" : "110234294234234234"
        }
      }

由此分析此分词器的核心作用:按照自定义想法切分细粒度分词:

针对身份证号码就可以放大位数来切割,例如地区(3-6位)、生日(8位) 最后4位 采用冗余的方案来做

而名字的场景看实际情况拆分,当数据量不大可以采用单个拆分保证及时搜索,
如果是海量数据,可以拆成姓+姓名来做 (汉字的个数还是比较庞大的,姓名都可能会出现),单个粒度不合适

海量数据:上述方案都可以做 ,但是切记数据中一定要有时间范围来控制 某个区间的搜索 ,保证高效稳定。

ngram优缺点(自我理解):
优点:
1 可以根据自己想要的一些特殊属性来切分,达到满足业务场景的需求
2 主要解决一些特殊场景的一些搜索、例如1-N个字符的搜索,或者固定字符的切割搜索
3 包括一些特定的停用词,过滤词等等

缺点:
1 当粒度太细,不一定满足所有的业务场景,导致搜索词条不能精准
2 粒度太细,会增加词条化的个数,那么搜索起来更加的需要去更多的索引中寻找,降低性能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章