天天看點

基于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 粒度太細,會增加詞條化的個數,那麼搜尋起來更加的需要去更多的索引中尋找,降低性能

繼續閱讀