天天看點

基于Elasticsearch Phrase Suggester的位址糾錯設計

什麼是phrase suggester?

Suggesters | Elasticsearch Guide [8.6] | Elastic

phrase suggester是基于term Suggester,加入了ngram思想的Suggester設計。

要了解phrase Suggester,必須先了解term Suggester和ngram。

phrase suggester 和 term Suggester的效果示例

asr文本:深圳市 福田區 香蜜湖北路 西園

期望糾錯:深圳市 福田區 香蜜湖北路 熙園

term Suggester

GET address-company-廣東省-深圳市/_search
{
  "suggest": {
    "term_suggestion": {
      "text": "深圳市 福田區 香蜜湖北路 西園",
      "term": {
        "field": "ner",
        "suggest_mode": "popular",
        "size": 20,
        "min_word_length": 2,
        "prefix_length": 0
      }
    }
  }  
}
      

傳回

  "suggest" : {
    "term_suggestion" : [
      {
        "text" : "深圳市",
        "offset" : 0,
        "length" : 3,
        "options" : [ ]
      },
      {
        "text" : "福田區",
        "offset" : 4,
        "length" : 3,
        "options" : [ ]
      },
      {
        "text" : "香蜜湖北路",
        "offset" : 8,
        "length" : 5,
        "options" : [
          {
            "text" : "香蜜湖路",
            "score" : 0.75,
            "freq" : 1228
          },
          {
            "text" : "香蜜湖街道",
            "score" : 0.6,
            "freq" : 37053
          },
          {
            "text" : "香蜜湖大廈",
            "score" : 0.6,
            "freq" : 84
          },
          {
            "text" : "香蜜湖1号",
            "score" : 0.6,
            "freq" : 39
          },
          {
            "text" : "香蜜湖水榭",
            "score" : 0.6,
            "freq" : 33
          },
          {
            "text" : "香蜜湖酒店",
            "score" : 0.6,
            "freq" : 14
          },
          {
            "text" : "香蜜湖體育",
            "score" : 0.6,
            "freq" : 12
          },
          {
            "text" : "香蜜湖較高價的電梯大廈",
            "score" : 0.6,
            "freq" : 9
          },
          {
            "text" : "香蜜湖新村",
            "score" : 0.6,
            "freq" : 8
          },
          {
            "text" : "香蜜湖一号",
            "score" : 0.6,
            "freq" : 6
          },
          {
            "text" : "香梅北路",
            "score" : 0.5,
            "freq" : 292
          },
          {
            "text" : "香密湖路",
            "score" : 0.5,
            "freq" : 11
          }
        ]
      },
      {
        "text" : "西園",
        "offset" : 14,
        "length" : 2,
        "options" : [
          {
            "text" : "家園",
            "score" : 0.5,
            "freq" : 1339
          },
          {
            "text" : "福園",
            "score" : 0.5,
            "freq" : 1033
          },
          {
            "text" : "園西",
            "score" : 0.5,
            "freq" : 890
          },
          {
            "text" : "佳園",
            "score" : 0.5,
            "freq" : 739
          },
          {
            "text" : "名園",
            "score" : 0.5,
            "freq" : 620
          },
          {
            "text" : "南園",
            "score" : 0.5,
            "freq" : 552
          },
          {
            "text" : "松園",
            "score" : 0.5,
            "freq" : 513
          },
          {
            "text" : "圍園",
            "score" : 0.5,
            "freq" : 445
          },
          {
            "text" : "可園",
            "score" : 0.5,
            "freq" : 407
          },
          {
            "text" : "桃園",
            "score" : 0.5,
            "freq" : 404
          },
          {
            "text" : "竹園",
            "score" : 0.5,
            "freq" : 310
          },
          {
            "text" : "蘭園",
            "score" : 0.5,
            "freq" : 277
          },
          {
            "text" : "桂園",
            "score" : 0.5,
            "freq" : 275
          },
          {
            "text" : "公園",
            "score" : 0.5,
            "freq" : 260
          },
          {
            "text" : "北園",
            "score" : 0.5,
            "freq" : 259
          },
          {
            "text" : "樂園",
            "score" : 0.5,
            "freq" : 201
          },
          {
            "text" : "麗園",
            "score" : 0.5,
            "freq" : 182
          },
          {
            "text" : "智園",
            "score" : 0.5,
            "freq" : 172
          },
          {
            "text" : "嘉園",
            "score" : 0.5,
            "freq" : 166
          },
          {
            "text" : "莊園",
            "score" : 0.5,
            "freq" : 140
          }
        ]
      }
    ]
  }
      

可以看到

可園的候選項,傳回的size設定為20,都沒有包含正确答案。(實際 熙園 的排序在70多位,因為詞頻低)。

再來看phrase Suggester的效果

