基于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地址纠错的特点是音近,因此,需要加入一个根据编辑距离排序的功能。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值