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

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