GET address-company-廣東省-深圳市/_search
{
  "suggest": {
    "ner_pinyin_suggest": {
      "text": "深圳市 福田區 香蜜湖北路 西園",

      "phrase":{
        "field": "ner.trigram",
        "gram_size": 3,
        "direct_generator": [ {
          "field": "ner.trigram",
          "suggest_mode": "always",
          "min_word_length": 2,
          "prefix_length": 0,
          "size": 100

        } ],
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        },
        "shard_size": 2000,
        "max_errors": 2,

        "size": 10
      }
    }
  }
  
}
      

傳回

  "suggest" : {
    "ner_pinyin_suggest" : [
      {
        "text" : "深圳市 福田區 香蜜湖北路 西園",
        "offset" : 0,
        "length" : 16,
        "options" : [
          {
            "text" : "深圳市 福田區 香蜜湖街道 熙園",
            "highlighted" : "深圳市 福田區 <em>香蜜湖街道 熙園</em>",
            "score" : 0.07858969
          },
          {
            "text" : "深圳市 福田區 香蜜湖街道 嘉園",
            "highlighted" : "深圳市 福田區 <em>香蜜湖街道 嘉園</em>",
            "score" : 0.07858969
          },
          {
            "text" : "深圳市 福田區 香蜜湖街道 竹園",
            "highlighted" : "深圳市 福田區 <em>香蜜湖街道 竹園</em>",
            "score" : 0.07858969
          },
          {
            "text" : "深圳市 福田區 香蜜湖街道 西路",
            "highlighted" : "深圳市 福田區 <em>香蜜湖街道 西路</em>",
            "score" : 0.07858969
          },
          {
            "text" : "深圳市 福田區 香蜜湖路 熙園",
            "highlighted" : "深圳市 福田區 <em>香蜜湖路 熙園</em>",
            "score" : 0.04161656
          },
          {
            "text" : "深圳市 福田區 香密湖路 西南",
            "highlighted" : "深圳市 福田區 <em>香密湖路 西南</em>",
            "score" : 0.023261044
          },
          {
            "text" : "深圳市 福田區 香蜜湖路 西南",
            "highlighted" : "深圳市 福田區 <em>香蜜湖路 西南</em>",
            "score" : 0.019111233
          },
          {
            "text" : "深圳市 福田區 香蜜湖路 西北",
            "highlighted" : "深圳市 福田區 <em>香蜜湖路 西北</em>",
            "score" : 0.017365677
          },
          {
            "text" : "深圳市 福田區 香密湖路 西北",
            "highlighted" : "深圳市 福田區 <em>香密湖路 西北</em>",
            "score" : 0.017214466
          },
          {
            "text" : "深圳市 福田區 香蜜湖北路 西鄉",
            "highlighted" : "深圳市 福田區 香蜜湖北路 <em>西鄉</em>",
            "score" : 0.002201289
          }
        ]
      }
    ]
  }
      

可以看到

phrase suggester的第一條就是:深圳市 福田區 香蜜湖街道 熙園

雖然 熙園 是低頻詞,但是考慮了ngram之後,得分卻是最高的。

解密Phrase Suggester

GET address-company-廣東省-深圳市

可以檢視索引的資訊

    "mappings" : {
      "properties" : {
        "address" : {
          "type" : "text"
        },
        "area" : {
          "type" : "keyword"
        },
        "city" : {
          "type" : "keyword"
        },
        "id" : {
          "type" : "keyword"
        },
        "ner" : {
          "type" : "text",
          "fields" : {
            "trigram" : {
              "type" : "text",
              "analyzer" : "trigram",
              "search_analyzer" : "whitespace"
            }
          },
          "analyzer" : "whitespace"
        },
        "ner_pinyin" : {
          "type" : "text",
          "fields" : {
            "trigram" : {
              "type" : "text",
              "analyzer" : "trigram",
              "search_analyzer" : "whitespace"
            }
          },
          "analyzer" : "whitespace"
        },
        "province" : {
          "type" : "keyword"
        }
      }
    },
      

我可以看到ner 和 ner_pinyin除了主field(text,whitespace)外,還有 trigram的field,這個trigram是自定義的類型,專門服務phrase suggester。

trigram這個field裡有一個自定義的trigram analyzer。

索引資訊裡可以檢視到這個analyzer的定義:

          "analyzer" : {
            "trigram" : {
              "filter" : [
                "lowercase",
                "shingle"
              ],
              "type" : "custom",
              "tokenizer" : "whitespace"
            },
      

這個analyzer除了使用空格分詞,關鍵使用了filter。

filter 并不是過濾器,更像流式程式設計中的map函數,輸入的token流經常變換得到新的token流  Token filter reference | Elasticsearch Guide [8.6] | Elastic

lowercase非常好了解,就是全部變換為小寫;

shingle是一個自定義的filter

什麼是shingle filter?

shingle就是token ngram(詞級别的ngram)的意思,這個詞來自ES的底層lucene。

自定義的shingle filter如下:

shingle filter的定義

        "analysis" : {
          "filter" : {
            "shingle" : {
              "max_shingle_size" : "3",
              "min_shingle_size" : "2",
              "output_unigrams": false,
              "type" : "shingle"
            }
          },
      

我們可以通過如下方法,形象的檢視和測試shingle filter的行為

如何檢視shingle的行為?

GET /_analyze
{
  "tokenizer": "whitespace",
  "filter": [
    {
      "type": "shingle",
      "min_shingle_size": 2,
      "max_shingle_size": 3,
      "output_unigrams": false
    }
  ],
  "text": "深圳市 福田區 香蜜湖北路 西園"
}
      

輸出如下:

{
  "tokens" : [
    {
      "token" : "深圳市 福田區",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "shingle",
      "position" : 0
    },
    {
      "token" : "深圳市 福田區 香蜜湖北路",
      "start_offset" : 0,
      "end_offset" : 13,
      "type" : "shingle",
      "position" : 0,
      "positionLength" : 2
    },
    {
      "token" : "福田區 香蜜湖北路",
      "start_offset" : 4,
      "end_offset" : 13,
      "type" : "shingle",
      "position" : 1
    },
    {
      "token" : "福田區 香蜜湖北路 西園",
      "start_offset" : 4,
      "end_offset" : 16,
      "type" : "shingle",
      "position" : 1,
      "positionLength" : 2
    },
    {
      "token" : "香蜜湖北路 西園",
      "start_offset" : 8,
      "end_offset" : 16,
      "type" : "shingle",
      "position" : 2
    }
  ]
}
      

如何了解shingle的參數定義

      "min_shingle_size": 2,
      "max_shingle_size": 3,
      "output_unigrams": false
      

根據前面shingle的執行個體輸出,可以發現,這是一個3gram的輸出(但不輸出單詞條,因為output_unigrams為false)

如何提升shingle性能?

shingle是動态生成的,如果需要更高性能,則需要提前預計算,這時可以采用index-phrases。

簡單的說,就是将ngram的輸出在建索引時,就寫在另一個field上,用空間換時間。

index_phrases | Elasticsearch Guide [8.6] | Elastic

shingle和ngram tokenizer的差別?

shingle:token ngram ,是一個基于詞級别的ngram Shingle token filter | Elasticsearch Guide [8.6] | Elastic

ngram tokenizer: char ngram,是一個基于字元級别的ngram N-gram tokenizer | Elasticsearch Guide [8.6] | Elastic

舉例:

GET /_analyze
{
  "tokenizer": {
    "type": "ngram",
    "min_gram": 3,
    "max_gram": 3
  },
  "text": "深圳市 福田區 香蜜湖北路 西園"
}
{
  "tokens" : [
    {
      "token" : "深圳市",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "圳市 ",
      "start_offset" : 1,
      "end_offset" : 4,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "市 福",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : " 福田",
      "start_offset" : 3,
      "end_offset" : 6,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "福田區",
      "start_offset" : 4,
      "end_offset" : 7,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "田區 ",
      "start_offset" : 5,
      "end_offset" : 8,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "區 香",
      "start_offset" : 6,
      "end_offset" : 9,
      "type" : "word",
      "position" : 6
    },
    {
      "token" : " 香蜜",
      "start_offset" : 7,
      "end_offset" : 10,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "香蜜湖",
      "start_offset" : 8,
      "end_offset" : 11,
      "type" : "word",
      "position" : 8
    },
    {
      "token" : "蜜湖北",
      "start_offset" : 9,
      "end_offset" : 12,
      "type" : "word",
      "position" : 9
    },
    {
      "token" : "湖北路",
      "start_offset" : 10,
      "end_offset" : 13,
      "type" : "word",
      "position" : 10
    },
    {
      "token" : "北路 ",
      "start_offset" : 11,
      "end_offset" : 14,
      "type" : "word",
      "position" : 11
    },
    {
      "token" : "路 西",
      "start_offset" : 12,
      "end_offset" : 15,
      "type" : "word",
      "position" : 12
    },
    {
      "token" : " 西園",
      "start_offset" : 13,
      "end_offset" : 16,
      "type" : "word",
      "position" : 13
    }
  ]
}
      

很明顯,ngram的傳回并不是我們預期需要的。

什麼是direct generator?

phrase suggester是基于term suggester的ngram,那麼direct generator就類似term suggester,生成候選集,然後ngram基于這些基礎資料,進行計算。

是以direct generator的配置,要參考term suggester.

但有幾個配置不一樣。

        "direct_generator": [ {
          "field": "ner",
          "suggest_mode": "always",
          "min_word_length": 2,
          "prefix_length": 0,
          "size": 100

        } ]
      

suggest_mode 是選popular 還是 always

term suggester建議為popular,通過更高詞頻來判斷糾錯

但是phrase suggester是基于ngram,有上下文關系,不需要通過不嚴謹的詞頻來設計,是以,應該為always 

field 是否要加.trigram ?

網上的教程會有加.trigram的用法,那到底是用 ner 還是  ner.trigram

我們将ner.trigram 應用在term suggester中,看看其行為

GET address-company-廣東省-深圳市/_search
{
  "suggest": {
    "term_suggestion": {
      "text": "深圳市 福田區 香蜜湖北路 西園",
      "term": {
        "field": "ner.trigram",
        "suggest_mode": "always",
        "size": 80,
        "min_word_length": 2,
        "prefix_length": 0
      }
    }
  }  
}
      

輸出,和ner的差不多,但是,增加了一些:

香蜜湖 1,香蜜湖 店,香蜜湖 北環路 等等的輸出。

很明顯。ner.trigram的行為是,不僅僅用單個詞條作為糾錯,而是可以将後續的2,3個詞,一起作為整體進行糾錯。

如果建索引和搜尋時,采用的是相同粒度的分詞,則采用ner即可。

如果建索引采用細粒度分詞,搜尋的時候,采用粗粒度分詞,則采用ner.trigram 

TODO:待測試對比效果。

phrase suggester的參數如何設定?

gram_size:3

深圳市 福田區 香蜜湖北路 西園

如果不設定,第一條糾錯建議為:深圳市 福田區 香蜜湖街道 西鄉, 也就是unigram的糾錯能力。(西鄉是西園的最高頻單詞條糾錯建議)—— 很奇怪,官方說會從filed的filter中推導這個值,實際不會推導,是以手動設定。

max_errors:2

表示最多糾錯的詞條數量(注意,不是一個詞條内的最大糾錯字數)

舉例:

深圳市 福田區 香蜜湖北路 西園 

因為最大錯誤數量是2,是以可以糾正為:深圳市 福田區 香蜜湖街道 熙園

如果設定為1,則隻能糾正為:深圳市 福田區 香蜜湖北路 西鄉

shard_size:100

每個shard傳回的最大數量的建議詞條,預設是5

如果采用預設值,會發現, 無法将 西園 糾錯為  熙園。 因為,熙園的詞頻低,shard隻傳回了Top 5的詞頻詞條,熙園不在phrase suggester的候選資料裡,是以無法糾正對。

使用collate過濾掉不合理的suggestion

在phrase suggestion的建議中,存在一些不合理的,如:深圳市 福田區 香蜜湖北路 西鄉。(因為 福田區 根本沒有西鄉,西鄉在 寶安區)

這是一個unigram的糾錯(即使shingle設定不輸出unigram,phrase suggester還是會有unigram的糾錯,不知道為什麼)

可以采用collate參數,如下是示例:(具體使用參見:Suggesters | Elasticsearch Guide [8.6] | Elastic)

match_phrase是要求全部精确比對,且詞的順序也要符合的嚴格match模式。

prune預設為false,表示不符合query條件的,不輸出。

這裡設定為true,表示都會輸出,但是輸出增加了collate_match的标記,query比對的為true,不比對的為false,友善調試和做後續的優先級設計等。

(之是以保留不比對的原因如下:

使用者輸入:AAA BXB CCC DDD

語料有:AAA BBB CCC 和  AAA BBB DDD

根據BBB CCC,ES将BXB CCC 修正為 BBB CCC,最終輸出為:AAA BBB CCC DDD

根據match_phrase的全部比對要求,語料裡沒有一條可以和它比對。)

        "collate": {
          "query": { 
            "source" : {
              "match_phrase": {
                "{{field_name}}" : "{{suggestion}}" 
              }
            }
          },
          "params": {"field_name" : "ner"}, 
          "prune": true 
        }
      

Phrase Suggester的打分機制

smooth 模型,預設采用 stupid backoff 。

stupid backoff 比較簡單,比對上3gram是1,比對不上,如果比對上2gram,權重乘以0.4,如果還比對不上,比對unigram,權重在2gram的基礎上,再乘以0.4

詳細可以了解google的論文 https://aclanthology.org/D07-1090.pdf

基于拼音的編輯距離排序

根據phrase suggester的建議,存在高頻詞排序靠前的問題。

輸入:深圳市 龍崗區 龍崗街道 寶平路 五号

期望:深圳市 龍崗區 龍崗街道 寶坪路 五号

phrase suggester 糾錯為:

深圳市 龍崗區 龍崗街道 寶荷路 五号

而ASR位址糾錯的特點是音近,是以,需要加入一個根據編輯距離排序的功能。

排序後,可以得到期望的答案。

繼續閱讀